You are on page 1of 267

Vedran Mornar

Uvod u programiranje
Radna verzija udžbenika

Zagreb, 2018.
© 2018. Vedran Mornar
Sadržaj
Predgovor ........................................................................................................................................................... 1
Povijesni razvoj računarstva ....................................................................................................................... 3
Prikaz informacija u računalu.................................................................................................................. 15
Brojevni sustavi ........................................................................................................................................ 16
Bistabil ......................................................................................................................................................... 16
Binarni brojevni sustav ......................................................................................................................... 17
Registar ........................................................................................................................................................ 20
Aritmetika u binarnom brojevnom sustavu .................................................................................. 20
Negativni binarni brojevi ...................................................................................................................... 22
Oktalni brojevni sustav.......................................................................................................................... 25
Heksadekadski brojevni sustav .......................................................................................................... 26
Binarni razlomci ....................................................................................................................................... 27
Množenje i dijeljenje s potencijama broja 2 .................................................................................. 29
Realni brojevi standardne preciznosti ............................................................................................ 29
Posebni slučajevi ................................................................................................................................. 33
Raspon i preciznost brojeva standardne preciznosti............................................................ 34
Preciznost i točnost ............................................................................................................................ 34
Apsolutne i relativne greške ........................................................................................................... 35
Realni brojevi dvostruke preciznosti ............................................................................................... 36
Realni brojevi produljene preciznosti ............................................................................................. 36
Slova, znamenke i ostali znakovi ....................................................................................................... 37
Memorija (spremnik) računala .......................................................................................................... 44
Uvod u programiranje ................................................................................................................................ 46
Algoritam .................................................................................................................................................... 46
Program i programiranje ...................................................................................................................... 49
Prvi program u C-u, pseudokôd i dijagram toka .......................................................................... 49
Stilovi pisanja programa ....................................................................................................................... 55
Programske greške ................................................................................................................................. 57
Osnove pisanja programa u jeziku C ..................................................................................................... 59
Funkcija main ............................................................................................................................................ 59
Komentari ................................................................................................................................................... 59
Varijable i konstante............................................................................................................................... 60
Ključne riječi .............................................................................................................................................. 61
Koraci prevođenja ................................................................................................................................... 62
Naredbe pretprocesora .............................................................................................................................. 64
Naredba #include ..................................................................................................................................... 64
Naredba #define ....................................................................................................................................... 65
Uvjetno prevođenje................................................................................................................................. 66
Tipovi varijabli i konstanti ........................................................................................................................ 68
Naredba typedef........................................................................................................................................ 70
Enumeracije ............................................................................................................................................... 70
Operatori i izrazi ........................................................................................................................................... 72
Operator pridruživanja ......................................................................................................................... 72
Aritmetički operatori, eksplicitna i implicitna pretvorba tipa ............................................... 73
Numeričke greške ............................................................................................................................... 76
Unarni operatori ...................................................................................................................................... 78
Operatori uvećanja i umanjenja za 1 ........................................................................................... 78
Operator sizeof ..................................................................................................................................... 79
Prioritet i asocijativnost aritmetičkih operatora ........................................................................ 79
Relacijski operatori ................................................................................................................................. 80
Logički operatori ...................................................................................................................................... 81
Prioritet i asocijativnost logičkih i relacijskih operatora ......................................................... 84
Operatori nad bitovima ......................................................................................................................... 85
Skraćeno pridruživanje ......................................................................................................................... 87
Ternarni operator.................................................................................................................................... 88
Odjeljivanje naredbi zarezom ............................................................................................................. 88
Kontrola programskog toka ..................................................................................................................... 89
Naredba if.................................................................................................................................................... 89
Skretnica ..................................................................................................................................................... 94
Programske petlje ................................................................................................................................... 97
Petlja s ispitivanjem uvjeta ponavljanja na početku petlje – naredba while................ 97
Petlja s ispitivanjem uvjeta ponavljanja na kraju petlje – naredba do ........................... 99
Programska petlja s poznatim brojem ponavljanja .............................................................101
Naredba break ....................................................................................................................................105
Naredba continue ..............................................................................................................................107
Naredba goto ...........................................................................................................................................108
Funkcije ..........................................................................................................................................................110
Implicitna konverzija argumenata i rezultata ............................................................................114
Funkcije bez rezultata ..........................................................................................................................115
Funkcije bez parametara ....................................................................................................................115
Funkcije s varijabilnim brojem argumenata ...............................................................................116
Parametri i lokalne varijable funkcije ............................................................................................117
Mehanizam poziva funkcije ...............................................................................................................118
Rekurzivne funkcije ..............................................................................................................................122
Inline funkcije ..........................................................................................................................................126
Makro s parametrima...........................................................................................................................127
Organizacija složenih programa ...........................................................................................................130
Prototip funkcije ....................................................................................................................................130
Doseg i vrijeme života varijabli .............................................................................................................132
Globalne varijable ..................................................................................................................................134
Smještajni razred register...................................................................................................................135
Smještajni razred static .......................................................................................................................135
Funkcije iz standardne biblioteke ........................................................................................................137
Matematičke funkcije ...........................................................................................................................138
Funkcije nad znakovima .....................................................................................................................140
Izlazak iz programa ...............................................................................................................................142
Pokretanje drugog programa ............................................................................................................143
Generiranje pseudoslučajnih brojeva ............................................................................................144
Pokazivači ......................................................................................................................................................150
Tipovi pokazivača ..................................................................................................................................152
Aritmetika s pokazivačima .................................................................................................................153
Pokazivači na pokazivače ...................................................................................................................156
Pokazivač NULL ......................................................................................................................................157
Promjena vrijednosti argumenata funkcije .................................................................................157
Polja .................................................................................................................................................................160
Jednodimenzijska polja fiksne veličine .........................................................................................161
Indeksni izrazi .........................................................................................................................................166
Znakovni nizovi ......................................................................................................................................168
Jednodimenzijska polja varijabilne veličine ................................................................................170
Jednodimenzijska polja i funkcije ....................................................................................................171
Funkcije iz standardne biblioteke za rukovanje nizovima ....................................................176
Dvodimenzijska i višedimenzijska polja fiksne veličine .........................................................180
Dvodimenzijska i višedimenzijska polja varijabilne veličine ...............................................185
Dvodimenzijska i višedimenzijska polja i funkcije ...................................................................186
Složeni podaci ..............................................................................................................................................190
Strukture ...................................................................................................................................................190
Unije ............................................................................................................................................................198
Skupine bitova ........................................................................................................................................199
Ulaz i izlaz ......................................................................................................................................................202
Unos i ispis pojedinačnog znaka ......................................................................................................202
Unos i ispis niza ......................................................................................................................................203
Formatirani ulaz i izlaz ........................................................................................................................205
Trajna pohrana podataka ...................................................................................................................216
Ulazni i izlazni tokovi podataka .......................................................................................................219
Čitanje i pisanje tekstovnih datoteka .............................................................................................223
Čitanje i pisanje binarnih datoteka .................................................................................................228
Slijedne i direktne datoteke ...............................................................................................................231
Naredbeni redak ....................................................................................................................................237
Dinamička rezervacija memorije..........................................................................................................240
Funkcije malloc, calloc i free ..............................................................................................................240
Funkcija realloc.......................................................................................................................................244
Rukovanje greškama .................................................................................................................................250
Kompleksni brojevi ....................................................................................................................................252
Tablica prioriteta operatora ...................................................................................................................257
Popis slika......................................................................................................................................................258
Popis programa ...........................................................................................................................................260
Literatura .......................................................................................................................................................262
Uvod u programiranje Predgovor

Predgovor
Pred vama je radno, nelektorirano izdanje udžbenika Uvod u programiranje. Bez svake
sumnje puno je grešaka koje će se postepeno otklanjati. Hoće li ikada do kraja biti
uklonjene, teško je reći, jer jedan od stavaka poznatog Murphyjevog zakona1 nepogrešivo
kaže "There is always one more bug". Zato se preporuča čitanje prvenstveno elektroničke
inačice, koja će se stalno dopunjavati.
Udžbenik pokriva, a u nekim dijelovima i proširuje, gradivo iz predmeta Uvod u
programiranje koji se od akademske godine 2018./2019. predaje na Sveučilištu u
Zagrebu, Fakultetu elektrotehnike i računarstva. Tekst udžbenika, međutim, ne slijedi
ritam predavanja (ili obratno) iz jednostavnog razloga: na predavanjima je moguće do
neke mjere obraditi temu, prijeći na obradu neke druge teme i potom se vratiti i detaljnije
obraditi onu prvu temu. Kod udžbenika je takav pristup teže ostvariti.
Uvodni dio knjige sadrži kratki povijesni pregled razvoja računarstva koji nije dio ispitnog
gradiva, ali je ovdje da se prisjetimo ljudi koji su razmišljali ispred svoga vremena i koji
su još u XIX. i sredinom XX. stoljeća postavili osnovne koncepte arhitekture računala od
kojih se ni danas nismo suštinski odmaknuli. Ogroman je napredak postignut na brzini,
kapacitetu, komunikacijama, ulaznim i izlaznim jedinicama, ali ne i na osnovnoj logici
rada.
Dio ovdje obrađenog gradiva nekome će se možda činiti suvišan. Programerima kojima je
svejedno kako će brzo program raditi, koliko će računalnih resursa potrošiti i koji misle
da će po potrebi moći uvjeriti svoje nadređene da kupe jače i brže računalo, mnogo će toga
ovdje napisanoga biti nezanimljivo ili čak glupo. Tekst je namijenjen onima koji iz
postojećih resursa žele izvući maksimum, onima koji moraju ili žele prilagoditi svoj
program njima raspoloživom računalu ili mikrokontroleru, umjesto da postupe obratno,
tj. da napišu program ne vodeći o tome računa i da zatim traže računalo na kojem će njihov
proizvod dovoljno brzo raditi. Ovaj će im tekst pomoći da bolje razumiju međudjelovanje
hardvera i softvera, što je posebno važno u današnje vrijeme u kojem je razlika između
elektrotehnike i računarstva postala suptilna ili teško uočljiva.
Pokriveni su, jasno, univerzalni koncepti programiranja kao što su to varijable, konstante,
operatori, funkcije, naredbe za kontrolu programskog toka, polja, složeni podaci, ulaz i
izlaz, ali je jedan dio knjige posvećen i prikazu informacija u računalu, a gdje god se moglo,
pokušalo se objasniti što se događa "iznutra" kad se, primjerice, radi s poljem ili s
datotekama na disku, što se događa kad se pokazivačem skrene "u krivo", kako dinamički
rezervirati memoriju i koji problemi pri tome mogu nastati.
Programiranje se, međutim, ne može naučiti iz knjige. Tu neće pomoći ni fotografsko
pamćenje. Svaki je problem iz stvarnog života (pa i na ispitu) drukčiji, pa naučiti kako se
rješava jedan nije nikakva garancija da će se znati riješiti drugi. Da bi se naučilo
programirati, treba što više – programirati. I uložiti trud da se ovdje sadržano gradivo
razumije, a ne nauči napamet. Potonje je, osim što je beskorisno, i mnogo teže.

1 Skup uzrečica koje se mogu svesti pod zajednički nazivnik "Sve što može poći po zlu, poći će po zlu"
("Anything that can go wrong will go wrong"). Edward Aloysius Murphy Jr. (11.01.1918.– 17.07.1990.) bio
je američki inženjer zrakoplovstva i aeronautike koji je radio na sigurnosno osjetljivim sustavima. Upravo
je računarstvo jedna od grana ljudskih djelatnosti u kojima se se taj "zakon" pokazao u potpunosti
utemeljenim.

1
Uvod u programiranje Predgovor

Sastavni dio ovog udžbenika čini devedesetak programa, od jednostavnih do donekle


složenih, koji se nalaze u mapi Primjeri. Uz svaki primjer u tekstu stoji naziv datoteke u
kojoj je pohranjen izvorni kôd, kao cjelina koja se može prevesti i izvesti. Preglednik PDF
dokumenata, nažalost, ne dopušta da se iz dokumenta izravno pokrene neki program, pa
će programe trebati otvoriti "ručno". Svakako se preporuča izvesti što više njih i pratiti
tijek izvođenja u programu za traženje grešaka (debuggeru), ali i samostalno riješiti što
više zadataka i problema, od kojih će neki biti objavljeni u repozitoriju predmeta.
Preporuča se rad u razvojnom okruženju Visual Studio Code s prevodiocem gcc jer je riječ
o besplatnim programima otvorenog koda koje se može naći u inačicama za sve poznatije
operacijske sustave. Upute za instalaciju i rad s ovim alatima nalaze se u posebnim
dokumentima u repozitoriju.

U Zagrebu, rujna 2018.

2
Uvod u programiranje Povijesni razvoj računarstva

Povijesni razvoj racunarstva


Ljudska potreba za brojanjem vjerojatno je starija i od pisane povijesti. Da bi na vrijeme
organizirali svoje vjerske obrede i proslave još su plemenski vračevi morali na neki način
brojati dane, mjesece, godišnja doba. Počeci brojanja gotovo sigurno su vezani uz uporabu
prstiju. Engleska riječ digit, koja etimološki vuče porijeklo od latinskog naziva za prst,
digitus, ravnopravno označava prst ljudske ruke ili noge, odgovarajući organ ostalih
kralješnjaka, te znamenku dekadskog ili nekog drugog sustava brojanja. Postepeno, kako
su količine koje je trebalo prebrojati rasle, u uporabu se uvode i ostali materijali iz
neposrednog čovjekova okoliša. Posebno praktični pokazali su se kamenčići, koji su pred
prstima imali barem dvije prednosti: moglo ih se skupiti više nego što ima prstiju na
rukama i nogama, a nisu odmah mijenjali položaj i time zaboravljali pohranjenu
informaciju kad je trebalo zapaliti vatru ili uloviti plijen. Engleska riječ calculate, računati,
ima korijen u latinskoj riječi calculus, koja označava kamenčić s kojim se računalo. Za
trajniju pohranu informacija, u prikladni materijal urezivali su se zarezi. U tadašnjoj
Čehoslovačkoj je 1937. godine pronađena vučja kost stara oko 20.000 godina, na kojoj je
urezano 55 zareza u grupama po 5. Ova se kost smatra najstarijim nalazom sustavnog
brojanja u svijetu.
Uporaba kamenčića pri računanju oko 6000. godine p.n.e. rezultira i prvim abax
uređajima. Grčka riječ abax (ἄβαξ) označava ploču za računanje, na kojoj su urezani utori
po kojima kližu kamenčići. Kamenčići predstavljaju različite numeričke veličine. Koncept
je poboljšavan tijekom vremena i koristio se u Mezopotamiji oko 5000. godine p.n.e.
Osnovna slabost sustava bila je sporost rada - prebrzo pomicanje kamenčića često je
rezultiralo njihovim ispadanjem iz utora. Ideja da se kamenčići probuše i nataknu na
štapove koji će se, da bi se spriječilo ispadanje, ugraditi u okvir, vjerojatno se rodila u Kini.
Naprava je nazvana abacus, i još je uvijek u uporabi u zemljama Dalekog Istoka. Osoba
vješta u korištenju abacusa u brzini operacija zbrajanja i oduzimanja ne zaostaje za
elektroničkim kalkulatorom.

Slika 1. Abacus (Pixabay, Creative Commons CC0)

3
Uvod u programiranje Povijesni razvoj računarstva

Dugo se nije pojavilo učinkovitije mehaničko računalo. Sljedećim napretkom smatraju se


štapovi Johna Napiera2, načinjeni 1612. godine, koji su služili kao pomoć pri množenju
višeznamenkastih brojeva jednoznamenkastim. Ti su štapovi korišteni u nastavi
matematike u osnovnim školama u Engleskoj do sredine šezdesetih godina XX. stoljeća.
Godine 1622. John Oughtred 3 načinio je analogno računalo zasnovano na Napierovim
logaritmima koje je, zavisno od veličine, imalo preciznost od 3 do 4 dekadske znamenke.
Računalo s preciznošću od 4 dekadske znamenke bilo je dugačko 2,5 m.
Wilhelm Schickard 4 1623. godine poboljšava koncept Napierovih štapova omogućivši
množenje višeznamenkastih brojeva.
Koristilo se kao glavno inženjersko pomagalo pri računanju u XIX. i većem dijelu XX.
stoljeća. Na Elektrotehničkom fakultetu u Zagrebu (danas FER-u) sve do sedamdesetih
godina prošlog stoljeća polagao se kolokvij iz korištenja takvog logaritamskog računala,
popularno zvanog šiber. Studenti koji nisu uspijevali položiti kolokvij, gubili su pravo
studija.
Godine 1642., tada 18-godišnji Blaise Pascal5, sin skupljača poreza, načinio je računalo da
pomogne ocu u svakodnevnom poslu. Računalo, nazvano Pascaline, imalo je međusobno
povezane brojčanike, od kojih je svaki predstavljao dekadsku znamenku. Kad bi jedan
brojčanik načinio puni krug (10 koraka), okrenuo bi sljedeći za 1 korak. Ideja je to koja se
i danas koristi pri mehaničkim brojačima okretaja. Računalo je bilo ograničeno samo na
zbrajanje.

Slika 2. Pascaline (autor Rama, CC BY-SA 30. FR)

2 John Napier (1550., Edinburgh – 04.04.1617., Edinburgh), škotski matematičar, pronalazač logaritama
3 John Oughtred (05.03.1574., Eton – 30.06.1660., Albury), engleski matematičar
4Wilhelm Schickard (22.04.1592., Herrenberg – 23.10.1635., Tübingen), njemački astronom, matematičar i
kartograf
5 Blaise Pascal (19.06.1623., Clermont - 19.08.1662., Paris), francuski matematičar.

4
Uvod u programiranje Povijesni razvoj računarstva

Godine 1673. Gottfried Wilhelm von Leibniz6 poboljšao je Pascalov koncept omogućivši i
operaciju množenja.
Godine 1801. Joseph-Marie Jacquard 7 načinio je automatski tkalački stan upravljan
bušenim karticama kojima se definirao uzorak na tkanini. Taj je izum izazvao nerede jer
su se radnici pobojali da će ostati bez posla.
Godine 1822. Charles Babbage 8 započinje razvoj
diferencijalnog stroja. Babbage, sin bankara, boležljiv
u djetinjstvu, rano se počinje baviti matematikom. Na
početku svoga studija na Trinity koledžu u
Cambridgeu znao je matematiku i bolje od nekih
nastavnika. Primijetivši da su navigacijske tablice
pune grešaka koje su dovodile do gubitaka brodova,
osmišljava koncept parnog uređaja koji bi mogao
automatski izračunavati vrijednosti iz navigacijskih
tablica. Od engleske vlade dobiva financijsku potporu
za svoje istraživanje, što se može smatrati prvim
državnim financiranjem na području računarstva.
Godine 1828. postaje profesor matematike na
sveučilištu u Cambridgeu, ali nikada ne predaje jer ga
cijelog zaokuplja razvoj stroja. Bavi se još i
magnetizmom, filozofijom, i (neuspješno) politikom.
Godine 1834. objavljuje svoje najznačajnije djelo On Slika 3. Charles Babbage (Public Domain)
the Economy of Machinery and Manufactures, u kojem
predlaže rani oblik operacijskih istraživanja, znanstvene i stručne discipline koja na
temelju egzaktnih metoda pomaže pri donošenju odluka. Mrzi glazbu, što susjedi znaju i
često mu pod prozor šalju ulične svirače. U travnju 1833. obustavlja rad na
diferencijalnom stroju, koji je trebao obavljati jednu operaciju, jer dolazi do nove i bolje
ideje, koju naziva analitičkim strojem. Ponovno od vlade traži financiranje, što engleski
parlament ovoga puta odbija, jer prethodni projekt nije bio dovršen.

6 Gottfried Wilhelm von Leibniz (01.06.1646., Leipzig – 14.11.1716., Hannover), njemački matematičar i
filozof
7 Joseph-Marie Jacquard (07.06.1752., Lyons - 07.08.1834., Oullins), francuski tkalac svile
8 Charles Babbage (26.12.1792., Teignmouth – 18.10.1871., London), engleski matematičar

5
Uvod u programiranje Povijesni razvoj računarstva

Analitički stroj Charlesa Babbagea zapanjujuće je


koncepcijom sličan modernim računalima. Osnovna
namjena bila mu je izračunati rezultat bilo koje
matematičke funkcije nad bilo kojim skupom ulaznih
vrijednosti obavljajući elementarne računske
operacije slijedom koji je zadao matematičar. Imao je
dva osnovna dijela: procesni dio (Mill) i memoriju
(Storage). Bio je upravljan s tri vrste bušenih kartica:
karticama s ulaznim podacima (Number Cards),
karticama koje određuju gdje smjestiti podatke
(Directive Cards) i karticama koje određuju što
načiniti s podacima (Operation Cards). Za rješenje
jedne klase problema priprema se slijed kartica koje
određuju smještaj i operacije, i s tim se skupom
kartica može riješiti proizvoljno mnogo problema
iste klase zamjenom kartica s ulaznim podacima.
Slika 4. Analitički stroj, Science Museum Babbage, nažalost, nije dobro opisao mogućnosti
London (autor Marcin Wichary, San Francisco,
U.S.A, CC BY 2.0)
stroja, pa njegova ideja, zbog nerazumijevanja, nije
bila široko prihvaćena. Stroj za života, djelomično i
zbog nedostataka tadašnje tehnologije, nije završio. Godine 1847. vraća se planovima
diferencijalnog stroja, radi nacrte za drugu inačicu, prema kojima je tek u XX. stoljeću
načinjen funkcionalni stroj.
U zanimljivom članku Analitički stroj, general major Henry Prevost Babbage, Charlesov
sin, 1888. godine objašnjava programiranje stroja na primjeru izračunavanja izraza
(ab + c) d.
Vrijednosti a, b, c i d valja izbušiti na kartice i staviti redom na ulaz stroja. Potrebno je
pripremiti sljedeći slijed kartica o smještaju i upravljačkih kartica:
Smještaj Operacija Objašnjenje
1. - Smješta prvu ulaznu veličinu (a) u memoriju 1
2. - Smješta drugu ulaznu veličinu (b) u memoriju 2
3. - Smješta treću ulaznu veličinu (c) u memoriju 3
4. - Smješta četvrtu ulaznu veličinu (d) u memoriju 4
5. - Dohvaća a iz memorije u procesni dio (Mill)
6. - Dohvaća b iz memorije u procesni dio
1. Množi a i b
7. - Sprema rezultat p u memoriju 5
8. - Dohvaća p iz memorije u procesni dio
9. - Dohvaća c iz memorije u procesni dio
2. Zbraja p i c
10. - Sprema rezultat q u memoriju 6
11. - Dohvaća q iz memorije u procesni dio
12. - Dohvaća d iz memorije u procesni dio
3 Množi p i d
13. - Sprema rezultat r u memoriju 7
14. - Ispisuje rezultat r
Može se uočiti da praktički nema razlike između ovdje opisanog slijeda naredbi i
asemblerskog programa na modernim računalima. Babbage mlađi priznaje da je
prethodni postupak kompliciran, ali samo za jednokratno izračunavanje. Zanimljiva je u

6
Uvod u programiranje Povijesni razvoj računarstva

istome članku spomenuta moguća primjena: opisani postupak itekako bi se isplatio ako
bi, primjerice, osiguravajuće društvo trebalo izračunati ratu za policu životnog osiguranja
za sve svoje klijente.
Bilo je, međutim, ljudi koji su odmah prepoznali potencijale
analitičkog stroja. Neki su matematičari toga vremena počeli
pisati programe za analitički stroj i prije nego što je stroj
stvarno izrađen. Ada Augusta Byron9, grofica Lovelace, bila
je jedna od prvih. Ada je bila kći iz kratkog braka pjesnika
Lorda Byrona i Anne Isabelle Milbanke. Byron, nemirna
duha, rastao se od Anne mjesec dana nakon Adinog rođenja.
Nakon 4 mjeseca zauvijek je napustio Englesku i umro u
Grčkoj od groznice u ratu za oslobođenje od Turaka 1823.
godine. Ada ga nikad nije upoznala. Majka je željela da Ada
ne bude poput svoga oca, te je inzistirala na obrazovanju u
matematici i glazbi. Sa 17 godina, Ada susreće Charlesa
Babbagea i započinje s njime opsežnu korespondenciju o
matematici i logici. Godine 1842. talijanski matematičar
Luigi Menabrea10 napisao je rad na francuskom jeziku Skica
analitičkog stroja koji je izumio Charles Babbage.
Babbage je angažirao Adu za prevođenje na engleski, pri
Slika 5. Ada Lovelace (Public Domain)
čemu je ona dodala niz korisnih opaski.
Razumjela je ideju stroja, ali čak i bolje od Babbagea, njegovu perspektivu – prepoznala
ga je kao računalo opće namjene kojim se može izraziti funkcija bilo kojeg stupnja
složenosti ili općenitosti. U svojim je opaskama prepoznala čak i moguću primjenu
računala u glazbi. Kao ni Babbage, ni Ada nije dočekala da se stroj dovrši. Umrla je u
Londonu od raka u 37. godini života i sahranjena je kraj oca u Nottinghamu.
Godine 1854. George Boole11 opisuje svoj sustav simboličke logike, na kojem se zasniva
programiranje i dizajn modernih računala.
Godine 1890. Herman Hollerith 12 načinio je elektromehanički uređaj koji je koristio
bušene kartice. Namjena toga uređaja bila je ubrzanje postupka brojanja glasova u SAD.
Brojanje glasova je na ovaj način bilo tri puta brže od ručnog prebrojavanja. Potaknut
uspjehom, Herman Hollerith osniva Tabulating Machine Company, tvrtku koja prodaje
njegove strojeve vladama diljem svijeta. U nacističkoj Njemačkoj ti su strojevi odigrali
značajnu ulogu u evidenciji i progonu Židova. 1924. godine kompanija mijenja ime u
International Bussiness Machines (IBM), koji je i danas jedna od vodećih kompanija u
informacijskim tehnologijama.

9 Ada Augusta Byron (10.12.1815., London – 27.11.1852., London), engleska matematičarka


10 Luigi Federico Menabrea (04.09.1809., Chambéry– 24.05.1896., Saint-Cassin), talijanski general, političar
i matematičar
11 George Boole (02.11.1815., Lincoln – 08.12.1864., Ballintemple), engleski matematičar
12 Herman Hollerith (29.02.1860., Buffalo – 17.11.1929., Washington DC), američki statističar

7
Uvod u programiranje Povijesni razvoj računarstva

Slika 6. Hollerithov stroj (Public Domain)

Utemeljiteljem modernog računarstva smatra se Alan


Mathison Turing13. U školi je bio prosječan učenik, lošeg
rukopisa, mučio se s engleskim. Matematičke probleme
rješavao je na svoj nekonvencionalan način. Bio je
zainteresiran i za kemiju, gdje je mnogo
eksperimentirao.
Diplomira 1934., bavi se teorijom vjerojatnosti i
nezavisno postavlja centralni granični teorem14. Taj je
značajni teorem, međutim, bio otkriven neposredno
prije, pa nije nikad pripisan Turingu.
Godine 1936. objavljuje članak On Computable Numbers,
with an application to the Entscheidungsproblem 15 , u
kojem opisuje apstraktni stroj, danas nazvan
Turingovim strojem.
Godine 1939. započinje Drugi svjetski rat i Turing se
zapošljava u vojsci, gdje njegovom genijalnošću Slika 7. Alan Turing (Public Domain)
dešifriraju mnoge njemačke poruke. Zajedno s W. G.
Welchmanom16 razvija stroj The Bombe, koji je od 1940. godine uspješno dekodirao sve
poruke kodirane Enigmom njemačkih zračnih snaga (Luftwaffe). Stroj Enigma njemačke
mornarice (Kriegsmarine) imao je snažnije kodiranje, ali je Turing statističkim pristupom
i te kodove razbio do sredine 1941. Radi i na razvoju stroja Collosus, dovršenog 1943. i
korištenog u dekodiranju poruka prije invazije u Normandiji. Postojanje toga stroja čuvalo
se kao tajna sve do 1970., a algoritmi korišteni pri dekodiranju ni danas nisu objavljeni.

13 Alan Mathison Turing (23.06.1912., London – 07.06.1954., Wilmslow), engleski matematičar i filozof
14Suma velikog broja proizvoljno distribuiranih nezavisnih slučajnih varijabli daje slučajnu varijablu koja
se ravna po normalnoj razdiobi
15Entscheidungsproblem – problem odlučivanja: U simboličkoj logici valja pronaći univerzalni algoritam
koji će odrediti je li općeniti logički izraz univerzalno valjan. Turing je dokazao da takav algoritam ne postoji.
16 William Gordon Welchman (15.06.1906., Bristol – 08.10.1985., Newburyport), engleski matematičar

8
Uvod u programiranje Povijesni razvoj računarstva

Poslije rata Turing je pozvan u National Physical Laboratory u London da sudjeluje u


projektu novog računala, nazvanog Automatic Computing Engine (ACE). Zbog za to
vrijeme preambicioznih Turingovih zahtjeva na kapacitet računala, projekt je odgođen.
Ozbiljno se, da odagna stres izazvan poslom, počinje baviti i atletikom, gdje postiže
zavidne rezultate. Njegovo najbolje vrijeme maratona samo je 11 minuta slabije od
vremena olimpijskog pobjednika 1948. Da proširi vidike, na Cambridgeu 1947.-48.
studira i neurologiju i fiziologiju.
1950. godine objavljuje djelo Computing machinery and intelligence gdje predviđa što će
se događati s razvojem strojne inteligencije. Predlaže tzv. Turingov test: ispitivač preko
terminala postavlja pitanja na koje s druge strane odgovaraju računalo i čovjek. Ako
ispitivač ne može odrediti tko je tko, računalo je inteligentno.
Godine 1952. uhapšen je zbog homoseksualnosti. Na suđenju 31. ožujka 1952. ne poriče
krivnju, i može birati između godine dana zatvora i hormonskih injekcija. Izabire potonje,
i nastavlja akademski rad na kvantnoj teoriji i teoriji relativnosti. Nastavlja rad na
kodiranju u vrijeme hladnog rata. Službu sigurnosti počinju brinuti njegove međunarodne
veze, čemu su pridonijeli i praznici koje je proveo u Grčkoj godine 1953.
Godine 1954. pronađen je mrtav u svojoj sobi, otrovan cijanidom. Kraj njega je nađena
napola pojedena jabuka. Istraga zaključuje da je riječ o samoubojstvu.
Turingov stroj jednostavan je zamišljeni stroj koji se sastoji od trake, glave za čitanje i
pisanje, te programa. Pamti jedan jedini podatak – stanje stroja. Traka je neograničene
duljine, a sadrži konačni broj simbola nakon kojih slijede praznine. Broj i vrijednosti
simbola mogu se mijenjati tijekom izvođenja programa. Glava za čitanje i pisanje čita i
analizira simbole s trake i u ovisnosti o trenutačnom stanju stroja može upisati novi
simbol preko trenutačnog, izmijeniti trenutačno stanje stroja, ili se pomaknuti lijevo ili
desno po traci. Program predstavlja konačni slijed instrukcija, koje nalažu glavi što će
pisati i kako će se pomicati u ovisnosti od simbola na traci i stanju stroja.
Ako je programiran ispravno, ovaj jednostavni stroj može riješiti bilo koji problem kao i
današnje računalo.
Funkcionalnost Turingova stroja ilustrirat će se na primjeru uvećavanja binarnog broja
za 1. Na početku posla, glava za čitanje nalazi se nad krajnjom desnom znamenkom broja,
a stroj se nalazi u stanju A. U stanju A učitava se sadržaj trake i ispituje njegova vrijednost.
Ako je vrijednost 1, na trenutačnu poziciju trake upisuje se vrijednost 0, glava se pomiče
za jedno mjesto ulijevo, a stroj ostaje u stanju A. Ako je učitana vrijednost 0, na trenutačnu
poziciju trake upisuje se vrijednost 1, a stroj prelazi u završno stanje, stanje B.

9
Uvod u programiranje Povijesni razvoj računarstva

A 0 0 1 1

1
A 0 0 1 0
B A
0 0 0 0 0
A
Upiši 0
Upiši 1
Pomakni glavu lijevo
B 0 1 0 0

Slika 8. Program za Turingov stroj

Godine 1935. Konrad Zuse 17 konstruira relejno računalo Z-1 koje koristi binarnu
aritmetiku. Nastavlja razvoj stroja, koji rezultira 1938. godine računalom Z-2. Na početku
Drugog svjetskog rata traži pomoć za daljnji razvoj od njemačke vlade, koja ga odbija jer
smatra da će rat završiti prije nego što se dovrši novi stroj. Zuse odlazi u Švicarsku gdje
osniva računalnu tvrtku, koju će kasnije preuzeti Siemens.
Godine 1937. Howard Aiken18 na Harvardskom sveučilištu i uz pomoć IBM-a započinje
razvoj elektromehaničkog uređaja Harvard Mark I. Stroj je koristio bušene kartice i bio u
uporabi u američkoj mornarici do 1959. godine. Iste godine i George Stibitz 19 u Bell
Telephone Laboratories razvija relejno računalo koje proziva Model-K, jer je načinjeno na
kuhinjskom (kitchen) stolu.
Na sveučilištu Iowa State 1939. godine započine razvoj prvog elektroničkog digitalnog
računala, specijaliziranog za rješavanje sustava linearnih jednadžbi. U poslu sudjeluju
John Vincent Atanasoff 20 i Clifford Berry 21 , koji računalo nazivaju Atanasoff & Berry
Computer (ABC). Stroj nikad nije do kraja završen.
John W. Mauchly22 i John Prosper Eckert23 s University of Pennsylvania's Moore School
1943. godine započinju za vojne potrebe razvoj novog elektroničkog računala ENIAC
(Electronic Numerical Integrator and Computer). ENIAC je prvo funkcionalno elektroničko
računalo.
Sastojalo se od 17468 elektronskih cijevi, bilo je teško 30 t, trošilo je 174 kW električne
energije, a zauzimalo je prostoriju veličine 10x15 m. Radilo je 1000 puta brže od računala

17 Konrad Zuse (22.06.1910., Berlin - 18.12.1995., Hünfeld), njemački inženjer građevine i izumitelj
18 Howard Hathaway Aiken (08.03.1900., Hoboken– 14.03.1973., St.Louis), američki fizičar
19 George Robert Stibitz (30.4.1904., York – 31.1.1995., Hanover), američki inženjer i istraživač
20John Vincent Atanasoff (04.10.1903., Hamilton– 15.06.1995., Frederick), američki fizičar i izumitelj
bugarskog podrijetla
21 Clifford Berry (19.04.1918., Gladbrook - 30.10.1963., New York City), američki inženjer elektrotehnike i
fizičar
22 John William Mauchly (30.08.1907., Cincinatti, - 08.01.1980., Ambler), američki fizičar
23John Adam Presper "Pres" Eckert Jr. (09.04.1919., Philadelphia - 03.06.1995., Bryn Mawr), američki
inženjer elektrotehnike

10
Uvod u programiranje Povijesni razvoj računarstva

Mark I. Bilo je namijenjeno rješavanju balističkih problema, ali kako je završeno tek 1946.,
u izvornu svrhu nije korišteno. Namjena mu je preinačena u procjenjivanje efekata
atomskih napada. Programiralo se fizičkim spajanjem komponenata, pa splet žica na
sljedećoj slici predstavlja program.

Slika 9. Programiranje Eniaca (Public Domain)

Temelje moderne arhitekture elektroničkih računala postavio je John von Neumann24 .


Rođen u Budimpešti kao Johann von Neumann, sa šest godina znao je dijeliti
osmeroznamenkaste brojeve. Studirao je i diplomirao kemiju, a doktorirao matematiku
1928. godine. Pozvan je na Princeton 1930. godine gdje 1933. postaje redovitim
profesorom, što ostaje do kraja života. Radi na matematičkoj podlozi za kvantnu fiziku. S
Turingom surađuje na umjetnoj inteligenciji.
Oko 1940. godine s teorijskih razmatranja naglo prelazi na primijenjena. Za vrijeme rata
sudjeluje u izradi atomske, a nakon rata i hidrogenske bombe, gdje osjeća potrebu za
korištenjem računala. Predlaže sljedeću arhitekturu elektroničkog računala:

24 John von Neumann (28.12.1903., Budimpešta – 08.02.1957., Washington DC), američki matematičar i
fizičar

11
Uvod u programiranje Povijesni razvoj računarstva

Podaci i Rezultati
Ulazni dio Memorija Izlazni dio
instrukcije

Argumenti Instrukcije
Rezultati

Aritmetičko
logička jedinica
Upravljačka
jedinica

Procesor
Slika 10. Von Neumannova arhitektura

Središte računala predstavlja procesor, koji se sastoji od upravljačke i logičke jedinice.


Upravljačka jedinica upravlja ostalim dijelovima računala, što je na slici prikazano
isprekidanim linijama. Pune linije prikazuju tokove podataka. Programske instrukcije i
ulazni podaci prihvaćaju se na ulaznom dijelu računala i prosljeđuju u memoriju.
Instrukcije se redom dohvaćaju iz memorije i šalju upravljačkoj jedinici. Argumenti
računskih operacija dohvaćaju se iz memorije u aritmetičko-logičku jedinicu koja obavlja
računske operacije, čiji se rezultati vraćaju u memoriju. Ovisno o rezultatu računske
operacije, dohvatit će se i obaviti sljedeća instrukcija programa, ili će program nastaviti
rad s nekom instrukcijom drugdje u programu. Konačno, rezultati programa prosljeđuju
se u izlazni dio računala koji se brine za njihovo predočavanje.
Važno je primijetiti da se baš svi, pa i najsloženiji, programi izvršavaju na temelju ovih
jednostavnih principa. U suštini, arhitektura modernih procesora relativno je malo
modificirana u odnosu na Von Neumannovu25.
Na osnovi ovako postavljene arhitekture, John von Neumann je 1945. razvio EDVAC
(Electronic Discrete Variable Automatic Computer), prvo računalo koje je izvršni program
pohranjivalo u radnoj memoriji i koje je koristilo magnetske diskove.
U ovoj kratkoj povijesti računarstva nije moguće zaobići Grace Murray Hopper26. Rođena
u New Yorku, doktorirala je matematiku na sveučilištu Yale. Godine 1943. godine stupa u
američku mornaricu. Nakon rata prelazi u pričuvni sastav i predaje na Harvardu. Od 1949.
radi i u industriji. Umirovljena kao kapetan korvete (Commander) godine 1966., sljedeće
godine biva vraćena u aktivnu službu kao direktor Mornaričke grupe za programske

25 Harvardska arhitektura, na primjer, memoriju dijeli na dio za instrukcije i dio za podatke te omogućava
istovremeni prijenos instrukcije i podatka iz memorije u procesor. Modificirana harvardska arhitektura,
prema kojoj su načinjeni gotovo svi moderni procesori, dodaje mogućnost pristupa instrukcijama kao da su
podaci.
26 Grace Murray Hopper (09.12.1906., New York – 01.01.1992., Alexandria), časnica američke mornarice

12
Uvod u programiranje Povijesni razvoj računarstva

jezike. Postaje kapetan bojnog broda (Captain) godine


1973., komodor (Commodore) godine 1983. i
kontraadmiral (Rear-admiral) godine 1985.
Kao poručnica, 9. rujna 1945. sa svojim timom otkriva na
Harvardu razlog prestanka rada stroja Mark II Aiken Relay
Calculator. Bio je to moljac zaglavljen između kontakata
releja br. 70 na panelu F.
Moljca su zalijepili u dnevnik uz opasku First actual case of
bug being found. Od tada se termin bug (buba, stjenica)
uvriježio u računarstvu i elektrotehnici kao o sinonim za
grešku. Danas je ta stranica u Nacionalnom muzeju
američke povijesti u Washingtonu.
Slika 11. Grace Hopper (Public Domain)

Slika 12. Prvi bug (Public Domain)

Grace Hopper brzo uviđa da bi računala pronašla širu primjenu kad bi ih bilo
jednostavnije programirati. Godine 1949. predlaže stvaranje programskih biblioteka, i
sudjeluje u stvaranju prvog prevodioca (compiler) nazvanog A-O, koji je prevodio
simboličke naredbe u strojni kôd.
Odigrala je i značajnu ulogu u stvaranju programskog jezika COBOL i dobila nadimak
Grandma COBOL. Poznata je po izvrsnim i traženim predavanjima, gdje često analogijama
i primjerima objašnjava različite koncepte. Primjerice, da bi vizualizirala razliku između
nanosekunde i mikrosekunde, na predavanje donosi stopu žice (udaljenost koju elektron

13
Uvod u programiranje Povijesni razvoj računarstva

prijeđe u nanosekundi) i kolut od 1000 stopa žice (udaljenost koju elektron prijeđe u
mikrosekundi).
Izumom tranzistora, 23. prosinca 1947. nastala je
golema prekretnica u razvoju računarstva. Za taj su
izum Walter Bratain 27 , John Bardeen 28 i Wiliam
Shockley29 iz Bell laboratorija 1956. godine nagrađeni
Nobelovom nagradom za fiziku. Elektronička računala
do tada sastavljena od nezgrapnih elektronskih cijevi,
koja su trošila nevjerojatne količine električne
energije, postala su pristupačnija.

Slika 13. Bardeen, Shockley, Brattain (Public Domain)

Pionirska vremena računarstva mogu se možda zaključiti izradom prvog komercijalnog


računala na osnovi poluvodiča, UNIVAC I (Universal Automatic Computer), izrađenog u
tvrtki Remington Rand 1951. godine, koje je načinjeno u 46 primjeraka.
Smisao nabrajanja pojedinačnih modela računala ovdje prestaje. Razvoj je sve brži,
pogotovo nakon 1959. godine kada je Jack Kilby30 izradio prototip integriranog sklopa.
Godine 1965. Gordon E. Moore 31 predviđa da će se broj tranzistora u integriranim
sklopovima udvostručavati svakih 18 mjeseci. Novinari ovu tvrdnju prozivaju Mooreovim
zakonom. O tome koliko je dalekovidna bila ta tvrdnja, govori činjenica da vrijedi još i
danas: procesor SPARC M7 s 32 jezgre, načinjen 2015. godine, sadrži 10,000,000,000
tranzistora, a procesor AMD Epyc s 32 jezgre, načinjen 2017. godine, sadrži
19,200,000,000 tranzistora. Za usporedbu, procesor Intel 8086 iz 1978. imao je samo
29,000 tranzistora, a Pentium iz 1993. 3,100,000 tranzistora.
Na kraju, jedno računalo ovdje se ne može ne spomenuti. Godine 1968. Elektrotehnički
fakultet sveučilišta u Zagrebu za tadašnjih oko 100.000 dolara32 nabavlja IBM 1130, čime
je započeo ozbiljni razvoj računarstva u Hrvatskoj. Računalo je radilo na taktu procesora
od 500 kHz, imalo je 64 kB središnje memorije i izmjenjive magnetske diskove kapaciteta
1 MB. Programiralo se bušenim karticama, a rezultati su se ispisivali na papir, na
magnetski disk, mogli su se bušiti na kartice ili prikazati grafički koristeći inkrementalni
crtač dvodimenzijskih slika. Bilo je u uporabi punih 18 godina, sve do 1986. godine kada
je u funkcionalnom stanju preseljeno u Tehnički muzej u Zagrebu.

27 Walter Houser Bratain (10.02.1902., Xiamen – 13.10.1987., Seattle), američki fizičar


28 John Bardeen (23.05.1908., Madison – 30.01.1991., Boston) američki fizičar i inženjer elektrotehnike
29 Wiliam Bradford Shockley (13.2.1910., London – 12.8.1989., Stanford) američki fizičar
30Jack St. Clair Kilby (8.11.1923., Jefferson City, 20.06.2005., Dallas) američki izumitelj, dobitnik Nobelove
nagrade za fiziku godine 2000.
31Gordon Earle Moore (3.1.1929, San Francisco) američki kemičar i fizičar, osnivač, predsjednik i počasni
presjednik kompanije Intel
32 Računajući inflaciju, vrijednost od oko 730.000 dolara u 2018. godini.

14
Uvod u programiranje Prikaz informacija u računalu

Prikaz informacija u racunalu


Mnogo se puta na ispitu iz predmeta povezanih s programiranjem, umjesto odgovora na
neko od pitanja o tome kako su podaci određenog tipa spremljeni u računalu, od studenata
moglo čuti prigovaranje. "Što će to meni, to nema veze s programiranjem. Ja znam
programirati i bez toga." neke su od najčešćih rečenica. Mišljenje je pak autora ovih
redaka, da bez osnovnih znanja o prikazu podataka u računalu nije moguće do kraja
razumjeti ponašanje čak niti najjednostavnijih programa, što se lako može dokazati
sljedećim primjerom.
#include <stdio.h>
int main(void) {
short int i;
i = 0;
while (i < 100000) {
i = i + 1;
}
printf("Gotovo!");
return 0;
}
Program 1. CiklusBrojanja.c

I bez poznavanja programskog jezika C lako se intuitivno može razumjeti da bi program


trebao mijenjati vrijednost varijable i od 0 do 100000, te nakon toga ispisati na zaslon
poruku Gotovo!.
Ako se program prevede i pokrene, naizgled se ništa neće dogoditi. Prozor s rezultatima
ostat će otvoren sve dok se na tipkovnici ne pritisne Ctrl-C ili se prozor ne zatvori
pritiskom na dugme x. Može se pomisliti da program nije niti počeo s radom. Pokušat
ćemo saznati što se dogodilo tako da u programu načinimo sljedeću izmjenu:

while (i < 100000) {
printf("%d ", i);
i = i + 1;
}

tj. u svakom koraku programske petlje prikazat ćemo trenutačnu vrijednost varijable i.
Ispis programa sad izgleda ovako:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ... 32765 32766 32767 -
32768 -32767 -32766 -32765 ... -2 -1 0 1 2 3 4 5 6 7 8 9 10 ...
32765 32766 32767 -32768 -32767 -32766 -32765 ... -2 -1 0 1 2 3 4 5
6 7 8 9 10 ...
Uočava se sljedeći ciklus: vrijednost varijable raste od 0 do 32767, zatim naglo padne na
-32768, raste do 32767, naglo padne na -32768 i tako u beskraj. Zaista neočekivano
ponašanje, barem za osobu koja ne razumije kako se u računalu pohranjuju cijeli brojevi,
kako se u računalu obavlja aritmetika i koja ograničenja pri tome postoje.

15
Uvod u programiranje Prikaz informacija u računalu

Brojevni sustavi
Bilo koji podatak možemo prikazati brojevima. Ovo vrijedi i za znakovne podatke, ako se
primijeni prikladan brojčani kôd. Broj se prikazuje nizom znamenki, pri čemu je, osim
vrijednosti znamenke, značajan i položaj znamenke u broju.
U brojevnom sustavu kakvim se svakodnevno služimo, broj 123 možemo prikazati kao
1 ∙ 100 + 2 ∙ 10 + 3
ili
1 ∙ 102 + 2 ∙ 101 + 3 ∙ 100
Broj 10 nazivamo bazom brojevnog sustava. U brojevnom sustavu s bazom 10 moguće su
znamenke: 0, 1, 2, 3, 4, 5, 6, 7, 8 i 9. Takav brojevni sustav nazivamo dekadskim
brojevnim sustavom. Dekadski brojevni sustav vjerojatno je zasnovan na činjenici da dvije
ljudske ruke ukupno imaju 10 prstiju.
Broj od n znamenki u dekadskom brojevnom sustavu možemo općenito napisati kao
zn-1 ∙ 10n-1 + zn-2 ∙ 10n-2 + ... + z1 ∙ 101 + z0 ∙ 100
gdje je zi  { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
Lako se može zamisliti i brojevni sustav s bazom brojanja različitom od 10. Da su se geni
ljudskog genoma posložili malo drugačije, danas bismo se možda svakodnevno koristili
brojevnim sustavom s bazom 8, 12 ili čak 11.
Općenito, može se definirati brojevni sustav s bazom brojanja B i znamenkama
zi  { 0, 1, 2, ... , B-1}
Broj od n znamenki u brojevnom sustavu s bazom B može se napisati kao
zn-1 ∙ Bn-1 + zn-2 ∙ Bn-2 + ... + z1 ∙ B1 + z0 ∙ B0

Bistabil
Direktno korištenje dekadskog brojevnog sustava u elektroničkim računalima današnjice
nije nezamislivo. Štoviše, prva su (mehanička) računala i bila zasnovana na zupčanicima
s deset zubaca. U svijetu elektronike mogao bi se načiniti sklop koji bi pamtio deset
diskretnih vrijednosti kao, primjerice, 10 razina napona, pri čemu bi napon od 0 V
predstavljao znamenku 0, napon od 1 V znamenku 1 tako dalje. Problem ovakvog pristupa
leži, međutim, u složenosti, cijeni i pouzdanosti takvog sklopa. Male promjene napona
mogle bi uzrokovati promjenu vrijednosti zapisane u takvom sklopu. Postoji zato jeftino,
jednostavno, brzo i pouzdano rješenje: bistabil.
Bistabil je jednostavni elektronički sklop koji može pohraniti dva diskretna stanja.
Konkretnu izvedbu ovakvog sklopa zainteresirani će čitatelj pronaći u svakoj knjizi koja
se bavi impulsnom i digitalnom elektronikom. Za naše razmatranje, bistabil možemo
prikazati blok-shemom:
Ulaz D Q Izlaz

Pohraniti

16
Uvod u programiranje Prikaz informacija u računalu

Stanje napona na ulazu zapamtit će se u bistabilu (i time prikazati na izlazu) u trenutku


kad se na priključak Pohraniti dovede naponski impuls. Stanja napona kroz vrijeme na sva
tri priključka može se ilustrirati slikom:

Ulaz

t
Pohraniti

t
Izlaz

t
Slika 14. Stanje bistabila u vremenu

Sa stanovišta informacije, gornja se shema može još pojednostavniti: ako na izlazu


bistabila postoji napon koji je veći od neke granične vrijednosti, smatramo da je u
bistabilu pohranjena znamenka 1, inače 0. Ovakva je izvedba sklopa pouzdana jer je
neosjetljiva na manje promjene napona. Uz vršnu vrijednost napona npr. 5 V, sve napone
ispod 2.5 V možemo smatrati nulom, a napone iznad 2.5 V jedinicom. Do kraja
pojednostavnjeno, bistabil možemo prikazati i pravokutnikom u kojem je zapisana
znamenka:
0 ili 1

Binarni brojevni sustav


Uporabom bistabila kao sklopa za pohranu informacija, omogućili smo prikaz dvije
različite vrijednosti: nule i jedinice. Kako brojevni sustav sa znamenkama 0, ..., n-1 ima
bazu B = n, nula i jedinica čine znamenke brojevnog sustava s bazom B = 2. Takav se sustav
naziva binarnim brojevnim sustavom. Znamenka binarnog brojevnog sustava naziva se
binarnom znamenkom. Iz engleskog izvornog naziva za binarnu znamenku, binary digit,
nastao je naziv bit.
Broj od n znamenki u brojevnom sustavu s bazom 2 može se, dakle, napisati kao:
zn-1 ∙ 2n-1 + zn-2 ∙ 2n-2 + ... + z1 ∙ 21 + z0 ∙ 20, zi  { 0, 1 }
Za pohranu binarnog broja od n znamenki bit će nam potrebno n bistabila.

17
Uvod u programiranje Prikaz informacija u računalu

Kako se u životu prirodno služimo dekadskim brojevima, za pohranu takvih brojeva u


računalo morat ćemo pronaći algoritam pretvorbe dekadskog broja u binarni.
Raspišimo dekadske vrijednosti nekih potencija broja 2:
20 = 1
21 = 2
22 = 4
23 = 8
24 = 16
25 = 32
26 = 64
27 = 128
28 = 256
29 = 512
210 = 1024
211 = 2048
212 = 4096
213 = 8192
214 = 16384
215 = 32768
216 = 65536
...
Za manje brojeve intuitivno se može osmisliti pomalo nespretan, ali korektan postupak.
Pronađe se najveća potencija broja 2 manja ili jednaka broju kojega želimo pretvoriti.
Znamenka binarnog broja uz tu potenciju postavlja se na 1. Od broja se oduzme dekadska
vrijednost pronađene najveće potencije. Postupak se ponavlja sve dok je rezultat
oduzimanja različit od nule.
Primjer: Pretvoriti dekadski broj 57 u binarni.
Najveća potencija broja 2 manja ili jednaka 57 jest 32. Znamenka uz 25 bit će 1.
Od 57 oduzimamo 32. Ostaje 25.
Najveća potencija broja 2 manja ili jednaka 25 jest 16. Znamenka uz 24 bit će 1.
Od 25 oduzimamo 16. Ostaje 9.
Najveća potencija broja 2 manja ili jednaka 16 jest 8. Znamenka uz 23 bit će 1.
Od 9 oduzimamo 8. Ostaje 1.
Najveća potencija broja 2 manja ili jednaka 1 jest 1. Znamenke uz 22 i 21 su 0, a uz 20 bit
će 1.
Od 1 oduzimamo 1. Nema ostatka, pa možemo pisati konačni rezultat:
5710 = 1 ∙ 25 + 1 ∙ 24 + 1 ∙ 23 + 0 ∙ 22 + 0 ∙ 21 + 1 ∙ 20 = 1110012
Postoji, međutim, i bolji algoritam. Potrebno je zato provesti sljedeće razmatranje.

18
Uvod u programiranje Prikaz informacija u računalu

Svaki se cijeli broj N u binarnom sustavu može prikazati kao


N = zn-1 ∙ 2n-1 + zn-2 ∙ 2n-2 + ... + z1 ∙ 21 + z0 ∙ 20
Izlučimo li zajednički faktor 2 iz svih pribrojnika osim posljednjeg dobit ćemo
N = 2 ∙ (zn-1 ∙ 2n-2 + zn-2 ∙ 2n-3 + ... + z1 ∙ 20) + z0 ∙ 20
Prethodni izraz može se napisati kao
N = 2 ∙ q1 + z0
ili
z0 = N - 2 ∙ q1
Kako su N i q1 sigurno cijeli brojevi, a z0 binarna znamenka koja je 0 ili 1, očigledno je z0
ostatak dijeljenja N s 2.
Raspišimo sada izraz za količnik q1 dijeljenja N sa 2:
q1 = zn-1 ∙ 2n-2 + zn-2 ∙ 2n-3 + ... + z2 ∙ 21 + z1 ∙ 20
Iz svih pribrojnika osim posljednjeg može se izlučiti zajednički faktor 2:
q1 = 2 ∙ (zn-1 ∙ 2n-3 + zn-2 ∙ 2n-4 + ... + z2 ∙ 20) + z1 ∙ 20
što se dalje može napisati kao
q1 = 2 ∙ q2 + z1
ili
z1 = q1 - 2 ∙ q2
z1 je, dakle, ostatak dijeljenja q1 s 2.
Iz ovoga slijedi da se binarne znamenke mogu dobiti sukcesivnim dijeljenjem dekadskog
broja s 2 i zapisivanjem ostataka, sve dok se dijeljenjem ne postigne rezultat 0. Pri tome
valja paziti da se ostatci zapisuju s desna na lijevo, jer prvi je ostatak znamenka z0, sljedeći
znamenka z1 itd.
Primjer: Pretvoriti dekadski broj 57 u binarni.
57 : 2 = 28
ostatak 1
28 : 2 = 14
ostatak 0
14 : 2 = 7
ostatak 0
7:2=3
ostatak 1
3:2=1
ostatak 1
1:2=0
ostatak 1
1 1 1 0 0 1

19
Uvod u programiranje Prikaz informacija u računalu

Općenito, ovaj algoritam vrijedi za pretvorbu u bilo koji brojevni sustav: broj se dijeli
bazom ciljnog brojevnog sustava i ostatci dijeljenja zapisuju se zdesna.

Registar
Pretvorba dekadskog broja 57 u binarni rezultirala je povećanjem broja znamenki.
Umjesto 2 dekadske znamenke uporabili smo 6 binarnih znamenki (1110012).
Općenito se može primijetiti sljedeće: najveći dekadski broj s d znamenki iznosi 10d-1
Primjer:
S dvije dekadske znamenke, d=2, može se prikazati najveći broj 99, odnosno 102-1. Koliko
će biti potrebno binarnih znamenki za prikaz istog tog broja?
S n znamenki u brojevnom sustavu s proizvoljnom bazom B može se prikazati najveći broj
Bn-1. Ako se s b označi potreban broj binarnih znamenki, može se postaviti sljedeća
nejednadžba:
10d -1  2b -1  10d  2b  d  b ∙ log 2  b  d : log 2  b ⪆ d ∙ 3,32
Za dvije dekadske znamenke vrijedi
b ⪆ 2 ∙ 3,32 = 6,66
Broj se ne može napisati sa šest znamenki i s još dvije trećine znamenke, pa je iz gornje
jednadžbe jasno da treba uzeti prvi veći cijeli broj znamenki. Ipak, pokušat ćemo prvo sa
6 binarnih znamenki s kojima može prikazati najveći broj
1111112 = 32+16+8+4+2+1 = 63 = 64-1 = 26-1
6 binarnih znamenki očigledno nije dovoljno za prikaz broja 99.
Sa 7 binarnih znamenki možemo prikazati najviše
11111112 = 64+32+16+8+4+2+1 = 127 = 128-1 = 27-1
Općenito, potreban broj binarnih znamenki dobije se zaokruživanjem prethodnog izraza
viši cijeli broj:
b = d ∙ 3,32
Drugim riječima, za pohranu broja od d dekadskih znamenki potrebno je više od 3 ∙ d
bistabila. Teško je, međutim, tehnički ostvariti da se svakoj informaciji dodijeli upravo
potreban broj bistabila. Zato se bistabili grupiraju u grupe, pri čemu se je veličina grupe
obično 8, 16, 32 ili 64. Takva grupa bistabila naziva se registar. U izvedbi registra svi ulazi
Pohrani bistabila registra spojeni su zajedno, dok svaki bistabil ima zasebni ulaz i izlaz.
Pojednostavnjeno, registar možemo promatrati kao skupinu pravokutnika koji sadrže
binarne znamenke.
Primjer: 8-bitni registar koji sadrži dekadski broj 7 može se prikazati kao
0 0 0 0 0 1 1 1
Treba zapamtiti da se s b binarnih znamenki može prikazati najveći cijeli broj 2b-1, što je
ujedno i izraz koji označava najveći broj koji se može pohraniti registru od b bita.

Aritmetika u binarnom brojevnom sustavu


Ako u dekadskom brojevnom sustavu obavimo operaciju zbrajanja dva
jednoznamenkasta broja, rezultat može biti dvoznamenkast. Jednako tako, ako u

20
Uvod u programiranje Prikaz informacija u računalu

brojevnom sustavu s bazom B obavimo zbrajanje dva jednoznamenkasta broja x i y,


rezultat će biti dvoznamenkast ako je x + y >= B. Pri tome je vrijednost posljednje
znamenke (x + y) mod B, a s lijeva nastaje znamenka s vrijednošću (x + y) / B.
Novonastalu znamenku obično nazivamo prijenosom. Pri zbrajanju višeznamenkastih
brojeva, obavlja se zbrajanje parova pojedinačnih znamenki odgovarajuće težine krenuvši
s desna na lijevo, pri čemu se znamenkama na nekom težinskom mjestu dodaje eventualni
prijenos nastao na prethodnom težinskom mjestu.
Binarni brojevni sustav nije izuzetak. Kako su ovdje i znamenke i prijenos nule ili jedinice,
moguće je logičkim elektroničkim sklopovima, u čije detalje ovdje nećemo ulaziti,
jednostavno realizirati sklop za zbrajanje.
Načinimo nekoliko primjera zbrajanja u binarnom brojevnom sustavu.
a) 10012 + 102
Rješenje je jednostavno, jer nema prijenosa.
1001
+ 10
1011
b) 10112 + 12
Rješenje je nešto složenije, jer se prijenos pojavljuje i kod zbrajanja na poziciji 20 i kod
zbrajanja na poziciji 21.
1011
+ 1
1100
c) 10012 + 10102
Prijenos se pojavljuje uz 23, pa rezultat ima više znamenki nego pribrojnici.
1001
+ 1010
10011
Pri zbrajanju u računalu uvijek treba voditi računa o veličini registara u kojima se
pohranjuju pribrojnici i rezultat. Pretpostavimo da ove iste primjere želimo pokazati uz
pretpostavku da se pribrojnici i rezultat pohranjuju u 4-bitnim registrima. Kod primjera
a) i b) nema nikakvih problema. Kod primjera c), međutim, prijenos nastao zbrajanjem
znamenki uz 23 nema se kamo pohraniti.
1 0 0 1

+ 1 0 1 0

1 0 0 1 1
Nastao je preliv (overflow), koji je uzrokom neočekivanog rezultata. Zbrajanjem broja 910
(10012) i 1010 (10102) dobili smo rezultat 310 (112).
Zanimljivo je promatrati ponašanje registra s ograničenim brojem bita kojemu se
sukcesivno dodaje jedinica. Kao primjer, uzet ćemo 3-bitni registar:

21
Uvod u programiranje Prikaz informacija u računalu

Sadržaj Dekadski
registra ekvivalent
0 0 0 0 0

0 0 1 1

0 1 0 2

0 1 1 3

1 0 0 4

1 0 1 5

1 1 0 6

1 1 1 7 23-1
Dodavanjem jedinice na najveću moguću vrijednost pohranjenu u registru dobije se 0.
Brojevi koje je moguće pohraniti u 3-bitnom registru kreću se, dakle, u intervalu [0, 23-1].
Općenito, za n bita, interval je [0, 2n-1].

Negativni binarni brojevi


Kako se u registru mogu pohraniti samo znamenke 0 ili 1, očito je da je za pohranu
negativnog predznaka potrebna neka konvencija.
Jedna od mogućih konvencija mogla bi biti, na primjer, rezervirati jedan bit koji će imati
vrijednost 1 ako je vrijednost negativna, a 0 ako je vrijednost pozitivna. Prirodno je
rezervirati bit s najvećom težinskom vrijednošću, tj. krajnji lijevi bit registra.
Uz primjenu takve konvencije broj 2410 prikazao bi se kao
0 0 0 1 1 0 0 0
dok bi se broj -2410 prikazao kao
1 0 0 1 1 0 0 0
Za svaki prirodni broj mora vrijediti n + (-n) = 0. Ako pokušamo zbrojiti tako prikazane
brojeve 2410 i -2410, rezultat neće biti 0.
0 0 0 1 1 0 0 0

+ 1 0 0 1 1 0 0 0

= 1 0 1 1 0 0 0 0

Kako bismo dobili rezultat 0, valjalo bi sklopovski ostvariti operaciju oduzimanja i, ovisno
o predznacima, ugraditi određenu logiku odlučivanja što je umanjenik, a što umanjitelj.
Nije nemoguće, ali je relativno složeno. Pokušajmo zato predložiti drugu konvenciju: broj
pretvaramo u negativan komplementiranjem. Nule postaju jedinice i obratno. Prvi bit s
lijeva određuje predznak. U pozitivnom broju bit će to 0, koja će komplementiranjem

22
Uvod u programiranje Prikaz informacija u računalu

postati 1, pa se negativne brojeve lako prepoznati. Ovu je operaciju jednostavno načiniti


sklopovljem.
24 0 0 0 1 1 0 0 0
.
-24 1 1 1 0 0 1 1 1
Zbrojimo li ove dvije vrijednosti rezultat je
1 1 1 1 1 1 1 1
Rezultat opet nije nula, ali će to dodavanjem jedinice postati:
1 1 1 1 1 1 1 1

+ 0 0 0 0 0 0 0 1

= 0 0 0 0 0 0 0 0

Preliv nastao na bitu najveće težinske vrijednost nije se mogao nigdje pohraniti. Jedinicu
smo, očito, mogli dodati i bilo kojem od pribrojnika prije njihovih zbrajanja.
Najprikladnija je, dakle, sljedeća konvencija prikaza negativnih brojeva: pozitivni broj se
komplementira i komplementiranom broju dodaje se jedinica. Tako će se osigurati da se
zbrajanje i oduzimanje, bilo s pozitivnim ili negativnim brojevima, uvijek obavlja
zbrajanjem, što pojednostavnjuje izvedbu sklopovlja. Dakle, primjena operatora minus na
broj 2410 izgleda u konačnici ovako:
24 0 0 0 1 1 0 0 0

Komplementiranjem dobijemo:
1 1 1 0 0 1 1 1
da bismo dodavanjem jedinice dobili konačni rezultat:
1 1 1 0 0 1 1 1

+ 0 0 0 0 0 0 0 1

= 1 1 1 0 1 0 0 0

Provjerimo vrijedi li -(-n) = n. Postupak bi trebao biti jednak: primjena operatora minus
znači komplementiranje i dodavanje jedinice.
-24 1 1 1 0 1 0 0 0

Komplementiranjem dobijemo:
0 0 0 1 0 1 1 1
da bismo dodavanjem jedinice dobili konačni rezultat:
0 0 0 1 0 1 1 1

+ 0 0 0 0 0 0 0 1

= 0 0 0 1 1 0 0 0

23
Uvod u programiranje Prikaz informacija u računalu

Rezultat je jednak binarnom prikazu broja 2410, čime smo pokazali da je postupak dobar.
Primjer: u 5-bitnim registrima obaviti operaciju 810 - 610.
Binarni prikaz broja 810 u 5-bitnom registru jest
0 1 0 0 0
Broj 610 izgleda ovako:
0 0 1 1 0
Da bismo dobili -610, potrebno je obaviti komplementiranje i dodavanje jedinice:
1 1 0 0 1

+ 0 0 0 0 1

1 1 0 1 0
Zbrojimo sada 810 i -610:
0 1 0 0 0

+ 1 1 0 1 0

1 0 0 0 1 0
Prijenos nastao na najvišem težinskom mjestu nije se imao kamo pohraniti, pa je rezultat
210, što odgovara rezultatu računske operacije 810 - 610.
Pogledajmo sada ponašanje registra od 3 bita, pri čemu je prvi bit predznak, kad se
sukcesivno dodaje jedinica.
Sadržaj Dekadski
registra ekvivalent
0 0 0 0

0 0 1 1
-22
0 1 0 2

0 1 1 3
0
1 0 0 -4

1 0 1 -3
22-1
1 1 0 -2

1 1 1 -1

Brojevi se kreću u intervalu [-(22), 22-1]. Općenito vrijedi da se u registru od n bita, pri
čemu je prvi bit predznak, mogu pohraniti brojevi u intervalu [-(2n-1), 2n-1-1].

24
Uvod u programiranje Prikaz informacija u računalu

Oktalni brojevni sustav


Za kraće zapisivanje brojeva u računarstvu se često koristi i oktalni brojevni sustav, koji
se jednostavno pretvara u binarni i obratno. Baza oktalnog brojevnog sustava je B=8, a
znamenke su zi  {0, 1, 2, 3, 4, 5, 6, 7}. I ovdje se može provesti razmatranje slično onome
kod binarnog brojevnog sustava.
Svaki se cijeli broj N u oktalnom sustavu može prikazati kao
N = zn-1 ∙ 8n-1 + zn-2 ∙ 8n-2 + ... + z1 ∙ 81 + z0 ∙ 80
Izlučimo li zajednički faktor 8 iz svih pribrojnika osim posljednjeg, dobit ćemo
N = 8 ∙ (zn-1 ∙ 8n-2 + zn-2 ∙ 8n-3 + ... + z1 ∙ 80) + z0 ∙ 80
Prethodni izraz može se napisati kao
N = 8 ∙ q1 + z0
ili
z0 = N - 8 ∙ q1
Kako su N i q1 sigurno cijeli brojevi, a z0  {0, 1, 2, 3, 4, 5, 6, 7}, očigledno je z0 ostatak
dijeljenja N s 8. Jednakim postupkom pokazuje se da je z1 ostatak dijeljenja količnika q1 s
8 itd. Ostatci dijeljenja s 8 zapisuju kao oktalne znamenke krenuvši s desna.
Primjer: pretvoriti dekadski broj 57 u oktalni
57 : 8 = 7
ostatak 1
7:8=0
7 1
ostatak 7
Provjerimo je li rezultat dobar:
718 = 7 ∙ 81 + 1 ∙ 80 = 5610 + 1 = 5710
Osim sukcesivnim dijeljenjem s 8, oktalni broj može se dobiti i izravno iz binarnog zapisa.
Broj N zapisan u binarnom brojevnom sustavu jest
N = zn-1 ∙ 2n-1 + ... + z5 ∙ 25 + z4 ∙ 24 + z3 ∙ 23 + z2 ∙ 22 + z1 ∙ 21 + z0 ∙ 20
Pribrojnike možemo, krenuvši s desna, grupirati u grupe po 3:
N = ... + (z5 ∙ 25 + z4 ∙ 24 + z3 ∙ 23) + (z2 ∙ 22 + z1 ∙ 21 + z0 ∙ 20)
pa iz svake grupe izdvojiti zajednički faktor:
N = ... + (z5 ∙ 22 + z4 ∙ 21 + z3 ∙ 20) ∙ 23 + (z2 ∙ 22 + z1 ∙ 21 + z0 ∙ 20) ∙ 20
iz čega slijedi
N = ... + (z5 ∙ 22 + z4 ∙ 21 + z3 ∙ 20) ∙ 81 + ( z2 ∙ 22 + z1 ∙ 21 + z0 ∙ 20) ∙ 80
Može se primijetiti da je (z1 ∙ 22 + z1 ∙ 21 + z0 ∙ 20) oktalna znamenka uz 80, (z5 ∙ 22 + z4 ∙ 21
+ z3 ∙ 20) oktalna znamenka uz 81 itd.
Primjer: pretvoriti dekadski broj 57 u oktalni služeći se binarnim međuzapisom:
5710 = 1110012 = (1 ∙ 22 + 1 ∙ 21 + 1 ∙ 20) ∙ 81 + (0 ∙ 22 + 0 ∙ 21 + 1 ∙ 20) ∙ 80 = 718
Primjer: Broj od 32 binarne znamenke
111001101101001000110110111010112

25
Uvod u programiranje Prikaz informacija u računalu

pretvara se u oktalni grupiranjem znamenki po 3, krenuvši s desna


11 100 110 110 100 100 011 011 011 101 0112
nakon čega se izravno može zapisati oktalni zapis
346644333538
koji ima 11 znamenki.
Očigledno je da je za prikaz oktalnog broja potrebno tri puta manje znamenki nego za
prikaz binarnog broja. Za vježbu bi bilo dobro međusobno usporediti potreban broj
znamenki u dekadskom i oktalnom sustavu. Neka je d broj potrebnih dekadskih, a k
oktalnih znamenki:
10d -1  8k -1  10d  8k  k  d : log 8  k ⪆ d ∙ 1,11
Oktalni je zapis oko 11% dulji od dekadskog.

Heksadekadski brojevni sustav


Primjenom oktalnog zapisa postiže se, uz jednostavnu pretvorbu, kraće zapisivanje
binarnih brojeva. Zato što je baza oktalnog brojevnog sustava B = 8 = 23, potrebno je 3
puta manje znamenki nego za binarni zapis. U sustavu s B = 4 = 22 bit će potrebno 2 puta
manje znamenki nego u binarnom, u sustavu s B = 16 = 24 bit će potrebno 4 puta manje
znamenki nego u binarnom i tako dalje. Iz praktičnih razloga sustavi s B > 16 ne koriste
se.
Sustav s B = 16 naziva se heksadekadskim brojevnim sustavom. Brojevni sustav s bazom
B koristi B različitih znamenki, pa će ovdje biti potrebno njih 16. Znamenke 0 do 9
preuzimamo iz dekadskog sustava, dok se za preostale koriste slova abecede. Slovo A
koristi se za prikaz znamenke koja bi odgovarala dekadskoj vrijednosti 10, B za 11, C za
12, D za 13, E za 14 i F za 15. Znamenke su dakle zi  {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E,
F}.
Dokaz da se pretvorba iz dekadskog u heksadekadski sustav može načiniti sukcesivnim
dijeljenjem sa 16 i zapisivanjem ostataka s desna na lijevo lako je provesti po uzoru na
oktalni i binarni brojevni sustav. Jednako tako, po uzoru na oktalni sustav, lako je dokazati
da se pretvorba iz binarnog u heksadekadski brojevni sustav dade načiniti grupiranjem
znamenki po 4.
Primjer: dekadski broj 1246 prikazati kao heksadekadski
1246 : 16 = 77
ostatak 14
77 : 16 = 4
ostatak 13
4 : 16 = 0
ostatak 4 4 D E
Primjer: Broj od 32 binarne znamenke
111001101101001000110110111010112
pretvara se u dekadski grupiranjem znamenki po 4, krenuvši s desna
1110 0110 1101 0010 0011 0110 1110 10112

26
Uvod u programiranje Prikaz informacija u računalu

nakon čega se izravno može zapisati heksadekadski zapis


E6D236EB16
koji ima samo 8 znamenki.
Heksadekadski zapis ima manje znamenki i od dekadskog. Neka je d broj potrebnih
dekadskih, a h heksadekadskih znamenki:
10d  16h  d ∙ log 10  h ∙ log 16  d  h ∙ log 16  h  d : log 16 
h ⪆ d : 1,204  h ⪆ d ∙ 0,83
Heksadekadski je zapis oko 17% kraći od dekadskog. Ako nam skraćenje ne izgleda veliko,
s obzirom na razliku u bazi sustava, podsjetimo se da bi za dvostruko manje znamenki
nego što je potrebno u dekadskom sustavu, bio potreban sustav s bazom 102 = 100.
Uvođenje sustava s bazom B = 32 zahtijevalo bi uvođenje sljedećih 16 slova abecede, s
kojima bi se teško bilo snalaziti, pa se tako dobiveno daljnje skraćenje zapisa na 66% od
broja dekadskih znamenki ne isplati.

Binarni razlomci
Cijelim se brojevima može prikazati samo dio informacija. Veličine koje susrećemo u
prirodi često imaju i necjelobrojni dio. Dekadski razlomak može se zapisati kao
zn-1 ∙ 10n-1 + ... + z1 ∙ 101 + z0 ∙ 100 + z-1 ∙ 10-1 + z-2 ∙ 10-2 + ... + z-(n-1) ∙ 10-(n-1) + z-n ∙ 10-n
U zapisu koji se služi isključivo znamenkama, potrebno je nekako odrediti mjesto na
kojem započinju znamenke uz negativne potencije broja 10. U zemljama izvan engleskog
govornog područja to se mjesto označava zarezom, koji se uobičajeno zove decimalnim
zarezom. U zemljama engleskog govornog područja umjesto decimalnog zareza koristi se
decimalna točka. Kako su računala potekla iz Sjedinjenih Američkih Država, naredbe
programskih jezika tvore se engleskim riječima, pa je tako u svim programskim jezicima
u uporabi decimalna točke. Zato će se i u ovom tekstu u tome kontekstu umjesto zareza
svugdje koristiti točka, a umjesto točke zarez. Broj npr.
5 ∙ 102 + 8 ∙ 101 + 3 ∙ 100 + 4 ∙ 10-1 + 9 ∙ 10-2
pisat ćemo kao 583.49 a ne kao 583,49.
Ako želimo naznačiti poziciju tisućica, broj 2018 nećemo pisati kao 2.018 nego kao 2,018.
Po analogiji s dekadskim sustavom, binarni razlomak može se napisati kao
zn-1 ∙ 2n-1 + ... + z1 ∙ 21 + z0 ∙ 20 + z-1 ∙ 2-1 + z-2 ∙ 2-2 + ... + z-(n-1) ∙ 2-(n-1) + z-n ∙ 2-n
Broj npr. 1 ∙ 22 + 0 ∙ 21 + 1 ∙ 20 + 1 ∙ 2-1 + 1 ∙ 2-2 pisat ćemo s binarnom točkom umjesto
binarnog zareza: 101.112
Korisno je raspisati neke negativne potencije broja 2:
2-1 = 0.510
2-2 = 0.2510
2-3 = 0.12510
2-4 = 0.062510
2-5 = 0.0312510
2-6 = 0.01562510

27
Uvod u programiranje Prikaz informacija u računalu

...
Uz ovakvu tablicu brzo se može preračunati: 101.112 = 5.7510
Prilikom pretvorbe dekadskog razlomka u binarni, cijeli se dio pretvara na otprije poznat
način. Za pretvorbu necjelobrojnog dijela moguće je, ako ne znamo bolje, uporabiti
algoritam koji uz najveću potenciju s negativnim eksponentom manju ili jednaku
necjelobrojnom dijelu postavlja jedinicu, od necjelobrojnog dijela oduzima vrijednost te
najveće potencije i tako dalje, sve dok ima ostatka.
Primjer: Pretvoriti 0.625 u binarni broj.
Najveća potencija broja 2 manja ili jednaka 0.625 jest 0.5. Znamenka uz 2-1 bit će 1.
Od 0.625 oduzimamo 0.5. Ostaje 0.125.
Najveća potencija broja 2 manja ili jednaka 0.125 jest 0.125. Znamenka uz 23 bit će 1.
Od 0.125 oduzimamo 0.125. Nema ostatka, pa možemo pisati konačni rezultat:
0.62510 = 1 ∙ 2-1 + 0 ∙ 2-2 + 1 ∙ 2-3 = 0.1012
Navedeni algoritam radi korektno i za pozitivne i za negativne potencije broja 2.
Moguće je načiniti i sljedeće razmatranje. Ako se binarno raspiše razlomljeni dio broja:
R = z-1 ∙ 2-1 + z-2 ∙ 2-2 + z-3 ∙ 2-3 ... + z-(n-1) ∙ 2-(n-1) + z-n ∙ 2-n
ova se jednadžba može pomnožiti s 2:
2 ∙ R = z-1 ∙ 20 + z-2 ∙ 2-1 + z-3 ∙ 2-2 ... + z-(n-1) ∙ 2-(n) + z-n ∙ 2-(n+1)
Iz gornje jednadžbe može se uočiti da se znamenka z-1 može očitati kao znamenka uz 20
koja nastaje kad se necjelobrojni dio broja pomnoži s 2.
Ako se s R' označi necjelobrojni dio od 2 ∙ R, može dalje pisati
R' = z-2 ∙ 2-1 + z-3 ∙ 2-2 ... + z-(n-1) ∙ 2-(n) + z-n ∙ 2-(n+1)
Pomnoži li se gornja jednadžba s 2 dobije se
2 ∙ R' = z-2 ∙ 20 + z-3 ∙ 2-1 ... + z-(n-1) ∙ 2-(n+1) + z-n ∙ 2-(n+2)
iz čega se može očitati z-2 kao znamenka uz 20 u 2 ∙ R'.
Necjelobrojni dio dekadskog broja pretvara se u binarni zapis, dakle, sukcesivnim
množenjem s dva i uzimanjem cjelobrojnog dijela umnoška kao vrijednosti sljedeće
binarne znamenke, s lijeva na desno.
Primjer: pretvoriti 0.625 u binarni broj.
0.625 ∙ 2 = 1.250 = 1 + 0.250

0.250 ∙ 2 = 0.5 = 0 + 0.5 0 . 1 0 1

0.5 ∙ 2 = 1. = 1 + 0.

Valja primijetiti da se pretvorbom dekadskog razlomka s konačnim brojem znamenki iza


decimalne točke ne dobiva uvijek binarni broj s konačnim brojem znamenki iza binarne
točke. Da bi se neki broj prikazao s najviše n binarnih znamenki iza točke, njegov
necjelobrojni dio mora biti višekratnik od 2-n. Poneki jednostavni dekadski razlomci to
nisu.

28
Uvod u programiranje Prikaz informacija u računalu

Primjer: pretvoriti 2.110 u binarni broj.


2.110 = 210 + 0.110
0.1 ∙ 2 = 0.2 = 0 + 0.

0.2 ∙ 2 = 0.4 = 0 + 0.4 1 0 . 0 0 0 1 1 0 0 1 1 0 0 1 ...


.
0.4 ∙ 2 = 0.8 = 0 + 0.8

0.8 ∙ 2 = 1.6 = 1 + 0.6

0.6 ∙ 2 = 1.2 = 1 + 0.8

0.2 ∙ 2 = 0.4 = 0 + 0.4


Treba uočiti da će se ciklus 0011 koji započinje množenjem 0.2 s 2 ponoviti beskonačan
broj puta.
. .
Dekadski broj 2.1 prikazuje se beskonačnim binarnim razlomkom 10.00011

Množenje i dijeljenje s potencijama broja 2


U svim brojevnim sustavima posebno je jednostavna operacija množenje s ili dijeljenje
broja s potencijom baze. Množenje se obavlja pomakom točke udesno za onoliko mjesta
koliko odgovara vrijednosti eksponenta. Dijeljenje (množenje s negativnom potencijom)
se obavlja pomakom točke ulijevo za onoliko mjesta koliko odgovara vrijednosti
eksponenta.
Primjeri:
10.1001012 ∙ 23 = 10100.1012
10.12 ∙ 24 = 1010002
111001.012 : 24 = 11.1001012
1.12 : 25 = 0.0000112

Realni brojevi standardne preciznosti


Kod prikaza realnih brojeva u registrima računala, binarnu točku, zbog prirode bistabila,
neće biti moguće izravno prikazati. Kao što je i problem prikaza predznaka kod cijelih
brojeva riješen određenom konvencijom, tako će se i kod realnih brojeva mjesto binarne
točke u broju odrediti konvencijom. Jedna od mogućih konvencija mogla bi biti: podijeliti
registar na dva jednaka dijela, pri čemu bi bitovi lijevo od sredine predstavljali
nenegativne, a desno od sredine negativne potencije broja 2. Promotrimo sada raspon
pozitivnih brojeva koji bi se na ovaj način mogli prikazati u, primjerice, 32-bitnom
registru. Ako oduzmemo bit za prikaz predznaka, preostaje nam 16 bitova za nenegativne
i 15 bitova za negativne potencije broja 2.
Najveći broj po apsolutnoj vrijednosti bio bi
01111111111111111.111111111111111
Pozitivne potencije broja 2 daju broj 215+214 + ... + 21 + 20 što iznosi točno 216 – 1, dok je
suma negativnih potencija broja 2 nešto manja od 1 (točnije 1 – 2-15 = 1 –
29
Uvod u programiranje Prikaz informacija u računalu

0.00003051757812510 = 0.99996948242187510). Najveći broj po apsolutnoj vrijednosti


bio bi, dakle, vrlo približno 216, tj. 6553610.
Najmanji broj po apsolutnoj vrijednosti, različit od nule, bio bi
00000000000000000.000000000000001
ili 2-15 što iznosi 0.00003051757812510.
Predloženom konvencijom, uz relativno veliki utrošak bitova, dobili smo relativno malen
raspon brojeva: [0.000030517578125, 65536].
Posljedica je to neekonomičnog načina zapisivanja. I u dekadskom sustavu, za učinkovitiji
prikaz velikih ili malih brojeva služimo se eksponencijalnim zapisom. Tako ćemo, na
primjer, broj 1500000000000 obično pisati kao 1.5 ∙ 1012, a 0.000000006 kao 6 ∙ 10-9.
Time se ista informacija prikazuje s manje znamenki. Osim u financijskom svijetu, rijedak
je slučaj da veliki brojevi imaju mnogo točnih znamenki. Iako je, na primjer, prosječna
udaljenost od Zemlje do Sunca 149597890 km, ona varira između 147.1 ∙ 106 i 152.1 ∙ 106
km, pa će je za većinu primjena biti dovoljno izraziti kao približno 0.15 ∙ 10-9 km. Slično je
i s malim brojevima. Promjer vodikovog atoma nikad nećemo izraziti kao 0.00000000007
m, već kao 7 ∙ 10-11 m.
Zato se zapis realnog broja sastoji od dva dijela: komponente koja prikazuje eksponent i
komponente koja prikazuje mantisu. Broj bita pridijeljen pojedinoj komponenti određen
je standardima. Za 32-bitne registre, najčešće se koristi IEEE standard broj 754, koji
propisuje sljedeći oblik realnog broja standardne preciznosti:
PK M
31 30 23 22 0
Bit 31 sadrži informaciju o predznaku, pri čemu 0 označava pozitivan, a 1 negativan broj.
Bitovi 30 do 23 sadrže karakteristiku. Kako binarni eksponent može biti i pozitivan i
negativan, da bi se izbjeglo uvođenje dodatnog bita za predznak eksponenta,
karakteristiku čini binarni eksponent uvećan za 127. Time svaka karakteristika postaje
pozitivna.
Bitovi 22 do 0 sadrže normaliziranu mantisu. Normalizacija se obavlja pomicanjem
binarne točke sve do pozicije neposredno iza vodeće jedinice.
Primjeri:
11101.111012 = 1.1101111012 ∙ 24
0.00101102 = 1.01102 ∙ 2-3
Može se primijetiti da se normalizacijom svakog binarnog broja, osim nule, dobije binarni
broj u obliku 1.mmmm... ∙ 2be, gdje je be binarni eksponent. Kako se zna da vodeća jedinica,
osim kod nule, uvijek postoji, neće se fizički pohranjivati u registar, radi uštede na broju
bitova. Ta se jedinica zato zove skrivenim bitom.
Na temelju ovih činjenica možemo opisati postupak pretvorbe realnog dekadskog broja u
realni broj standardne preciznosti:
• pretvoriti dekadski broj u binarni. Cijeli dio pretvara se sukcesivnim dijeljenjem, a
decimalni dio sukcesivnim množenjem s 2.
• normalizirati dobiveni binarni broj da se dobije oblik 1.mmmm.. ∙ 2be.
• binarni eksponent be uvećati za 127 da se dobije karakteristika k.

30
Uvod u programiranje Prikaz informacija u računalu

• karakteristiku k pretvoriti u binarni broj i upisati je u bitove 30 do 23, pri čemu bit
23 odgovara bitu uz potenciju 20, bit 24 bitu uz potenciju 21 itd.
• mantisu mmmm... upisati u bitove 22 do 0, s lijeva na desno.
• upisati predznak u bit 31.
Primjeri:
• Prikazati broj 14.7510 kao binarni realni broj standardne preciznosti u računalu.
o pretvorbom u binarni broj dobije se 1110.112
o obavlja se normalizacija. Broj 1110.112 možemo pisati i kao 1110.112 ∙ 20.
Pomak binarne točke za tri mjesta ulijevo znači dijeljenje s 23. Da bi
vrijednost izraza ostala ista, tako podijeljenu vrijednost potrebno je
pomnožiti s 23. Normalizirani binarni broj je 1.110112 ∙ 23.
o izračunava se karakteristika k = be + 127. Karakteristika je, dakle, 130.
o binarni prikaz karakteristike iznosi 10000010
o predznak, binarna vrijednost karakteristike i mantisa bez skrivenog bita
upisuju se u odgovarajuće komponente 32-bitnog registra
0 10000010 11011000000000000000000
31 30 23 22 0

o radi skraćenog zapisivanja heksadekadskim sustavom, grupirat ćemo


znamenke po 4: 0100 0001 0110 1100 0000 0000 0000 0000
o heksadekadski zapis glasi: 416C0000
• Prikazati broj 210 kao binarni realni broj standardne preciznosti u računalu.
210 = 102 ∙ 20 = 12 ∙ 21 
Predznak = 0
Karakteristika = 1 + 12710 = 12810 = 100000002
Mantisa = (1.) 00000000000000000000000 
Registar = 0 10000000 00000000000000000000000
Pretvorba u heksadekadski:
0100 0000 0000 0000 0000 0000 0000 0000 = 4000000016
• Prikazati broj -210 kao binarni realni broj standardne preciznosti u računalu.
-210 = 102 ∙ 20 = 12 ∙ 21 
Predznak = 1
Karakteristika = 1 + 12710 = 12810 = 100000002
Mantisa = (1.) 00000000000000000000000 
Registar = 1 10000000 00000000000000000000000
Pretvorba u heksadekadski:
1100 0000 0000 0000 0000 0000 0000 0000 = C000000016
o Valja primijetiti da je zapis jednak zapisu broja 2, ali s bitom predznaka
postavljenim na 1.

31
Uvod u programiranje Prikaz informacija u računalu

• Prikazati broj 410 kao binarni realni broj standardne preciznosti u računalu.
410 = 1002 ∙ 20 = 12 ∙ 22 
Predznak = 0
Karakteristika = 2 + 12710 = 12910 = 100000012
Mantisa = (1.) 00000000000000000000000 
Registar = 0 10000001 00000000000000000000000
Pretvorba u heksadekadski:
0100 0000 1000 0000 0000 0000 0000 0000 = 4080000016
o Mantisa je jednaka mantisi broja 2. Razlikuju se karakteristike.
• Prikazati broj 610 kao binarni realni broj standardne preciznosti u računalu.
610 = 1102 ∙ 20 = 1.12 ∙ 22 
Predznak = 0
Karakteristika = 2 + 12710 = 12910 = 100000012
Mantisa = (1.) 10000000000000000000000 
Registar = 0 10000001 10000000000000000000000
Pretvorba u heksadekadski:
0100 0000 1100 0000 0000 0000 0000 0000 = 40C0000016
• Prikazati broj 110 kao binarni realni broj standardne preciznosti u računalu.
110 = 12 ∙ 20 
Predznak = 0
Karakteristika = 0 + 12710 = 12710 = 011111112
Mantisa = (1.) 00000000000000000000000 
Registar = 0 01111111 00000000000000000000000
Pretvorba u heksadekadski:
0011 1111 1000 0000 0000 0000 0000 0000 = 3F80000016
• Prikazati broj 0.7510 kao binarni realni broj standardne preciznosti u računalu.
0.7510 = 0.112 ∙ 20 = 1.1 ∙ 2-1 
Predznak = 0
Karakteristika = -1 + 12710 = 12610 = 011111102
Mantisa = (1.) 00000000000000000000000 
Registar = 0 01111110 10000000000000000000000
Pretvorba u heksadekadski:
0011 1111 0100 0000 0000 0000 0000 0000 = 3F40000016

32
Uvod u programiranje Prikaz informacija u računalu

Posebni slučajevi
Nula
Postojanje skrivenog bita znači da se pri interpretaciji svake mantise na početak dodaje
jedinica. Normalizacijom broja 0, međutim, ne može se dobiti jedinica na vodećem mjestu
mantise, pa je za prikaz nule potrebna posebna konvencija.
Nula se pohranjuje kao
0 00000000 00000000000000000000000
31 30 23 22 0

dakle kao broj kojemu je karakteristika postavljena na nulu i svi bitovi mantise postavljeni
na nulu.
I sljedeći prikaz predstavlja nulu:
1 00000000 00000000000000000000000
31 30 23 22 0

točnije -0, ali i +0 i -0 u računskim operacijama tretiraju se jednako.

Denormalizirani broj
Realni broj
p 0 0 0 0 0 0 0 0 mm mm mm m mm mm mm m mm mm m mm mm
31 30 23 22 0

kojemu su svi bitovi karakteristike postavljeni na nulu i kod kojega je barem jedan bit m
mantise različit od 0 je denormalizirani broj kojemu se podrazumijeva potencija 2-126, a
mantisa 0.mmmmmmmmmmmmmmmmmmmmmmm.

±∞
Realni broj
p 11111111 00000000000000000000000
31 30 23 22 0

kojemu su svi bitovi karakteristike postavljeni na jedinicu, a svi bitovi mantise postavljeni
na nulu prikazuje ±∞. Takav rezultat dobiva se dijeljenjem broja koji nije nula s nulom ili
kao posljedica operacije kojoj rezultat po apsolutnoj vrijednosti premašuje najveći broj
koji je u standardnoj preciznosti moguće prikazati.

±NaN
Realni broj
p 1 1 1 1 1 1 1 1 mm mm mm m mm mm mm m mm mm m mm mm
31 30 23 22 0

kod kojega su svi bitovi karakteristike postavljeni na jedinicu, a barem jedan bit m mantise
je različit od 0, smatra se nenumeričkim podatkom (not a number, NaN). Takav rezutat
dobije se kad je rezultat računanja nedefiniran, kao što je to kod dijeljenja nule s nulom.

33
Uvod u programiranje Prikaz informacija u računalu

IEEE standard definira operacije s veličinama NaN i  prema sljedećoj tablici, u kojoj je n
je neki broj različit od nule:
Operacija Rezultat
±n / ± 0
± ∙ ± ±
±n / 0 ±
+ 
±0 / ±0 NaN
- NaN
- +  NaN
± / ± NaN
± ∙ 0 NaN

Raspon i preciznost brojeva standardne preciznosti


Da bismo odredili u kojem se rasponu kreću brojevi koje se može prikazati po IEEE
standardu broj 754, valja najprije odrediti raspon u kojem se kreće karakteristika.
Najmanja karakteristika koja se može prikazati u 8 bita bez predznaka iznosi 0, što je
konvencijom rezervirano za prikaz broja 0 kao i za denormalizirani broj. Najveća
karakteristika koja se može prikazati u 8 bita bez predznaka iznosi 28-1 tj. 255, ali je i ta
karakteristika rezervirana konvencijom za prikaz veličine NaN (Not A Number, ako je
mantisa različita od nule) ili  (Infinity, ako je mantisa jednaka nuli). Najveća
karakteristika koja predstavlja stvarni broj iznosi 254, iz čega se može izračunati najveći
eksponent kao 254-127=127.
Najmanji je broj koji se može prikazati je denormalizirani broj kojemu su svi bitovi osim
posljednjega postavljeni na nulu, tj. 0.00000000000000000000001 ∙ 2-126 = 1 ∙ 2-149.
Najveći broj ima sve bitove mantise postavljene na jedinicu i eksponent 127, tj.
1.11111112.... ∙ 2127. Kako je 1.11111112... vrlo približno 102 (razlika iznosi najviše 2-23 ili
0.00000011920928955078125), može se reći da je dozvoljeni interval realnih brojeva
vrlo približno [1 ∙ 2-149, 1 ∙ 2128], ili, koristeći se uobičajenijom notacijom,
[1.401298464324 ∙ 10-45, 3.402823669209 ∙ 10-38].
Preciznost, izražena u broju dekadskih znamenki, može se odrediti sljedećim
razmatranjem. Za zapis broja koriste se 24 binarne znamenke mantise (23 stvarne i
skriveni bit). Nepoznat broj x odgovarajućih dekadskih znamenki može se približno
odrediti kao
224 ≈ 10x  24 log2 ≈ x log10  x ≈ 24 log2  x ≈ log224  x ≈ 7.224719895936
Može se, dakle, zaključiti da se s 24 binarne znamenke mantise može prikazati nešto više
od 7 dekadskih znamenki.

Preciznost i točnost
Pažljiviji će se čitatelj vjerojatno zapitati zašto se u dosadašnjem tekstu govorilo o
preciznosti, a ne o točnosti, iako se ta dva termina na prvi pogled slična i često se
ravnopravno koriste. Terminom preciznost (od engleske riječi precision) zvat ćemo broj
prvih važećih točnih znamenki koji opisuje neku veličinu. Primjerice u broju 0.0012345
ima 5 prvih važećih točnih znamenki jer bi se u eksponencijalnom obliku broj mogao
prikazati kao 0.12345 ∙ 10-2. Točnošću (accuracy) ćemo smatrati bliskost stvarnoj
vrijednosti. Da ta dva termina ne označavaju isto, može ilustrirati primjer opisivanja

34
Uvod u programiranje Prikaz informacija u računalu

tjelesne mase neke osobe. Tjelesnu masu, koristeći standardnu preciznost, možemo
opisati sa 7 znamenki, primjerice 86.58923 kg. Koliko je ta informacija zapravo točna? I
skuplje digitalne vage za kućnu uporabu u najboljem slučaju imaju točnost ± 100g. Što je
osoba imala na sebi prilikom vaganja? Je li i kada popila čašu vode? Očito, stvarna točnost
takve informacije ne prelazi treću, a možda ni drugu znamenku.
Može se zaključiti da je za dovoljnu točnost potrebna adekvatna preciznost, ali preciznost
ne podrazumijeva automatski točnost, jer su iskazane znamenke mogle nastati na temelju
npr. pogrešnog mjerenja.

Apsolutne i relativne greške33


Ograničena veličina registra koja kao posljedicu ima ograničeni broj bitova mantise, kao
što je već bilo pokazano, u nekim će slučajevima rezultirati određenom razlikom između
veličine koju se u registar htjelo pohraniti i stvarno pohranjene veličine. Ovu razliku
zovemo greškom. Ako se s x označi realni broj koji treba pohraniti u registar računala, a s
x* približna vrijednost broja x koja je zaista pohranjena u registar, može se definirati
apsolutna greška α kao
α = | x* - x |
te relativna greška β kao
β=α/|x|
Na primjer, ako je u nekom malom registru umjesto potrebne vrijednosti x = 1.57
pohranjena vrijednost x* = 1.625
α = | 1.625 - 1.57 | = 0.055
β = 0.055 / | 1.57 | = 0.035
U standardnoj preciznosti po standardu IEEE 754 mantisa se prikazuje u obliku
1.mmmm…, gdje bitova m mantise, bez skrivenog bita, ima upravo 23. Posljednji bit, dakle,
ima težinsku vrijednost od 2-23. Kad bi pri spremanju brojeva dolazilo do odsijecanja
bitova koji ne stanu u mantisu, kad bi odsječenih bitova bilo beskonačno mnogo i kad bi
svi imali vrijednost 1, ukupna njihova težina ne može premašiti 2-23. Međutim, kod
spremanja ne dolazi do odsijecanja nego do zaokruživanja na bližu vrijednost, tako da je
najveća ukupna greška pri takvom zaokruživanju upola manja tj. 2-24.
Za standardnu preciznost po standardu IEEE 754 možemo, dakle, napisati maksimalni
iznos relativne greške kao
β ≤ 2-24 ≈ 6 · 10-8
Maksimalni iznos apsolutne greške tada je
α = β · | x | ≈ 6 · 10-8 · | x |
Drugim riječima, apsolutna greška je reda veličine osme po redu od prvih važećih
znamenki broja koji se u registar želi pohraniti.

33Pojmovi greška i pogreška često se shvaćaju i prihvaćaju kao sinonimi, iako postoji suptilna razlika:
pogreška je radnja kojom je načinjeno nešto krivo, a greška je posljedica takve pogreške ili ograničenih
mogućnosti uređaja.

35
Uvod u programiranje Prikaz informacija u računalu

Realni brojevi dvostruke preciznosti


Postoje primjene kod kojih raspon ili preciznost brojeva standardne preciznosti nisu
zadovoljavajući. Za konstantni broj bitova, raspon bi se mogao povećati na štetu
preciznosti i obratno. I raspon i preciznost mogu se povećati samo korištenjem većeg
broja bitova. Standard IEEE 754 propisuje da se povećana preciznost i točnost postižu
korištenjem 64-bitnih registara, čiji je sadržaj raspodijeljen na sljedeći način:
PK … M ...
63 62 52 51 ... 0

Bit 63 sadrži informaciju o predznaku, pri čemu 0 označava pozitivan, a 1 negativan broj.
Bitovi 62 do 52 sadrže karakteristiku. Ima ih ukupno 11. Da bi se izbjeglo uvođenje
dodatnog bita za predznak eksponenta, karakteristiku čini binarni eksponent uvećan za
1023. Bitovi 51 do 0 sadrže mantisu.
I ovdje je najmanja karakteristika (s iznosom 0) konvencijom rezervirana za prikaz broja
0, ako su svi bitovi mantise postavljeni na 0, odnosno denormalizirani broj ako postoji bit
mantise različit od 0. Kod denormaliziranog broja pretpostavlja se eksponent -1022.
Najveća karakteristika koja se može prikazati u 11 bita bez predznaka iznosi 211-1 tj. 2047,
ali je i ovdje takva karakteristika rezervirana konvencijom za prikaz veličina NaN ili .
Najveća karakteristika koja prikazuje stvarni broj iznosi 2046. Kako se kod brojeva
dvostruke preciznosti binarni eksponent uvećava za 1023 da se dobije karakteristika,
najveći eksponent iznosti 2043-1023=1023.
Najmanji pozitivni broj različit od nule koji se može prikazati je denormalizirani broj
kojemu je samo posljednji bit mantise postavljen na 1:
0.000…001 ∙ 2-1022 = 2-1074 ≈ 4.941 ∙ 10−324
a najveći je:
1.1111.....1111112 ∙ 21023 ≈ 21024 ≈ 1.798 ∙ 10308
Preciznost iznosi 53 binarne znamenke (52 stvarne i skriveni bit), pa se može pisati
253 ≈ 10x  53 log2 ≈ x log10  x ≈ 53 log2 = 15.95458977019
Preciznost brojeva dvostruke preciznosti iznosi nešto manje od 16 dekadskih znamenki.
Najveća relativna greška iznosi
β ≤ 2-53 ≈ 1.11 · 10-16
Maksimalni iznos apsolutne greške tada je
α = β · | x | ≈ 1.11 · 10-16 · | x |

Realni brojevi produljene preciznosti


Standard IEEE 754 ne propisuje eksplicitno raspored bitova u produljenoj preciznosti, ali
definira osnovne karakteristike. Jedan od prikaza koji zadovoljavaju te karakteristike je
prikaz produljene preciznosti x86 implementiran u prevodiocu gcc i matematičkim
koprocesorima zasnovanim na x86 arhitekturi:
PK … CM ...
79 78 64 63 62 ... 0

36
Uvod u programiranje Prikaz informacija u računalu

Bit 79 sadrži informaciju o predznaku, pri čemu 0 označava pozitivan, a 1 negativan broj.
Sljedećih 15 bitova, bitovi 78 do 64 sadrže karakteristiku. Karakteristiku čini binarni
eksponent uvećan za 16383.
U ovom prikazu nema skrivenog bita, već je cijeli dio C realnog broja (jedinica dobivena
normalizacijom) pohranjen u bitu 63.
Bitovi 62 do 0 sadrže mantisu.
I ovdje je najmanja karakteristika (s iznosom 0) konvencijom rezervirana za prikaz broja
0, ako su svi bitovi mantise postavljeni na 0, odnosno denormalizirani broj ako postoji bit
mantise različit od 0. U oba slučaja bit 63 je nula.
Kod denormaliziranog broja pretpostavlja se eksponent -16382.
Najveća karakteristika koja se može prikazati u 15 bita bez predznaka iznosi 215-1 tj.
32767, ali je i ovdje takva karakteristika rezervirana konvencijom za prikaz veličina NaN
ili . Kod tih posebnih slučajeva i bit 63 postavljen je na 1.
Najveća karakteristika koja prikazuje stvarni broj iznosi 32766. Najveći eksponent iznosi,
dakle, 32766-16383=16383.
Najmanji pozitivni broj različit od nule 0 koji se može prikazati je denormalizirani broj
kojemu je samo posljednji bit mantise postavljen na 1:
0.000…001 ∙ 2-16382 = 2-16445 ≈ 3.645 ∙ 10−4951
a najveći je:
1.1111.....1111112 ∙ 216383 ≈ 216384 ≈ 1.19 ∙ 104932
Preciznost iznosi 64 binarne znamenke, pa se može pisati
264 ≈ 10x  64 log2 ≈ x log10  x ≈ 64 log2 = 19.26591972249
Preciznost brojeva produljene preciznosti iznosi nešto više od 19 dekadskih znamenki.
Najveća relativna greška iznosi
β ≤ 2-64 ≈ 5.42 · 10-20
Maksimalni iznos apsolutne greške tada je
α = β · | x | ≈ 5.42 · 10-20 · | x |

Slova, znamenke i ostali znakovi


Kao i brojevi, slova, znamenke i ostali ostali znakovi u računalu će biti prikazani u
registrima kombinacijom jedinica i nula. Kako je svaka kombinacija jedinica i nula zapravo
broj, očito će se koristiti određeni kôd – svakom će znaku biti pridružena određena
numerička vrijednost. Razmotrimo na početku koliko je velik registar potreban za
pohranu alfanumeričkih informacija. Prije nego što se računarstvo komercijaliziralo izvan
engleskog govornog područja, bilo je potrebno kodirati 26 velikih i 26 malih slova
engleske abecede, 10 znamenki, te određen broj operatora, interpunkcija i upravljačkih
znakova. Ukupno više od 64, ali manje od 128 različitih znakova, što znači da je za prikaz
svakog pojedinog znaka bilo dovoljno 7 bita. Ne računavši same početke računarstva u

37
Uvod u programiranje Prikaz informacija u računalu

kojima su se koristile i druge veličine, uobičajena veličina registra odgovara nekoj


potenciji broja 2. Zato je za pohranu alfanumeričkih informacija uzeto 8 bita, ili 1 bajt34.
Osmi je bit bio rezerviran za paritet, jednostavni mehanizam koji omogućava otkrivanje
jednostruke pogreške pri prijenosu informacija. Bit pariteta postavlja se prije prijenosa
na vrijednost koja osigurava da je ukupni broj jedinica (uključujući i bit pariteta) neparan
broj. Po dolasku informacije na odredište provjerava se da li je još uvijek ukupni broj
jedinica neparan. Ovakav postupak naziva se provjerom neparnog pariteta (odd parity
check). Postoji i alternativni postupak, provjera parnog pariteta (even parity check), pri
čemu se osigurava da je ukupni broj bitova paran.
Jasno je da ovakav postupak otkriva samo neparni broj promjena broja bita u bajtu. Ako
se dogodi parni broj promjena, provjera pariteta to neće otkriti, pa su već dugo vremena
u uporabi bolji postupci kontrole pogrešaka, a bit pariteta koristi se za pohranu stvarnih
informacija.
Iako su se tijekom vremena koristili i drugi kôdovi (npr. 6-bitni FIELDATA kompanije
Control Data, 8-bitni EBCDIC kompanije IBM), danas je u širokoj uporabi tzv. ASCII kôd
(American Standard Code for Information Interchange, američki standardni kôd za
razmjenu informacija). Američki Ured za standardizaciju (National Bureau of Standards,
NBS) više je godina radio na standardizaciji kôdova. Godine 1963. donesen je standard
X3.4-1963 u kojem još nisu bila definirana mala slova abecede, koje definira 1967. godine
standard X3.4-1967. Godine 1968. tadašnji predsjednik SAD Lyndon Johnson potpisao je
Savezni standard za obradu informacija (Federal Information Processing Standard, FIPS),
koji odgovara standardu X3.4-1967. Podržan od savezne vlade, taj je standard općenito
prihvaćen u industriji i zadržan je do danas te definira sljedeću kodnu tablicu:
Dekadski Binarno Oktalno Heksadekadski Znak Engleski opis
0 00000000 0 0 NUL null character
1 00000001 1 1 SOH start of header
2 00000010 2 2 STX start of text
3 00000011 3 3 ETX end of text
4 00000100 4 4 EOT end of transmission
5 00000101 5 5 ENQ enquiry
6 00000110 6 6 ACK acknowledge
7 00000111 7 7 BEL bell (ring)
8 00001000 10 8 BS backspace
9 00001001 11 9 HT horizontal tab
10 00001010 12 A LF line feed
11 00001011 13 B VT vertical tab
12 00001100 14 C FF form feed
13 00001101 15 D CR carriage return
14 00001110 16 E SO shift out
15 00001111 17 F SI shift in
16 00010000 20 10 DLE data link escape
17 00010001 21 11 DC1 device control 1
18 00010010 22 12 DC2 device control 2

34U hrvatskom jeziku često se koristi pojam oktet, a smišljeni su i izrazi slovnjak ili bitnjak. Međutim, ako je
prihvaćena strana riječ riječ bit, nema razloga da se ne prihvati i bajt, što je kraće i određenije od okteta
(koji može biti i gudački), posebno zato što tijekom povijesti "okteti" nisu uvijek imali osam bita.

38
Uvod u programiranje Prikaz informacija u računalu

19 00010011 23 13 DC3 device control 3


20 00010100 24 14 DC4 device control 4
21 00010101 25 15 NAK negative acknowledge
22 00010110 26 16 SYN synchronize
23 00010111 27 17 ETB end transmission block
24 00011000 30 18 CAN cancel
25 00011001 31 19 EM end of medium
26 00011010 32 1A SUB substitute
27 00011011 33 1B ESC escape
28 00011100 34 1C FS file separator
29 00011101 35 1D GS group separator
30 00011110 36 1E RS record separator
31 00011111 37 1F US unit separator
32 00100000 40 20 space
33 00100001 41 21 ! exclamation mark
34 00100010 42 22 " quotation mark
35 00100011 43 23 # number sign
36 00100100 44 24 $ dollar sign
37 00100101 45 25 % percent sign
38 00100110 46 26 & ampersand
39 00100111 47 27 ' apostrophe
40 00101000 50 28 ( left parenthesis
41 00101001 51 29 ) right parenthesis
42 00101010 52 2A * asterisk
43 00101011 53 2B + plus sign
44 00101100 54 2C , comma
45 00101101 55 2D - hyphen
46 00101110 56 2E . period
47 00101111 57 2F / slash
48 00110000 60 30 0 digit 0
49 00110001 61 31 1 digit 1
50 00110010 62 32 2 digit 2
51 00110011 63 33 3 digit 3
52 00110100 64 34 4 digit 4
53 00110101 65 35 5 digit 5
54 00110110 66 36 6 digit 6
55 00110111 67 37 7 digit 7
56 00111000 70 38 8 digit 8
57 00111001 71 39 9 digit 9
58 00111010 72 3A : colon
59 00111011 73 3B ; semicolon
60 00111100 74 3C < less-than
61 00111101 75 3D = equals-to
62 00111110 76 3E > greater-than
63 00111111 77 3F ? question mark
64 01000000 100 40 @ at sign
65 01000001 101 41 A uppercase A
66 01000010 102 42 B uppercase B
39
Uvod u programiranje Prikaz informacija u računalu

67 01000011 103 43 C uppercase C


68 01000100 104 44 D uppercase D
69 01000101 105 45 E uppercase E
70 01000110 106 46 F uppercase F
71 01000111 107 47 G uppercase G
72 01001000 110 48 H uppercase H
73 01001001 111 49 I uppercase I
74 01001010 112 4A J uppercase J
75 01001011 113 4B K uppercase K
76 01001100 114 4C L uppercase L
77 01001101 115 4D M uppercase M
78 01001110 116 4E N uppercase N
79 01001111 117 4F O uppercase O
80 01010000 120 50 P uppercase P
81 01010001 121 51 Q uppercase Q
82 01010010 122 52 R uppercase R
83 01010011 123 53 S uppercase S
84 01010100 124 54 T uppercase T
85 01010101 125 55 U uppercase U
86 01010110 126 56 V uppercase V
87 01010111 127 57 W uppercase W
88 01011000 130 58 X uppercase X
89 01011001 131 59 Y uppercase Y
90 01011010 132 5A Z uppercase Z
91 01011011 133 5B [ left square bracket
92 01011100 134 5C \ backslash
93 01011101 135 5D ] right square bracket
94 01011110 136 5E ^ caret
95 01011111 137 5F _ underscore
96 01100000 140 60 ` acute accent
97 01100001 141 61 a lowercase a
98 01100010 142 62 b lowercase b
99 01100011 143 63 c lowercase c
100 01100100 144 64 d lowercase d
101 01100101 145 65 e lowercase e
102 01100110 146 66 f lowercase f
103 01100111 147 67 g lowercase g
104 01101000 150 68 h lowercase h
105 01101001 151 69 i lowercase i
106 01101010 152 6A j lowercase j
107 01101011 153 6B k lowercase k
108 01101100 154 6C l lowercase l
109 01101101 155 6D m lowercase m
110 01101110 156 6E n lowercase n
111 01101111 157 6F o lowercase o
112 01110000 160 70 p lowercase p
113 01110001 161 71 q lowercase q
114 01110010 162 72 r lowercase r
40
Uvod u programiranje Prikaz informacija u računalu

115 01110011 163 73 s lowercase s


116 01110100 164 74 t lowercase t
117 01110101 165 75 u lowercase u
118 01110110 166 76 v lowercase v
119 01110111 167 77 w lowercase w
120 01111000 170 78 x lowercase x
121 01111001 171 79 y lowercase y
122 01111010 172 7A z lowercase z
123 01111011 173 7B { left curly brace
124 01111100 174 7C | vertical bar
125 01111101 175 7D } right curly brace
126 01111110 176 7E ~ tilde
127 01111111 177 7F DEL delete (rubout)

Prva 32 znaka u tablici upravljački su ili kontrolni znakovi. Znakovi su to kojima bitovi 6 i
7 imaju vrijednost 0. Njihova je namjena uzrokovati određenu akciju kada stignu na
izlaznu jedinicu računala, ili dati dodatnu informaciju o sadržaju poslane poruke. Na
primjer, znak dekadske vrijednosti 7 (BEL, bell) aktivirat će zvučni signal. Znak 10 (LF,
line feed) uzrokovat će nastavak ispisa u novom retku. Znak 13 (CR, carriage return)
postavit će poziciju za nastavak ispisa na početak trenutačnog retka. Znak 1 (SOH, start of
header) označava početak zaglavlja poruke itd. Ova grupa znakova naziva se i neispisivim
(non-printable) znakovima.
Znakovi s kôdom većim ili jednakim 32, s izuzetkom znaka 127, prikazat će se na izlaznim
jedinicama računala, pa se zovu ispisivim (printable) znakovima. Prvi od njih, 32 (space)
uzrokuje ispis praznine. Kôd 48 rezerviran je za znamenku 0, 49 za 1 i tako dalje. Valja
primijeti da su kod znamenki bitovi 4 i 5 postavljeni na vrijednost 1, dok posljednja 4 bita
imaju iste vrijednosti kao i bitovi odgovarajućih brojeva. Veliko slovo A ima kôd 65. Iza
njega, redom, slijedi 26 velikih slova engleske abecede. Malo slovo a ima kôd 97, nakon
čega slijede ostala mala slova engleske abecede.
Može se primijetiti da se pamćenjem praktički jedne numeričke vrijednosti i poznavanjem
abecede može zapamtiti struktura cijeloga kôda i izračunati gotovo polovicu konkretnih
kôdova.
Drukčije formulirano, nakon 32 kontrolna znaka slijedi praznina. Znamenka 0 ima kôd 48
(32 + 32/2). Nakon kôda 64 (2∙32) slijede velika slova abecede. Razlika između malog i
odgovarajućeg velikog slova abecede iznosi upravo 32.
Za uporabu u zemljama engleskog govornog područja bolji kôd ne treba tražiti. Kvalitetu
ovakvog kôda potvrđuje činjenica da se zadržao u uporabi kroz više od 30 godina. Zemlje
koje u svome pismu koriste različite dijakritičke znakove, među kojima je bila i bivša
Jugoslavija, snalazile su se tako da su umjesto nekih rjeđe korištenih znakova u kôd
postavljale svoja slova. Tako je na prostorima bivše Jugoslavije dulje vrijeme bio u uporabi
tzv. YUASCII kôd, propisan 1982. godine jugoslavenskim standardom (JUS) I.B1.002.
Naši su se znakovi prema tom standardu smještali na sljedeće pozicije:
Znak Dekadski Izvorni znak Znak Dekadski Izvorni znak
Č 126 ~ Č 94 ^
Ć 125 } Ć 93 ]
Đ 124 | Đ 92 \

41
Uvod u programiranje Prikaz informacija u računalu

Š 123 { Š 91 [
Ž 96 ` Ž 64 @
Zadržano je svojstvo da razlika između malog i odgovarajućeg velikog slova abecede
iznosi upravo 32. Sortiranje jednostavnom usporedbom znakova, naravno, nije bilo
moguće. Posebnu zanimljivost predstavlja činjenica da je na engleskim tipkovnicama (a
drugih tada praktički nije ni bilo) za utipkavanje znakova }, { i | trebalo stisnuti tipku
Shift, dok za znakove [, ] i \ to nije bilo. Time je logika utipkavanja velikih i malih slova
š, ć i đ bila suprotna od logike utipkavanja ostatka abecede. C program ispisan na uređaju
prilagođenom YUASCII standardu izgledao je otprilike ovako:
#include <stdio.h>
int main(void) š
int i, aŠ10Ć;
for (i = 0; i < 10; i++) š
aŠiĆ = i;
ć
ć
Zbrka je bila potpuna ako se koristio uređaj prilagođen, primjerice, njemačkim znakovima
s prijeglasom.
Srećom, brzo se uvidjelo da kontrola pariteta ionako nije naročito pouzdani mehanizam,
te da je osmi bit pametnije iskoristiti za proširenje skupa znakova. U novih se 128 pozicija
pronašlo mjesta za znakove većine europskih jezika. 1988. donesen je osmobitni JUS
I.B1.013 koji predstavlja nostrifikaciju međunarodnog standarda ISO (International
Organisation for Standardization) 8859-2 poznatog još i pod nazivom Latin-2, u kojem su
naši znakovi smješteni na sljedeće pozicije:
Znak Dekadski Znak Dekadski
č 232 Č 200
ć 230 Ć 198
đ 240 Đ 208
š 185 Š 169
ž 190 Ž 174
Nažalost, kompanija IBM u operacijskom sustavu PC DOS 5.0 za primjenu u istočnoj
Europi uvodi vlastiti standard, poznat kao kôdna stranica (codepage) 852, s vrlo sličnim
nazivom: Latin II. Slijede kôdovi ovoga standarda:
Znak Dekadski Znak Dekadski
č 159 Č 172
ć 134 Ć 143
đ 208 Đ 209
š 231 Š 230
ž 167 Ž 166
Microsoft, pak, u operacijski sustav Windows 3.1, u inačicu za istočnu Europu, uvodi
Kôdnu stranicu 1250:
Znak Dekadski Znak Dekadski
č 232 Č 200
ć 230 Ć 198
đ 240 Đ 208
š 154 Š 138
ž 158 Ž 142

42
Uvod u programiranje Prikaz informacija u računalu

Time nastaje apsurdna situacija: na istom računalu isti tekst prikazuje se različito, ovisno
o tome gleda li se u naredbenom prozoru ili drugim prozorima. Ovakvo stanje, nažalost,
postoji još uvijek u inačicama operacijskog sustava Windows35.
Da bi se podržali znakovi i ostalih svjetskih jezika, primjerice japansko, kinesko i arapsko
pismo, 256 znakova ni izdaleka nije dovoljno. Zato se 1993. godine donosi standard ISO
10646, pod nazivom Universal Multiple-Octet Coded Character Set (UCS). Ovaj je standard
prvi pokušaj da se uključe svi znakovi svih pisama korištenih u svijetu, kao i matematički
i drugi simboli. Trenutačno ovaj standard pokriva sva glavna i komercijalno značajna
pisma, i postoji u 2 inačice: 16-bitnoj (UCS-2) i 32-bitnoj (UCS-4).
Konzorcij glavnih američkih računarskih tvrtki specificira isprva svoj standard, 16-bitni
Unicode, koji u inačici 1.1 usklađuje s ISO 10646 inačicom UCS-2.
Unicode se u međuvremenu proširuje, pa ga je moguće naći u više inačica kodiranja, od
kojih su poznatije inačice UTF-8, UCS-2 i UTF-16 (nasljednik UCS-2).
UTF-8, najzastupljenija inačica od svih, koristi za prikaz znakova od jednog do četiri bajta.
Originalnih 128 ASCII znakova prikazuje se jednim bajtom. Sljedećih 1920 znakova
prikazuje se u dva bajta, čime su pokrivena sva europska pisma i dio zapadne Azije. Tri
bajta koriste se za sve ostale znakove u širokoj uporabi, uključujući kinesko, japansko i
korejsko pismo. Četiri bajta koriste se za prikaz rijetko korištenih kineskih, japanskih i
korejskih znakova, povijesnih pisama i emotikona. Duljina zapisa prepoznaje se po
vodećim bitovima prvog bajta, na sljedeći način
Bajta Bitova za Od Do 1. 1. bajt 2. 2. bajt 3. 3. bajt 4. 4. bajt
podatke kôda kôda
1 7 0 7F 0xxxxxxx -
2 11 80 7FF 110xxxxx 10xxxxxx -
3 16 800 FFFF 1110xxxx 10xxxxxx 10xxxxxx -
4 21 10000 10FFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

Naši znakovi nalaze se u u skupini Latin Extended-A:


Znak Dekadski Heksadekadski Znak Dekadski Heksadekadski
č 269 10D Č 268 10C
ć 263 107 Ć 262 106
đ 273 111 Đ 272 110
š 353 161 Š 352 160
ž 382 17E Ž 381 17D
Način kodiranja bit će objašnjen na primjeru slova č (26910, 10D16, 1000011012). Za
prikaz toga slova potrebno je, dakle, najmanje 9 bitova, pa će se koristiti dva bajta, od kojih
su 11 bitova za rezervirani za podatke, a 5 bitova za identifikaciju načina kodiranja. 9
bitova proširit će se na 11 dodavanjem dvije nule na početak:
00100001101

35Kodna stranici u naredbenom prozoru može se promijeniti naredbom chcp oznaka_koda. Važnije oznake
koda su 852, 1250, 28592 (ISO 8859-2) i 65001 (UTF-8)

43
Uvod u programiranje Prikaz informacija u računalu

Prvi bajt mora biti oblika 110xxxxx a drugi 10xxxxxx, tako da se prvih 5 od ukupno 11
bitova sprema u prvi bajt, a preostalih 6 u drugi bajt, pa je stvarni kôd slova č
11000100 10001101

Memorija (spremnik) računala


Kako što se u prethodnim poglavljima moglo vidjeti, podaci različitih tipova u računalu se
zapisuju s različitim brojem bitova: znak s 8 do 32 bita, cijeli broj sa 16 ili 32 bita, realni
broj s 32 ili 64 bita. Računalo koje bi pamtilo veću količinu podataka različitog tipa moglo
bi se sastojati od određenog broja 8 bitnih registara, određenog broja 16 bitnih registara
i tako dalje. Očito je da bi ovakva organizacija bila krajnje neprikladna. U slučaju da se u
nekom programu radi, primjerice, uglavnom s 32-bitnim brojevima, većina bi 8-bitnih, 16-
bitnih i 64 bitnih registara ostala neiskorištena.
Mogla bi se predložiti još jedna loša organizacija: načiniti registre toliko velikima da mogu
pohraniti bilo kakav podatak. Ako maksimalna veličina podatka koji se u nekom računalu
pohranjuje iznosi 64 bita, takvo bi računalo moralo imati 64-bitne registre. 8-bitni
podatak tada bi zauzimao samo jednu osminu registra, dok bi ostatak bio neiskorišten.
Ako se uzme u obzir da se svaki bit realizira konkretnim elektroničkim sklopom,
bistabilom, očito bi ovakva organizacija predstavljala neodgovorno rasipanje bistabilima.
Zato se spremnik ili memorija računala ostvaruju uvijek s registrima jednakih veličina,
kojima veličina odgovara najmanjem podatku. U modernim računalima veličina registra
u memoriji uvijek je 8 bita ili 1 bajt. 16-bitni podatak pohranit će se u dva takva registra,
32-bitni u 4, 64-bitni u 8.
Takvi 8-bitni registri, kojih danas čak i u široko dostupnim računalima ima i do nekoliko
milijardi, označavaju se rednim brojevima. Prvi registar nosi redni broj 0, drugi 1, i tako
dalje. Redni broj registra naziva se i adresom.
Memorija računala može se, dakle, shematski prikazati sljedećom slikom, u kojoj svaki
pravokutnik označava 1 bajt, a n ukupan broj bajtova memorije. Oznake uz pravokutnike
označavaju adrese.
0 1 2 3 4 5 6 7 ... n-6 n-5 n-4 n-3 n-2 n-1
...
Količina bajtova u memoriji računala kolokvijalno se mjeri kilobajtima, megabajtima i
gigabajtima. Međutim, kod „kilobajta“ radi se o 210=1024 bajta, kod „megabajta“ o 210
„kilobajta“ ili 220 = 1048576 bajta, a kod „gigabajta“ o 210 „megabajta“ ili 230 = 1073741824
bajta. Kilo, mega i giga prefiksi su koji označavaju množitelje 103, 106 i 109, pa je ispravnije
koristiti prefikse kibi, mebi i gibi prihvaćene 1998. godine od strane Međunarodnog
elektrotehničkog povjerenstva (International Electrotechnical Commission, IEC) a koji
označavaju množitelje 210, 220 i 230. Ispravno je, dakle, memoriju mjeriti u kibibajtima,
mebibajtima i gibibajtima.
Bajtovi višebajtnog registra također se označavaju rednim brojevima. Bajt k-bajtnog
registra koji sadrži najniže potencije broja 2 označava se rednim brojem 0 i nosi naziv
najmanje značajni bajt (Least Significant Byte, LSB). Bajt k-bajtnog registra koji sadrži
najviše potencije broja 2 označava se rednim brojem k-1 i nosi naziv najznačajniji bajt
(Most Significant Byte, MSB). Na primjeru 4-bajtnog registra, bajtovi će se označili ovako:

44
Uvod u programiranje Prikaz informacija u računalu

3(MSB) 2 1 0(LSB)
Višebajtni registar može se na zadanu adresu u memoriji pohraniti na dva načina:
• tako da se najznačajniji bajt pohrani na zadanu adresu, manje značajni bajt na
sljedeću višu adresu i tako dalje. Ovakav način pohrane zove se veliki endijan (Big
Endian). Kao primjer, ilustrirat ćemo pohranjivanje 4-bajtnog registra na adresu 2:
0 1 2 3 4 5 6 7 ... n-6 n-5 n-4 n-3 n-2 n-1
...
3(MSB) 2 1 0(LSB)

• tako da se najmanje značajni bajt pohrani na zadanu adresu, značajni bajt na


sljedeću višu adresu i tako dalje. Ovakav način pohrane zove se mali endijan (Little
Endian). 4-bajtni registar na adresi 2 sad izgleda ovako:
0 1 2 3 4 5 6 7 ... n-6 n-5 n-4 n-3 n-2 n-1
...
0(LSB) 1 2 3(MSB)

Nazivi Little Endian i Big Endian nastali su prema romanu Guliverova putovanja Jonathana
Swifta36, engleskoj satiri suvremenog društva iz 18. stoljeća. Junak romana, Guliver, dolazi
u carstvo Liliput, gdje se djed tadašnjeg cara kao dječak posjekao razbijajući jaje na
tradicionalni način, s debljeg kraja. Zbog toga je donesen zakon po kojem se, pod
prijetnjom teških kazni, jaja moraju razbijati s tanjeg kraja. Narod je zakon dočekao tako
ogorčeno da je izbilo šest velikih pobuna u kojima je poginulo mnogo ljudi. Pristaše
zakona nazivali su se Big Endians, a pobunjenici Little Endians. Susjedno carstvo Blefuscu
mnogo je puta slalo izaslanike koji su pokušali objasniti da je svejedno s kojeg se kraja
razbijaju jaja. Tvorci naziva Little Endian i Big Endian očito su htjeli naglasiti da je
svejedno i kojim će se redom bajtovi registra spremati u memoriju. Naravno, jednom
odabrani način mora se rabiti konzistentno.
U danas brojem najzastupljenijima, osobnim računalima, koristi se način Little Endian.

36Jonathan Swift (30.11.1667., Dublin - 19.10.1745., Dublin), irski satirist, pisac, pjesnik i svećenih
engleskog podrijetla

45
Uvod u programiranje Uvod u programiranje

Uvod u programiranje
I najsuvremeniji procesori današnjice sposobni su obaviti samo ograničeni skup
instrukcija (logičke i aritmetičke operacije, kopiranje podataka, enkripcija i sl.). Kao što je
već spomenuto, Von Neumannova arhitektura elektroničkih računala i njene moderne
izvedenice ograničene su na sasvim elementarne operacije: dohvat sljedeće instrukcije iz
memorije u procesor, dohvat podataka nad kojima će se instrukcija obaviti, obavljanje
instrukcije, te pohranjivanje rezultata u memoriju, s eventualnom odlukom o sljedećoj
instrukciji koju treba obaviti. Čak i jednostavni problemi svakodnevnice sastoje se od
većeg broja takvih osnovnih instrukcija, iako to na prvi pogled nije vidljivo.
Kao primjer može poslužiti određivanje najvećeg u skupu brojeva
{1, 7, 105, 16, 20}
Rezultat je, očigledno, 105. Ljudski je mozak u stanju čak i bez razmišljanja ponuditi
odgovor. Ali pokušajmo odrediti najveći broj u sljedećem skupu:
{41, 18467, 6334, 26500, 19169, 15724, 11478, 29358, 26962, 24464, 5705, 28145,
23281, 16827, 9961, 491, 2995, 11942, 4827, 5436, 32391, 14604, 3902, 153, 292, 12382,
17421, 18716, 19718, 19895, 5447, 21726, 14771, 11538, 1869, 19912, 25667, 26299,
17035, 9894, 28703, 23811, 31322, 30333, 17673, 4664, 15141, 7711, 28253, 6868,
25547, 27644, 32662, 32757, 20037, 12859, 8723, 9741, 27529, 778, 12316, 3035,
22190, 1842, 288, 30106, 9040, 8942, 19264, 22648, 27446, 23805, 15890, 6729, 24370,
15350, 15006, 31101, 24393, 3548, 19629, 12623, 24084, 19954, 18756, 11840, 4966,
7376, 13931, 26308, 16944, 32439, 24626, 11323, 5537, 21538, 16118, 2082, 22929,
16541, 4833, 31115, 4639, 29658, 22704, 9930, 13977, 2306, 31673, 22386, 5021,
28745, 26924, 19072, 6270, 5829, 26777, 15573, 5097, 16512, 23986, 13290, 9161,
18636, 22355, 24767, 23655, 15574, 4031, 12052, 27350, 1150, 16941, 21724, 13966,
3430, 31107, 30191, 18007, 11337, 15457, 12287, 27753, 10383, 14945, 8909, 32209,
9758, 24221, 18588, 6422, 24946, 27506, 13030, 16413, 29168, 900}
Ovdje će i čovjek morati uporabiti određeni princip, a možda se i koristiti olovkom. Ovaj
problem može se riješiti na sljedeći način:
Zapamtiti (zapisati) prvi od brojeva u skupu kao dosad najveći
Ponavljati sve dok ima brojeva
Uzeti sljedeći broj
Ako je taj broj veći od dosad najvećeg
Zapamtiti (zapisati) ga kao dosad najvećeg
Problem smo, dakle, raspisali u niz ponavljajućih sasvim jednostavnih operacija: napisali
smo prvi algoritam.

Algoritam
Algoritam je skup pravila kojima se opisuje kako riješiti neki problem. Riječ algoritam je
eponim37, nastao kao izvedenica imena perzijskog matematičara imenom Muhammad ibn
Musa al-Khwarizmi. Al-Kwarizmi („iz Kwarezma“), rođen oko 780. godine u mjestu
Kwarezm na današnjoj granici Uzbekistana i Kazahstana, smatra se utemeljiteljem
algebre. Ustaljena riječ algebra je izvedenica iz pojma al-ğabr u imenu njegovog glavnog

37Eponim (grč. Eponymon: ime boga ili junaka po kome je što nazvano) je riječ nastala poopćivanjem nečijeg
imena (npr. cepelin, giljotina, narcis, penkala)

46
Uvod u programiranje Uvod u programiranje

djela, Al-kitāb al-mukhtaṣar fī ḥisāb al-ğabr wa’l-muqābala (Sažeta knjiga o računanju


prenošenjem i poništenjem). Original njegovog drugog glavnog djela, knjige o aritmetici
za koju se pretpostavlja da se zvala Kitāb al-Jam‘ wat-Tafrīq bi-Ḥisāb al-Hind (Knjiga o
zbrajanju i oduzimanju prema Hindu računici) je izgubljen, ali je sačuvan latinski prijevod
pod nazivom Algoritmi de numero indorum (Al-Kwarizmi o Hindu brojevima). Pravila
računanja u toj knjizi započinju riječima Dixit Algorizmi (Al-Kwarizmi je rekao), pa se
postepeno riječ algoritam počinje uobičajeno koristiti za neki skup pravila, prvo u
matematici, a kasnije i u drugim područjima.
Algoritam mora posjedovati sljedeća svojstva:
• Imati početne objekte nad kojima se obavljaju operacije
• Rezultirati završnom objektima (rezultatima)
• Biti precizan: svaki korak mora biti jasno opisan
• Biti konačan: mora završiti nakon konačnog broja koraka/instrukcija
• Biti ispravan: za svaki početni objekt mora dati točan rezultat
• Biti djelotvoran: mora dati rezultat u konačnom vremenu, čak i kad bi se izvodio
koristeći olovku i papir
Nužno je ovdje istaknuti razliku između pojmova djelotvoran (effective) i učinkovit
(efficient), koji su naizgled sinonimi. Matematički gledano, milijarda godina je konačno
vrijeme i algoritam koji bi dao točan rezultat nakon tolikog vremena zadovoljava gore
pobrojana svojstva, ali je neuporabiv. Primjer potpuno neuporabivog algoritma može se
ilustrirati na primjeru programa za igranje šaha, koji bi sljedeći potez odabirao na temelju
analize svih mogućih posljedica toga poteza. Pokušat ćemo to ilustrirati na vrlo
pojednostavnjen način.

Bijeli, koji je na prvom potezu, može povući bilo koji od 20 mogućih poteza (bilo koji
pješak jedno ili dva polja naprijed te 2 moguća poteza svakim skakačem). Crni na to može
odgovoriti također s 20 poteza. U sljedećem potezu, ako je prvi potez otvorio lovca ili
kraljicu, broj poteza u nastavku se još i povećava. Zamislimo, radi jednostavnosti, da je

47
Uvod u programiranje Uvod u programiranje

broj mogućih poteza svakog igrača u svakom potezu 20. Kad bismo gledali samo 10 poteza
unaprijed, riječ je o
(20x20)x(20x20)x(20x20)x(20x20)x(20x20)x(20x20)x(20x20)x(20x20)x(20x20)x(20x
20)=
2020 = 104.857.600.000.000.000.000.000.000 različitih situacija. Kad bi se svaka od tih
situacija analizirala samo jednu nanosekundu, što je nije sasvim realno za mogućnosti
današnjih računala koja pojedinačnu instrukciju izvode u vremenu reda veličine
nanosekunde, takav algoritam bi završio za
104.857.600.000.000.000 sekundi / 60 / 60 / 24 / 365 = 79.800.304.414 godine.
Tek tada bismo povukli prvi potez i opet jednako dugo čekali na nastavak. Riječ je o
teorijski djelotvornom, ali potpuno neuporabivom algoritmu.
Učinkovitost algoritma ne može se jednoznačno kvantificirati i ovisi o namjeni. Algoritam
koji bi na Google maps pola sata tražio optimalnu rutu od kuće do posla svakako je
neuporabiv. Ako ga i pokrenemo na vrijeme, prije doručka, prometna situacija se sigurno
u međuvremenu promijenila. S druge strane, algoritam koji bi isto toliko dugo tražio
optimalni godišnji proizvodni program neke velike kompanije sigurno se ne bi mogao
smatrati neuporabivim.
Algoritmom se, općenito, može nazvati svaki skup uputa za obavljanje nekog posla.
Sljedeći recept za kiseljenje krastavaca zadovoljava sva svojstva algoritma:
▪ Početni objekti:
• 5 kg krastavaca, 1 l alkoholnog octa (9%), 30 dag šećera, 10 dag soli, kopar,
papar
▪ krastavce i kopar oprati i posložiti u čiste staklenke
▪ u 2 l vode dodati ocat, šećer, sol i papar
▪ zakuhati uz miješanje
▪ vruću otopinu uliti u staklenke
▪ staklenke zatvoriti celofanom i gumicom
▪ složiti staklenke u široki lonac napunjen vodom do grla staklenki
▪ ako je toplomjer raspoloživ
• zagrijati vodu do 80 stupnjeva
▪ inače
• zagrijavati dok se s dna ne počnu dizati mjehurići zraka
▪ ostaviti stajati barem 24 sata
▪ Završni objekti:
• vrlo ukusni kiseli krastavci (višekratno isprobano od strane autora)
Dio algoritma opisan kurzivom je algoritam sam za sebe i upućenima koji ga poznaju
dovoljno bi bilo reći „krastavce pasterizirati38“.

38 Još jedan eponim, po Louisu Pasteuru (27.12.1822., Dole – 28.09.1895., Marnes-la Coquette), francuskom
biologu, mikrobiologu i kemičaru koji je, osim cijepljenja, osmislio i proces eliminacije patogenih bakterija
iz hrane zagrijavanjem na visoku temperaturu ispod točke ključanja.

48
Uvod u programiranje Uvod u programiranje

Program i programiranje
Pod pojmom program u širem smislu riječi smatra se bilo koji unaprijed definirani slijed
događaja ili akcija (televizijski program, program putovanja i sl.).
U užem smislu riječi, u računarstvu, pod pojmom program podrazumijeva se opis
algoritma u nekom programskom jeziku39 kojim se jednoznačno određuje što računalo
treba napraviti. Proces opisivanja algoritma nekim od programskih jezika naziva se
programiranjem.
Odabir programskog jezika u kojemu će se programirati uvelike ovisi o namjeni programa.
Programi za web koji se izvode u internetskom pregledniku pisat će se u Javascriptu.
Poslužiteljski sloj bit će pisan u Javi, Javascriptu, C#-u, možda i u PHP-u. Postupci nad
podacima u bazi podataka programirat će se u SQL-u. Programi za mikrokontrolere pisat
će se u C-u ili C++-u. Sve je više, kao jezik opće namjene, u uporabi i Python. Smatra se da
je u svijetu u uporabi više stotina programskih jezika, od kojih smo ovdje pobrojali samo
najpopularnije.
O pristupu učenju programiranja ne postoje zajednički stavovi u svjetskoj akademskoj
zajednici: neki zagovaraju odmah učenje objektnog jezika, drugi klasičnu proceduralnu
paradigmu, treći pak funkcijsko programiranje.
Stav je autora ovih redaka da je za buduće inženjere elektrotehnike, računarstva i
informacijske tehnologije ipak primjereno započeti u klasičnom, proceduralnom jeziku C,
jer je u današnjem interdisciplinarnom svijetu u kojem je nemoguće povući oštru granicu
između elektrotehnike i računarstva C jezik koji najbolje spaja oba svijeta, softver i
hardver, rezultira najbržim izvršnim kôdom 40 , a sintaksa C-a koristi se u 4 od 5
najpopularnijih programskih jezika, a oni su (redom, prema situaciji iz 2018.) Java, C, C++,
Python i C#. Sva znanja stečena u proceduralnom C-u bit će uporabiva i tijekom kasnijeg
prelaska na objektnu paradigmu41.

Prvi program u C-u, pseudokôd i dijagram toka


Gotovo svaka knjiga o programiranju na ovom mjestu spomenut će elementarni program
koji na izlaznu jedinicu računala ispisuje tekst „Hello, world!“. Ovdje ćemo ipak otići korak
dalje, napisati malo složeniji program i usput uvesti pojmove pseudokôda i dijagrama
toka.
Pretpostavimo da s tipkovnice računala treba učitati dva cijela broja, ispisati ih na zaslon,
a zatim na zaslon ispisati veći od učitanih brojeva.

39 Programski jezik: rječnik i skup gramatičkih pravila kojima se računalu opisuje kako obaviti neki posao
40Jednostavan program koji 10000 puta zbraja sve brojeve od 1 do 1000 u Pythonu se na računalu s Intel
I7 procesorom na 4.7 GHz izvodi oko 3 sekunde, a u C-u s potpuno isključenim optimizacijama prevodioca
(opcija -O0) 66 milisekundi. S prvom razinom optimizacije (opcija -O1) program u C-u se izvodi samo 8
milisekundi ili skoro 400 puta brže nego u Pythonu. Na sljedećim razinama optimizacije, C prevodilac
primjećuje da se 10000 puta radi jedno te isto pa se zbrajanje svih brojeva obavi samo jednom, u vremenu
reda veličine mikrosekunde
41Paradigma (grč. parádeigma, pokazati, učiniti razumljivim): skup osnovnih pravila koje prihvaćamo pri
poimanju stvarnosti. U računarstvu predložak, primjer ili način razmišljanja prema kojemu gradimo
programe

49
Uvod u programiranje Uvod u programiranje

Općenito, ovaj zadatak možemo rastaviti u nekoliko koraka i napisati pseudokôd 42


najopćenitije razine, uobičajeno se služeći pritom imperativom, jer se radi o naredbama
računalu:
učitaj brojeve
ispiši brojeve
odredi veći broj
ispiši veći broj
Ovaj pseudokôd treba dalje razraditi i dati imena podacima s kojima se radi. Nazovimo
učitane brojeve m i n, a neka se rezultat zove r. Detaljniji pseudokôd tada će izgledati
ovako:
učitaj m i n
ispiši m i n
ako je m > n tada
r = m
inače
r = n
ispiši rez
Osim pseudokodom, algoritam je moguće opisati i dijagramom toka43. Osnovni grafički
elementi dijagrama toka su:

Početak ili kraj

Nastavak izvršavanja

Akcija

Ulaz

Izlaz

Odluka

Služeći se ovim simbolima, naš algoritam možemo opisati ovako:

42 Pseudokôd: više ili manje detaljan opis algoritma u uobičajenim terminima govornog jezika
43 Dijagram toka: grafički opis algoritma ili, općenito, funkcioniranja nekog sustava

50
Uvod u programiranje Uvod u programiranje

početak

učitaj m i n

m>n

r=m r=n

ispiši m i n

kraj

Slika 15. Dijagram toka određivanja većeg od dva broja

Konkretan program u programskom jeziku C izgledat će ovako:


#include <stdio.h>
int main(void) {
int m, n, r;
scanf("%d %d", &m, &n);
if (m > n) {
r = m;
} else {
r = n;
}
printf("%d\n", r);
return 0;
}
Program 2. OdredjivanjeVecegBroja.c

Prikazani kôd programa potrebno je prvo unijeti u računalo. Programi pisani u


programskom jeziku C uobičajeno se upisuju u datoteke 44 s nastavkom imena
(ekstenzijom) .c. Za uređivanje kôda može se koristiti bilo koji na računalu već instalirani
uređivač teksta koji u datoteku neće ugrađivati dodatne skrivene oznake za formatiranje
kao što to čini npr. Word. Primjer takvog programa na Windowsima je osnovni uređivač
notepad, na Linuxu vi, vim ili gedit.
Programski kôd pisan u nekom programskom jeziku općenito se naziva izvornim ili
simboličkim kôdom. Procesor elektroničkog računala, međutim, izvršava samo naredbe

44 Datoteka (engl. file) je imenovani skup podataka zapisan na nekom mediju za pohranu

51
Uvod u programiranje Uvod u programiranje

pisane binarnim kôdom, pa je zato program u C-u potrebno prije izvođenja nekako prvo
pretvoriti u binarni ili izvršni kôd. Općenito, programski jezici dijele se u dvije kategorije
prema načinu pretvorbe izvornog u izvršni kôd. To su:
• Jezici koji se interpretiraju: izvođenje programa preuzima program koji se naziva
interpreter i koji analizira izvorni kôd naredbu po naredbu te izvršava
odgovarajuće binarne instrukcije.
• Jezici koji se prevode: programi se prije izvođenja moraju prevesti u izvršni kôd
kao cjelina i potom kao cjelina proslijediti na izvršavanje.
Proces pretvorbe provodi se u dva bitna koraka45:
Prevođenje u objektni kôd obavlja se programom koji se naziva prevodilac (compiler).
Prevodilac prevodi datoteku po datoteku simboličkog kôda i stvara datoteke s objektnim
kôdom (obično s nastavkom .o), koji se još ne može izvoditi jer u njemu ne postoje
poveznice s funkcijama46 i varijablama47 iz drugih datoteka objektnog kôda i programskih
biblioteka (libraries) u kojima su pripremljene standardne funkcije (ulaz/izlaz,
matematičke funkcije itd).
Povezivanje u izvršni kôd obavlja se programom koji se zove povezivač (kolektor, linker).
Taj program spaja sve potrebne datoteke objektnoga kôda s unaprijed pripremljenim
programskim bibliotekama i stvara izvršni kôd koji se može izravno pokrenuti iz
operacijskog sustava48. Izvršni strojni kôd je na operacijskom sustavu Windows datoteka
s nastavkom .exe, na operacijskim sustavima Unix/Linux oblik nastavka nije propisan.
Jedan od najčešće korištenih besplatnih prevodilaca je gcc49. Dostupan je na gotovo svim
platformama i u najjednostavnijem obliku poziva se tako da se u naredbenom ili
terminalskom prozoru (command promt, terminal window) operacijskog sustava izvrši
naredba
gcc izvorna_datoteka -o izvršna_datoteka
Ova naredba, kao i sve druge naredbe koje se zadaju operacijskom sustavu, sastoji se od
imena programa ili interne naredbe operacijskog sustava (gcc), imena datoteka s kojima
će se raditi, te opcija koje modificiraju način rada programa ili interne naredbe. Opcije se
većinom mogu prepoznati po prefiksima -, -- ili /. U konkretnom slučaju opcija -o (output)
kazuje prevodiocu da iza nje slijedi ime pod kojim treba spremiti izvršni program.
Ako datoteka u kojoj je spremljen izvorni program nosi naziv program.c, tada će naredba
za prevođenje na Windowsima glasiti

45 Sam proces prevođenja u objektni kôd zapravo se sastoji od tri koraka: pretprocesiranja, stvaranja
asemblerskog kôda i tek na kraju objektnog koda, ali ovi se koraci u pravilu izvode nedjeljivo. O njima će
biti više rečeno kasnije.
46Funkcija: programska cjelina koja obavlja određeni zadatak na temelju ulaznih parametara te većinom
vraća neki rezultat programskoj cjelini iz koje je pozvana.
Varijabla (lat. variabilis; promjenljivo): općenito, veličina promjenljive vrijednosti. U računarstvu, dio
47

memorije kojem je dodijeljeno ime i čiji sadržaj se može promijeniti kao cjelina
48Operacijski sustav (engl. operation system) je program koji se automatski pokreće po uključivanju
računala te brine o međudjelovanju korisnika, softvera i hardvera
49 gcc: GNU Compiler Collection, sustav načinjen u sklopu GNU projekta koji podržava više programskih
jezika. GNU projekt je besplatni softver, nastao masovnom suradnjom programera diljem svijeta, a sastoji
se od više programskih paketa za različite namjene

52
Uvod u programiranje Uvod u programiranje

gcc program.c -o program.exe


a za Linux
gcc program.c -o program

Napomena:
Prevodilac bi se mogao pozvati i bez navođenja imena izvršnog programa, primjerice
gcc program.c
U tome će slučaju nastati izvršna datoteka pretpostavljenog imena a.exe (ili a.out na
Linuxu) što je pogodno možda samo za inicijalno testiranje programa.
Tehnički, gcc nije samo prevodilac nego i sučelje prema pretprocesoru, asembleru i
povezivaču koji će, za korisnika nevidljivo, odraditi cijeli posao konverzije izvornog u
izvršni program.
Raščlanimo sada naš program u C-u naredbu po naredbu, s napomenom da će detaljno
objašnjenje svih ovdje spomenutih koncepata uslijediti kasnije.
#include <stdio.h>
#include je naredba pretprocesoru koja u izvršni program uključuje cjelokupni sadržaj
sistemske datoteke stdio.h (akronim od standard input output) koja se nalazi na
pretpostavljenom mjestu za određeni prevodilac. Na Linuxu je to obično mapa
/usr/include, na Windowsima podmapa include mape u kojoj je instaliran prevodilac.
Konkretna datoteka sadrži prototipove 50 funkcija za ulaz i izlaz te neke simboličke
konstante.
Napomena:
Prevodilac iz izvorne datoteke, nakon uključenja kôda iz svih potrebnih include datoteka
i obavljanja ostalih naredbi pretprocesora o kojima će biti govora kasnije, stvara
privremeno datoteku izvornog kôda s nastavkom .i. Nakon toga, iz izvornog kôda nastaje
asemblerski51 kôd u datoteci s nastavkom .s. Asembler se dalje prevodi u objektni kôd i
tek tada povezuje s bibliotekama u izvršni kôd. Datoteke .i, .s, i .o automatski se brišu (osim
u slučaju nezavisnog prevođenja pojedinih modula o čemu će biti govora kasnije).
Ako se prevođenje obavi uz opciju -save-temps, privremene datoteke ostat se sačuvane i
mogu se, osim .o koja sadrži binarni kôd, pregledati uređivačem teksta:
gcc -save-temps program.c -o program.exe
int main(void) {
Svaki program mora imati jednu početnu točku (entry point) od koje započinje njegovo
izvođenje. U C-u je to funkcija obveznog naziva main. Funkciju main poziva operacijski
sustav u trenutku pokretanja programa iz naredbenog prozora navođenjem naziva
izvršne datoteke:
program.exe
ili

50 Prototip: programska naredba koja sadrži samo tip funkcije, naziv funkcije te tipove i imena parametara,
a služi prevodiocu da provjeri je li funkcija pozvana na odgovarajući način
51Asembler (engl. assembler, assembly language): simbolički programski jezik najniže razine u kojemu,
načelno, naredbe odgovaraju instrukcijama procesora

53
Uvod u programiranje Uvod u programiranje

program
Opis svake funkcije mora prije imena sadržavati tip rezultata. Funkcija main vratit će
status izvođenja operacijskom sustavu u kojemu se taj status može analizirati i ovisno o
njemu pokrenuti sljedeća akcija. Taj status je uvijek cijeli broj (int).
Također, u opisu svake funkcije, nakon imena, u zagradama, definiraju se varijable čije će
vrijednosti funkciji biti proslijeđene pri pozivu s nadređene razine. Takve se varijable
nazivaju parametrima funkcije. Nadređena cjelina prosljeđuje konkretne vrijednosti
parametrima. Takve se konkretne vrijednosti nazivaju argumentima. Operacijski sustav
može programu proslijediti argumente, kao što su naziv izvorne i odredišne datoteke te
opcije pri pozivu prevodioca gcc. Ovaj jednostavni program ne dobiva nikakvu
informaciju od operacijskog sustava pa se to označava ključnom rječju void52 na mjestu
gdje bi inače bio popis parametara.
Prva linija programa završava znakom {. Ovaj znak označuje početak bloka naredbi u
programskom jeziku C i mora biti uparen sa znakom } kojim se završava blok. Pojedinačne
naredbe unutar bloka završavaju znakom ;.
int m, n, r;
Sintaksa C-a zahtijeva da se prije korištenja definiraju sve varijable navođenjem njihova
imena i tipa vrijednosti koju će sadržavati. U našem primjeru sve su varijable cjelobrojne
(int).
scanf("%d %d", &m, &n);
scanf je funkcija iz standardne biblioteke, koja tekst unesen s tipkovnice pretvara u
binarne vrijednosti i pohranjuje u memoriju. Prvi argument je obvezno tekst koji se naziva
format i koji sadrži jednu ili više konverzijskih specifikacija koje upravljaju načinom
pretvorbe. U ovom primjeru konverzijska specifikacija %d rezultira pretvorbom
unesenog teksta koji se sastoji od niza dekadskih znamenki u binarnu reprezentaciju
cijelog broja. Očekuju se dva niza znamenki koji su međusobno odvojeni prazninom.
Slijede memorijske adrese na koje se smještaju tako pretvorene vrijednosti. Adresa treba
biti upravo onoliko koliko ima konverzijskih specifikacija u formatu. O adresama i
pokazivačima bit će više govora kasnije, za sada treba zapamtiti da ispred svake varijable
koja se učitava funkcijom scanf treba stajati operator &.
if (m > n) {
Naredba if upravlja nastavkom programa. Ako je izraz u zagradi istinit (vrijednost
varijable m veća je od vrijednosti varijable n) tada će se obaviti programski blok koji
neposredno slijedi, započet s { i završen s }.
r = m;
Vrijednost varijable m pohranjuje se u varijablu r.
} else {
Završetak prethodnog bloka i naredba else koja se odnosi na prethodni if i definira blok
naredbi započet s { koji će se izvesti ako izraz u if-u nije istinit.
r = n;
Vrijednost varijable n pohranjuje se u varijablu r.
}

52 engl. void: prazno, prazan prostor

54
Uvod u programiranje Uvod u programiranje

Završetak bloka naredbi koji se obavlja ako izraz u if-u nije istinit
printf("%d\n", r);
Funkcija printf u komandni prozor ili terminal ispisuje sadržaj varijabli koje su joj
predane preko argumenata. Suprotno funkciji scanf, ovdje se obavlja pretvorba binarnih
vrijednosti pohranjenih u varijablama u čovjeku čitljiv tekst. Načinom pretvorbe i ovdje
upravljaju format i konverzijske specifikacije koje format sadrži. U ovom primjeru, kao
posljedica konverzijske specifikacije %d cjelobrojna binarna vrijednost varijable r
pretvara se u niz dekadskih znamenki koje se ispisuju u prozoru.
return 0;
Naredba return prekida izvođenje funkcije i na nadređenu razinu kao rezultat vraća
vrijednost koja slijedi neposredno iza riječi return. U ovom primjeru nadređena
programska cjelina je operacijski sustav, a vrijednost 0 po konvenciji obavještava
operacijski sustav da je sve proteklo u redu.
}
Završetak bloka funkcije main.
Zanimljivo je kronološki pratiti vrijednosti varijabli m, n i r tijekom izvođenja programa.
U tablici s desna bit će trenutačne vrijednosti varijabli nakon izvođenja naredbe s lijeve
strane. Treba primijeti da samom definicijom varijable nije postavljena njena vrijednost,
nego se tamo nalazi neka u u memoriji zatečena kombinacija bita. U sljedećem primjeru s
tipkovnice je utipkano
3 5

int main(void) { m n r
int m, n, r; ? ? ?
scanf("%d %d", &m, &n); 3 5 ?
if (m > n) { 3 5 ?
r = m; Naredba se ne izvodi
} else { 3 5 ?
r = n; 3 5 5
} 3 5 5
printf("%d", r); 3 5 5
return 0; 3 5 5
}

Stilovi pisanja programa


C prevodilac prepoznaje naredbe i blokove naredbi isključivo služeći se znakovima {, } i ;.
Praznine, tabulatori ili skokovi u nove retke potpuno su irelevantni za prevodilac, koji će
bez poteškoća korektno prevesti i sljedeći kôd:
#include <stdio.h>int
main(void){int m, n, r;scanf("%d %d", &m, &n);if (m >
n) {r
= m;} else {r = n;}printf("%d", r);return 0;}
Naravno, u ovako pisanom programu čovjeku će se biti gotovo nemoguće snaći. Zato je
dobro pri pisanju programa postaviti neke konvencije ili standarde i kasnije ih se čvrsto

55
Uvod u programiranje Uvod u programiranje

držati. Postoji više šire prihvaćenih standarda za pisanje koda, npr: LLVM, Google,
Chromium, Mozilla i WebKit. Ovdje predložena pravila dio su LLVM 53standarda.
U pravilu, naredba treba započinjati u novom retku. Sadržaj bloka naredbi uvlači se desno
za određen broj slovnih mjesta, koji ne smije biti manji od 2 jer se vidljivost uvlačenja gubi,
a ne bi trebao prelaziti 4 jer razina uvlačenja može biti više (ako unutar bloka započinje
drugi blok i tako dalje) pa bi se brzo potrošio koristan prostor. Ovo uvlačenje,
preuzimanjem izvornog engleskog glagola to indent, kolokvijalno se naziva i indentacijom.
U jednom od prihvaćenih stilova pisanja u programskom jeziku C svaki znak kojim se
započinje blok naredbi ({) piše se sâm u novom retku, pa bi u tome stilu pisanja prethodni
program izgledao ovako:
#include <stdio.h>
int main(void)
{
int m, n, r;
scanf("%d %d", &m, &n);
if(m > n)
{
r = m;
}
else
{
r = n;
}
printf("%d", r);
return 0;
}
Po mišljenju autora ovih redaka time se nepotrebno troši nekoliko linija u prozoru
uređivača kôda, pa će zato u ovom tekstu biti korištena konvencija u kojoj se znak { piše
na kraju linije kojom se započinje blok, a znak } kojim će taj blok završiti potpisuje se točno
ispod početka teksta u liniji kojom je započeo:
int main(void) {
int …
if (m > n) {


}
}
Postoji i više konvencija korištenja praznina unutar retka.
Preporuča se:
• ne stavljati prazninu između imena funkcije i otvorene zagrade koja prethodi
argumentima, pri definiciji i pri pozivu, niti prije znaka ; kojim završava naredba:
int main(void) {
printf("%d", r);
• stavljati praznine prije znaka { kojim započinje blok:

53LLVM: skup prevodilaca razvijen iz istraživačkog projekta na University of Illinois at Urbana–Champaign


pod inicijalnim nazivom Low Level Virtual Machine

56
Uvod u programiranje Uvod u programiranje

int main(void) {
• stavljati praznine iza zareza kojim se odvajaju elementi liste argumenata, ali ne
prije njega niti iza zagrade kojim započinje lista argumenata:
scanf("%d %d", &m, &n);
• stavljati praznine iza naredbe kojima se upravlja programskim tokom:
if (m > n) {
• stavljati praznine između operatora i operanada:
if (m > n) {
r = m;

Programske greške
Programiranje elektroničkog računala ne dopušta niti najmanju pogrešku. Svako slovo,
brojka i interpunkcija u programskom kôdu ima svoje značenje, stoga i najmanja pogreška
pri pisanju programa može rezultirati nemogućnošću prevođenja ili neispravnim radom
programa.
Srećom, veliki broj tako nastalih grešaka lako otkriva prevodilac. Ljudskom je oku nešto
teže uočiti grešku u ovom programu:
int main(void) {
printf('Dobar dan');
}
ali prevodilac će primijetiti da je niz znakova koji se želi ispisati napisan korištenjem
jednostrukih, a ne dvostrukih navodnika.
Greške kojima su prekršena pravila pisanja programa nazivaju se sintaktičkim54 greškama
i ne predstavljaju velik manji problem programerima.
Sljedeći primjer sadrži grešku koja se ponekad smatra sintaktičkom, jer će biti dojavljena
tijekom prevođenja:
int main(void) {
int I;
l = 1;
}
Ovdje se vrijednost 1 pokušava upisati u varijablu s imenom l, koja nije nigdje definirana:
definirana je varijabla s imenom I.
Striktno gledano, ovaj program je formalno korektan jer nigdje nisu prekršena formalna
gramatička pravila pisanja i spada u kategoriju statičkih semantičkih55 grešaka.
Slijedi još jedan primjer statičke semantičke greške:
int main(void) {
print("Dobar dan");
}

54sintaksa (grč. sýntaksis: slaganje u red, uređivanje): skup pravila kojim se određuje kako se gramatički
strukturira program
55semantika (grč. sēmantikós: značajan): značenje. U programiranju, odnosi se na značenje ili smisao
pojedinačnih naredbi odnosno cjelina.

57
Uvod u programiranje Uvod u programiranje

koja će biti otkrivena u procesu povezivanja, jer funkcija print ne postoji ni u jednoj
biblioteci.
Dinamičke semantičke greške, češće zvane logičkim greškama, najteže je otkriti. Radi se o
greškama koje rezultiraju neispravnim rezultatom izvođenja programa kao posljedicom
pogreške u razmišljanju, korištenja krivih operatora, varijabli i slično.
Slijedi nekoliko jednostavnih primjera dinamičkih semantičkih grešaka, za koje
objašnjenje nije potrebno:
povrsina = sirina + visina;

if (a > b) {
printf("a je manje od b");
}

printf("a uvećan za 1 iznosi %d", a – 1);


Ponegdje se još, kao posebna kategorija, može susresti pojam greške u oblikovanju
(dizajnu). Ovdje se radi o programu koji je sintaktički ispravan, radi sve kako treba, ali je
za rješavanje problema korišten pogrešan algoritam.

58
Uvod u programiranje Osnove pisanja programa u jeziku C

Osnove pisanja programa u jeziku C


Funkcija main
Program pisan u programskom jeziku C sastoji se minimalno od definicije funkcije main.
Sljedeći program potpuno je ispravan, iako besmislen:
int main(void) {
return 0;
}
Kao što je već rečeno, funkcija main poziva se iz operacijskog sustava, u ovom
jednostavnom primjeru od operacijskog sustava ne prima ništa (void) i operacijskom
sustavu vraća status 0 naredbom return.
Naredba return može se naći bilo gdje unutar funkcije i rezultira trenutačnim povratkom
na razinu s koje je funkcija pozvana. U sljedećem primjeru:
int main(void) {
printf("Jedan");
return 0;
printf("Dva");
return 0;
}
u prozoru će se ispisati samo
Jedan
nakon čega slijedi povratak u operacijski sustav.
Jezik C osjetljiv je na razliku velikih i malih slova, pa su Main i main dvije potpuno različite
funkcije. Pokušaj prevođenja programa
int Main(void) {
return 0;
}
rezultira greškom jer u njemu nije definirana obvezna funkcija main.

Komentari
Komentari su dijelovi programa koji se ne prevode i koji služe isključivo u svrhu
dokumentacije i bolje čitljivosti programa.
C poznaje dva načina označavanja komentara. Komentar koji započinje s dvije kose crte
// traje do kraja retka u kojemu je započet, dok komentar započet kombinacijom znakova
/* traje do završetka označenog kombinacijom znakova */.
/* Ovo je komentar koji se može protezati u više
redaka. Dolje će se nastaviti primjer komentara koji traje
do kraja retka, ali koji zasigurno nitko nikad neće napisati jer
komentira očigledno */
i = 0; // ovom se naredbom varijabla i postavlja na nulu
Komentar je nemoguće ugnijezditi. U sljedećem primjeru javit će se sintaktička greška jer
prva kombinacija /* započinje komentar tako da je sljedeća kombinacija /* dio komentara
koji će prevodilac zanemariti, a */ završava s komentarom:
/* Komentar

59
Uvod u programiranje Osnove pisanja programa u jeziku C

/* Komentar u komentaru */
Nastavak vanjskog komentara koji prevodilac smatra kôdom
*/
Standard pisanja LLVM preferira kose crte, pa i kod komentara u više redaka. Integrirana
razvojna sučelja većinom imaju funkcionalnost koja će na početak svakog od mišem
označenih redaka staviti oznaku komentara ili će tu oznaku ukloniti
(comment/uncomment). Nadalje, ako su pri pisanju komentara poštivane određene
konvencije, tako pisani komentari mogu poslužiti za automatsku izgradnju programske
dokumentacije korištenjem vanjskih generatora, poput programa doxigen.

Varijable i konstante
Varijable su dijelovi memorije kojima su pridijeljena imena i koji se mogu promijeniti kao
cjelina te služe za pohranu podataka s kojima program radi. Ime se smije sastojati
isključivo od velikih i malih slova engleske abecede, znamenki i podvlake (_), a mora
započinjati slovom ili podvlakom.
Svaka varijabla mora se definirati prije korištenja naredbom koja zove definicija, a sastoji
se od naziva tipa podatka i popisa varijabli koje će pripadati tome tipu, primjerice
int main (void) {
int i, j, k;
float a, b, c; // float -> realna, necjelobrojna veličina

Definicijom varijable rezervira se za nju prostor u memoriji računala na nekoj slobodnoj
poziciji - adresi. Količina rezerviranog prostora ovisi o tipu varijable (poglavlje Tipovi
varijabli i konstanti). U ovom konkretnom slučaju56 varijable su veličine 4 bajta pa ih je
prevodilac postavio na sljedeće adrese57:
c b a k j i
… …
6422296 6422300 6422304 6422308 6422312 6422316

Noviji standardi C-a dozvoljavaju definiciju bilo gdje u programu. Doseg (vidljivost, scope) i
trajanje (lifetime) varijable ovise o mjestu definicije i, ako postoji, modifikatoru definicije,
što će biti objašnjeno kasnije. Za sada, za svaku varijablu koja je definirana bez
modifikatora definicije (samo navođenjem naziva tipa i imena) unutar bloka omeđenog s
{ i } može se ustvrditi da je vidljiva od trenutka definicije do kraja bloka unutar kojeg je
definirana. Takvim varijablama početna vrijednost nije postavljena, pa se moraju na neki
način inicijalizirati prije nego što se njihova vrijednost počne koristiti.
Rezultat izvršavanja programa
#include <stdio.h>
int main(void) {
int a;
printf("%d", a);
return 0;

56U općenitom slučaju varijable mogu u memoriji biti smještene i drugačije, ne nužno jedna iza druge,
ovisno o prevodiocu i tipu varijabli
57Prostor za varijable smješten je na stogu koji raste s desna na lijevo, pa je prva definirana varijabla
smještena na najvišoj adresi

60
Uvod u programiranje Osnove pisanja programa u jeziku C

}
izgleda ovako (ili slično jer nije moguće predvidjeti koja će se vrijednost stvarno ispisati):
4194432
jer je u varijabli a neka kombinacija bitova koja se u memoriji računala na poziciji
dodijeljenoj toj varijabli nalazi od prije.
Varijabla se može inicijalizirati u definiciji tako da se iza imena varijable napiše znak = te
vrijednost, npr.
int a = 0;
Poželjno je varijabli dati ime koje će što bolje oslikati njenu semantiku. Ako varijabla treba
sadržavati, primjerice, prosječnu vrijednost nekih drugih veličina, mnogo je jasnije
varijablu deklarirati kao
float prosjek;
nego
float p;
Pri imenovanju varijable treba uzeti u obzir da je ime osjetljivo na razliku velikih i malih
slova. Suma i suma su dvije različite varijable, ali nije dobra praksa u istom programu imati
ih obje.
Prevodilac koristi samo 31 vodeći znak imena. Ako se imena dviju varijabli razlikuju tek
na 32. znaku ili kasnije, za prevodilac je to ista varijabla.
Preporuča se da ime varijable započne malim slovom58. Ako se u imenu varijable nalazi
više riječi, može ih se razdvojiti znakom _ ili, bolje, tako da druga i svaka sljedeća riječ
počinje velikim slovom, npr.
float prosjecnaVrijednost;
Ova konvencija naziva se Camel case jer podsjeća na grbu deve.
Konstante su u programskom jeziku C nepromjenjive veličine, određenog tipa, o čemu će
kasnije biti više rečeno. U sljedećem primjeru varijabli i pridjeljuje se vrijednost
numeričke konstante 0:
i = 0;

Ključne riječi
Programski jezik C poznaje određeni skup ključnih riječi, poput naziva tipova podataka,
naziva naredbi za kontrolu programskog slijeda, modifikatora definicija itd.
Zaključno sa standardom ISO C11, ključne riječi su:

auto do goto return


break double if short
case else inline signed
char enum int sizeof
const extern long static
continue float register struct
default for restrict switch

58Ova konvencija olakšat će prijelaz na objektno programiranje u kojemu konvencionalno velikim slovom
obično započinju imena razreda (classes), a znakom _ započinju privatne varijable

61
Uvod u programiranje Osnove pisanja programa u jeziku C

typedef volatile _Imaginary _Generic


union while _Alignas _Noreturn
unsigned _Bool _Alignof _Static_assert
void _Complex _Atomic _Thread_local

Treba primijetiti da neke novijim standardima uvedene ključne riječi započinju


podvlakom i velikim slovom, što u programskom jeziku C nije uobičajeno59.
Ključne riječi se ne smiju koristiti za imena varijabli i funkcija.

Koraci prevođenja
Naš prvi program u C-u bio je preveden elementarno, pozivom programa gcc, koji je, za
nas nevidljivo, obavio četiri osnovna koraka prevođenja:
1. pretprocesiranje
Program zvan pretprocesor, u gcc distribuciji nazvan cpp, obavlja transformaciju
izvornog programa pisanog u C razrješavanjem tzv. naredbi pretprocesora (u
sljedećem poglavlju). Rezultat pretprocesiranja također je C kôd
2. prevođenje u asembler
Prevodilac, program gcc, obavlja prevođenje C kôda u simbolički kôd čije
instrukcije u načelu odgovaraju instrukcijama procesora – asemblerski kôd
3. prevođenje u objektni kod
Asembler, u gcc distribuciji nazvan as, prevodi asemblerski kôd u binarne
instrukcije (objektni kôd), koje, međutim, ne mogu biti poslane na izvršavanje jer
u njima nedostaje poveznica prema varijablama i funkcijama koje su definirane u
drugim programskim modulima našega programa i standardnim programskim
bibliotekama
4. povezivanje
Povezivač (linker), u gcc distribuciji nazvan ld, obavlja povezivanje više modula
našega programa međusobno i s programskim bibliotekama stvarajući tako
izvršni kôd koji može biti izravno proslijeđen na izvršavanje.

Stvarne korake obavljene tijekom prevođenja otkriva opcija -v (verbose, opširan,


govorljiv)
gcc -v program.c -o program.exe
Cijeli proces može se ilustrirati sljedećim dijagramom toka:

59Ovo je uvedeno zbog kompatibilnosti s postojećim kôdom u kojemu su programeri sami, zahvaljujući
postojanju naredbe typedef, dodavali tipove podatka poput bool i complex. Za nove programe, u sistemskim
datotekama zaglavlja (complex.h i stdbool.h) nalaze se naredbe #define zahvaljući kojima je moguće koristiti
prirodniji oblik ključnih riječi.

62
Uvod u programiranje Osnove pisanja programa u jeziku C

Izvorni kôd
program.c

Pretprocesiranje
cpp program.c program.i

Transformirani izvorni kôd


program.i

Prevođenje u asemblerski kôd


gcc -S program.i

Asemblerski kôd
program.s

Prevođenje u objektni kôd


as program.s -o program.o

Objektni kôd
program.o biblioteka1

Povezivanje biblioteka2
ld program.o -o program.exe biblioteke

Izvršni kôd …
program.exe

Slika 16. Dijagram toka prevođenja izvornog kôda u izvršni kôd

63
Uvod u programiranje Naredbe pretprocesora

Naredbe pretprocesora
Prvi korak u prevođenju izvornog kôda pisanog u C-u naziva se pretprocesiranje.
Pretprocesor cpp identificira naredbe koje započinju znakom # i, ovisno o vrsti naredbe,
poduzima odgovarajuću akciju.

Naredba #include
Naredbom #include u izvorni kôd programa uključuje se sadržaj datoteke koja je u toj
naredbi navedena. Takvim se datotekama obično pridjeljuje nastavak imena .h (headers,
zaglavlja), iako se uključiti može i datoteka proizvoljnog imena.
Ime datoteke piše se u dvostrukim navodnicima ako se datoteka nalazi u mapi zajedno s
programom koji je uključuje ili ako je zadana cijela putanja do datoteke:
Datoteka iz mape u kojoj se nalazi program:
#include "program.h"
Datoteka zadana apsolutnom putanjom:
#include "/home/user1/programi/program.h"
Datoteka zadana relativnom putanjom (u podmapi zaglavlja mape u kojoj se nalazi
program)
#include "zaglavlja/program.h"
Napomena:
Na operacijskom sustavu Windows je uobičajeno hijerarhijske razine mapa razdvajati
obrnutom kosom crtom \ (backslash) i, u slučaju apsolutne putanje, na početku putanje
staviti oznaku logičkog tvrdog diska na kojemu se datoteka nalazi, primjerice
C:\UPRO\zaglavlja\program.h
Naredba #include, međutim, prepoznat će ovu putanju napisanu na bilo koji od sljedećih
načina
C:\UPRO\zaglavlja\program.h
C:\\UPRO\\zaglavlja\\program.h
C:/UPRO/zaglavlja/program.h
/UPRO/zaglavlja/program.h // pod uvjetom da je aktivan disk C:
Ime datoteke omeđeno znakovima < i > upućuje pretprocesor da datoteku traži na
zajedničkoj lokaciji za sve programe. Na operacijskom sustavu Windows bit će to
podmapa include mape u kojemu je instaliran sustav gcc, na Linuxu mapa /usr/include. U
toj se mapi nalazi većina standardnih datoteka zaglavlja koje će se po potrebi uključiti u
program, kao što je to datoteka <stdio.h> koja sadrži prototipove funkcija za ulaz i izlaz i
koju je zbog toga gotovo uvijek nužno uključiti.
#include <stdio.h>
Primjer:
Ako program.c sadrži
int main(void) {
#include "program.h"
return 0;
}

64
Uvod u programiranje Naredbe pretprocesora

a program.h sadrži
int j, i = 10;
j = i * 2;
naredba
cpp program.c program.i
rezultirat će sljedećim sadržajem datoteke program.i
int main(void) {
int j, i = 10;
j = i * 2;
return 0;
}

Naredba #define
Naredba #define definira leksičke cjeline 60 koje pretprocesor zamjenjuje alternativnim
tekstom, npr.
#define PI 3.1415926f
uzrokovat će da se svi nizovi znakova PI koje pretprocesor identificira kao samostalne
leksičke cjeline zamijene alternativnim nizom znakova 3.1415926f
#include <stdio.h>
#define PI 3.1415926f
int main(void) {
float xPI = PI;
printf("xPI = %f, PI = %f", xPI, PI);
return 0;
}
rezultirat će kôdom
sadržaj datoteke stdio.h
int main(void) {
float xPI = 3.1415926f;
printf("xPI = %f, PI = %f", xPI, 3.1415926f);
return 0;
}
%f je konverzijska specifikacija za ispis realnih brojeva koja u najjednostavnijem obliku
ispisuje realni broj s točno 6 znamenki iza decimalne točke, pa će izlaz programa izgledati
ovako
xPI = 3.141593, PI = 3.141593
Obično se naredbom #define definiraju simboličke konstante (konstante prikazane
simbolom umjesto konstantnom vrijednošću), ali se njome mogu zamijeniti i drugi
dijelovi programa, poput imena varijabli ili funkcija.
Također, naredbom #define mogu se definirati makro naredbe (objašnjenje će uslijediti
kasnije) kao i zastavice61 koje ispituje pretprocesor (u sljedećem poglavlju).

60 Ove leksičke cjeline zovu se još i identifikatorima i imaju strukturu jednaku imenima varijabli
61Zastavica (flag) je u računarstvu veličina slična logičkoj: može imati vrijednost postavljena, koja odgovara
logičkoj vrijednosti istina, ili nije postavljena, koja odgovara logičkoj vrijednosti laž.

65
Uvod u programiranje Naredbe pretprocesora

Uvjetno prevođenje
Naredbama #if, #elif, #else, #endif, #ifdef, #ifndef može se utjecati na odabir programskih
linija koje će se uzeti u obzir pri prevođenju.
#ifdef ispituje postojanje zastavice koja je postavljena naredbom #define.
Program:
#include <stdio.h>
#define OPSIRNO
int main(void) {
#ifdef OPSIRNO
printf("Dobar dan od programa pisanog u jeziku C!");
#else
printf("Dobar dan!");
#endif
return 0;
}
pretprocesor će pretvoriti u
sadržaj datoteke stdio.h
int main(void) {
printf("Dobar dan od programa pisanog u jeziku C!");
return 0;
}
Ako zastavica nije postavljena, rezultat će biti
sadržaj datoteke stdio.h
int main(void) {
printf("Dobar dan!");
return 0;
}
Zastavica se može postaviti i opcijom -D kod poziva prevodioca tako da će gornji program
napisan bez naredbe
#define OPSIRNO
ali preveden s
gcc -D OPSIRNO program.c -o program.exe
biti pretprocesorom pretvoren u isti kôd kao da ta naredba postoji.
#ifndef ispituje je li istina da zastavica ne postoji.
Naredba #if očekuje neki logički izraz, kao u sljedećem primjeru u kojem se količinom
informacija koja se ispisuje u prozor upravlja ovisno o vrijednosti simboličke konstante
RAZINA.

#define RAZINA 2

#if RAZINA == 2
printf("m iznosi %d, n iznosi %d, iznosi %d", m, n, r);
#elif RAZINA == 1
printf("m=%d, n=%d, r=%d", m, n, r);
#else
printf("r=%d", r);
#endif

66
Uvod u programiranje Naredbe pretprocesora


Vrijednost simboličke konstante može se postaviti i opcijom prevodioca -D:
gcc -D RAZINA=2 program.c -o program.exe
#if može ispitati i je li neka zastavica definirana operatorom defined

#if defined(OPSIRNO)

#endif

67
Uvod u programiranje Tipovi varijabli i konstanti

Tipovi varijabli i konstanti


Svaka varijabla koja će se u programu koristiti mora biti prije toga definirana, a
neizostavni dio definicije varijable je navođenje njenoga tipa. Tip varijable određuje
koliko će se bajtova u memoriji računala utrošiti za pohranu vrijednosti varijable, te na
koji će se način vrijednost varijable binarno prikazati.
Slijedi popis osnovnih tipova varijabli u programskom jeziku C, zauzeće memorije za
pojedinačnu varijablu i dozvoljeni raspon vrijednosti. Ovdje navedene veličine vrijede za
prevodilac gcc i arhitekturu procesora x86_64. Na drugim platformama mogu biti
drugačije.
Varijable cjelobrojnog tipa binarno se pohranjuju potencijama broja 2, a raspon
vrijednosti opisan je simboličkim konstantama definiranim u <limits.h>. Cjelobrojne
varijable mogu sadržavati predznak, a mogu biti i tipa unsigned čime se bit inače
rezerviran za predznak koristi za pohranu odgovarajuće potencije broja 2, pa se tako
efektivno udvostručuje najveći broj koji se u varijablu može pohraniti.
Tip Zauzeće Raspon u gcc Simboličke konstante
_Bool 1 bajt [0, 1]
char 1 bajt [-128, 127] CHAR_MIN,
CHAR_MAX
unsigned char 1 bajt [0, 255] UCHAR_MAX
short int 2 bajta [-32,768, 32,767] SHRT_MIN,
SHRT_MAX
unsigned 2 bajta [0, 65,535] USHRT_MAX
short int
int 4 bajta [-2,147,483,648, INT_MIN, INT_MAX
2,147,483,647]
unsigned int 4 bajta [0, 4,294,967,295] UINT_MAX
long int 4 bajta [-2,147,483,648, LONG_MIN,
2,147,483,647] LONG_MAX
unsigned long 4 bajta [0, 4,294,967,295] ULONG_MAX
int
long long int 8 bajtova [-9,223,372,036,854,775,808, LLONG_MIN,
9,223,372,036,854,775,807] LLONG_MAX
unsigned long 8 bajtova [0, ULLONG_MAX
long int 18,446,744,073,709,551,615]

Realne veličine binarno se pohranjuju po standardu IEEE 754, a raspon vrijednosti opisan
je simboličkim konstantama definiranim u <float.h>. Postoje po dvije konstante koje
definiraju najmanju vrijednost za svaki tip, prva koja daje minimalnu vrijednost
denormaliziranog broja (npr. FLT_TRUE_MIN), a druga koja daje minimalnu vrijednost
normaliziranog broja (npr. FLT_MIN).
Tip Zauzeće Raspon u gcc Simboličke konstante
float 4 bajta ±(1.4 · 10-45, 3.4 · 1038) FLT_TRUE_MIN, FLT_MIN,
FLT_MAX
double 8 bajtova ± (4.9 · 10-324, 1.8 · DBL_TRUE_MIN, DBL_MIN,
10308) DBL_MAX

68
Uvod u programiranje Tipovi varijabli i konstanti

long double 12 ± (3.6 · 10-4951, 1.9· LDBL_TRUE_MIN,


bajtova62 104932) LDBL_MIN, LDBL_MAX
float 8 bajtova kao float
_Complex
double 16 kao double
_Complex bajtova
long double 24 bajta kao long double
_Complex

Konstante su također određene tipom koji određuje utrošak memorije i način binarnog
prikaza konstante. Tip je određen oblikom pisanja.
Tip Oblik Primjeri
int samo dekadske znamenke 123
samo oktalne znamenke i prefix 0 0123
samo heksadekadske znamenke i prefix 0x ili 0X 0xA0
samo binarne znamenke i prefix 0b63 0b1010101
vrijednost u jednostrukim navodnicima64 'A'
'0'
'\n'
long int kao int sa sufiksom l ili L 123L
long long int kao int sa sufiksom ll ili LL 123ll
unsigned int kao int sa sufiksom u ili U 123u
unsigned long kao long int sa sufiksom u ili U 123LU
int
unsigned long kao long long int sa sufiksom u ili U 123llu
long int
double znamenke i decimalna točka i/ili eksponencijalni 123.
prikaz 123E10
long double kao double sa sufiksom L 123.L
123E10L
Prije ključne riječi koja označava tip varijable može se navesti i jedan od modifikatora
const ili volatile. Modifikator const proglašava varijablu konstantnom pa će svaki pokušaj
promjene takve varijable rezultirati greškom kod prevođenja. Modifikator volatile
označava da se vrijednost varijable može promijeniti bez utjecaja kôda iz okoliša pa služi
kao upozorenje prevodiocu da takve varijable isključi iz optimizacije65.

62Iako se za long double rezervira 96 bitova, broj se prikazuje u produljenoj preciznosti koja koristi samo
80 bitova, od trećeg bajta na dalje. Funkcija scanf u MinGW inačici prevodioca gcc ne radi dobro s long double
brojevima jer interno koristi Microsoftovu biblioteku u kojoj long double ima istu preciznost kao double
63Binarne konstante su proširenje koje ne postoji u standardu, ali su izuzetno korisne kod programiranja
na razini ugradbenih sustava i mikrokontolera. gcc neće dozvoliti binarne konstante ako se program
prevodi s opcijom -pedantic-errors, dok će uz opciju -pedantic dojaviti upozorenje.
64 Riječ je samo o drukčijem zapisu odgovarajućeg ASCII kôda
65Varijabla se može promijeniti bez utjecaja kôda iz okoliša ako je riječ o registru preslikanom u memoriju
(memory mapped register) koji sadrži stanje ulaznih vrata (input ports) nekog mikrokontrolera, ili je to
globalna varijabla koju mijenja neka druga dretva ili servisna rutina prekida (interrupt service routine, ISR).
Optimiranje kôda bi u slučaju petlje koja stalno ispituje vrijednost takve varijable npr. do { … } while
(v == 0); izostavilo učitavanje varijable u registar procesora u svakom koraku petlje jer ne vidi potrebu
za time, pa se, iako se varijabla promijeni na jedan od spomenutih načina, petlja nikad neće završiti.

69
Uvod u programiranje Tipovi varijabli i konstanti

Naredba typedef
Naredbom typedef moguće je nekom poznatom tipu podatka dodijeliti novo ime, čime se
može postići veća semantička jasnoća programa. Ugrađeni tipovi podataka imaju
univerzalnu namjenu. Primjerice, float može biti bilo kakva realna vrijednost, pa se iz
definicije poput
float v1, v2, i1, i2;
malo toga o kasnijoj namjeni ovih varijabli može zaključiti.
Međutim, ako se naredbama
typedef float napon;
typedef float struja;
tipu podataka float dodijele još i dva nova imena, napon i struja, tada su sljedeće definicije
mnogo semantički jasnije.
napon v1, v2;
struja i1, i2;
Općeniti je oblik naredbe typedef
typedef poznati_tip novo_ime;
pri čemu je nekom poznatom tipu moguće dati neograničen broj novih imena i dalje
koristiti poznati_tip. Naredbe iz ovoga primjera ne isključuju da se u programu i dalje
koristi float.
Puna semantička jasnoća programa postići će se pametnim kombiniranjem korištenja
naredbi typedef i dodjeljivanja prikladnih imena varijablama. U prethodnom primjeru, u
nekom izrazu poput
v1 = 5;
kad definicija varijable v1 nije u vidokrugu, nije odmah jasno što to v1 stvarno sadrži.
Varijable u prethodnom primjeru moglo se nazvati napon1, napon2, struja1 i struja2, ali u
programu će Ohmov zakon ipak izgledati prirodnije u naredbi
i = u / r;
nego u naredbi
struja = napon / otpor;

Enumeracije
Često se u programiranju neki skupovi vrijednosti, kao što su skup boja, skup dana u
tjednu ili skup igraćih karata, kodiraju numeričkim vrijednostima. Tako bi se moglo uvesti
kodiranje kod kojeg 0 predstavlja crnu boju, 1 crvenu, 2 zelenu, 3 plavu itd., ili da 0
predstavlja ponedjeljak, 1 utorak itd. Međutim, čak će i autoru programa, ako je boja
mnogo, biti teško odmah vidjeti da se naredba
boja = 3;
odnosi na plavu boju ili da se
dan = 0;
odnosi na ponedjeljak.
Naredbom enum definiraju se simboličke konstante kojima prevodilac redom dodjeljuje
numeričke vrijednosti, primjerice
70
Uvod u programiranje Tipovi varijabli i konstanti

enum boje { crna, crvena, zelena, plava, zuta, siva };


enum dani { ponedjeljak, utorak, srijeda, cetvrtak, petak };
Ovim naredbama prevodilac redom simboličkim konstantama dodjeljuje cjelobrojne
vrijednosti krenuvši od nule. Posljedično, naredbe
boja = plava;
dan = ponedjeljak;
efektivno rade isto kao da se napisalo
boja = 3;
dan = 0;
ali su semantički znatno jasnije.
Na bilo kojem mjestu u definiciji enumeracije može se navesti cjelobrojna vrijednost koju
neka konstanta treba poprimiti, čime će sljedeća konstanta poprimiti za jedan veću
vrijednost, osim ako i za nju nije eksplicitno navedena vrijednost.
Tako će nakon naredbe
enum boje { crna, crvena, zelena=10, plava, zuta, siva };
crna imati vrijednost 0, crvena 1, zelena 10, plava 11, žuta 12 i siva 13.
Moguće je definirati varijablu enumeracijskog tipa, primjerice
enum boje b;
ali će, iako je definicija semantički dobra, prevodilac bespogovorno prevesti i naredbu
b = 50;
pridijelivši tako varijabli b vrijednost 50 koja je izvan skupa boja.
Konstante i varijable enumeracijskog tipa u suštini su tipa int, pa u memoriji računala
zauzimaju 4 bajta.

71
Uvod u programiranje Operatori i izrazi

Operatori i izrazi
Operatori programskog jezika C obavljaju određene operacije nad vrijednostima svojih
operanada tvoreći izraze. Izraz je bilo koja (ispravna) kombinacija operatora i operanada.
Rezultat izraza može se spremiti u neku od varijabli primjenom operatora pridruživanja
p = a * b;
ili proslijediti funkciji kao argument:
printf("%d", a * b);
Operatore dijelimo na unarne, binarne i ternarne.
Unarni operatori djeluju nad jednim operandom, npr. operator - u izrazu -a. Unarni
operatori većinom se pišu u prefiksnoj notaciji (prije operanda), ali postoje i operatori
koji se mogu pisati i u postfiksnoj notaciji (iza operanda).
Binarni operatori djeluju nad dva operanda, npr. + u izrazu a + b i uvijek se pišu u infiksnoj
notaciji (između dva operanda).
Ternarni operatori koriste tri operanda.

Operator pridruživanja
Operator pridruživanja sprema vrijednost izraza napisanog s desne strane toga operatora
na adresu navedenu s lijeve strane toga operatora. Za sada će ta adresa biti određena
imenom varijable66, npr:
v = 0;
sprema vrijednost 0 u memoriju na adresu varijable v. Drugim riječima, postavlja
vrijednost varijable v na 0.
Često se za adresu koja se nalazi s lijeve strane operatora pridruživanja koristi termin l-
value (left value, location). L-value mora određivati adresu na koju se podatak pohranjuje.
Za vrijednost s desne strane koristi se termin r-value (right value). R-value može biti bilo
koja vrijednost (varijabla, konstanta, izraz).
Školski primjer za ilustraciju operatora pridruživanja je zamjena vrijednosti dviju
varijabli. Pretpostavimo da je u varijabli x pohranjena vrijednost 3 a u varijabli y
vrijednost 5. Na prvi pogled čini se da bi se taj problem mogao riješiti sa sljedeće dvije
naredbe:
x = y;
y = x;
Međutim, zapisivanjem vrijednosti varijabli nakon izvođenja svake od naredbi lako se
može vidjeti da je vrijednost od x naredbom x = y nepovratno uništena:
x y
#include <stdio.h> - -
int main(void) { - -
int x, y; ? ?
x = 3; 3 ?
y = 5; 3 5
printf("x = %d, y = %d", x, y); 3 5

66 O korištenju pokazivača u kontekstu pridruživanja bit će riječi kasnije

72
Uvod u programiranje Operatori i izrazi

x = y; 5 5
y = x; 5 5
printf("x = %d, y = %d", x, y); 5 5
} - -
Jedino ispravno rješenje je uvođenje pomoćne varijable u koju će se privremeno pohraniti
vrijednost jedne od varijabli:
x y p
#include <stdio.h> - - -
int main(void) { - - -
int x, y, p; ? ? ?
x = 3; 3 ? ?
y = 5; 3 5 ?
printf("x = %d, y = %d", x, y); 3 5 ?
p = x; 3 5 3
x = y; 5 5 3
y = p; 5 3 3
printf("x = %d, y = %d", x, y); 5 3 3
} - -
Program 3. ZamjenaVarijabli.c

Operator pridruživanja moguće je ulančavati. Sljedeći primjer


a = b = c;
treba čitati na sljedeći način: vrijednost varijable c pohranjuje se u varijablu b. Rezultat
toga izraza je pohranjena vrijednost koja se dalje pohranjuje u a.

Aritmetički operatori, eksplicitna i implicitna pretvorba


tipa
Aritmetički operatori djeluju nad brojevima i rezultat je broj.
Binarni operatori djeluju nad dva operanda
Operator Značenje Primjer Rezultat
a = 10; b = 3;
+ zbrajanje … a + b … 13
a = 10; b = 3;
- oduzimanje … a – b … 7
a = 10; b = 3;
* množenje … a * b … 30
a = 10; b = 3; 3 ili 3.33333, ovisno o
/ dijeljenje
… a / b … tipu varijabli a i b

% ostatak dijeljenja a = 10; b = 3; 1 ili greška, ako a i b


cijelih brojeva …a % b… nisu cjelobrojni,

Ovdje je nužno primijetiti da tip rezultata izraza ovisi o tipu operanada. U pravilu, tip
rezultata bit će jednak tipu onog operanda koji dozvoljava veću maksimalnu vrijednost,
tako da će se onaj drugi operand prvo pretvoriti u vrijednost odgovarajućeg tipa. Pri tome

73
Uvod u programiranje Operatori i izrazi

treba primijetiti da se u programskom jeziku C niti jedna računska operacija ne obavlja


nad podacima manjim od int.
Implicitna pretvorba tipa obavlja po se sljedećem algoritmu
• Ako je jedan od operanada long double, drugi se pretvara u long double, rezultat je
long double
• inače ako je jedan operand double, drugi se pretvara u double, rezultat je double
• inače ako je jedan operand float, drugi se pretvara u float, rezultat je float
• inače ako je jedan operand unsigned long long int, drugi se pretvara u unsigned long
long int, rezultat je unsigned long long int
• inače ako je jedan operand long long int, drugi se pretvara u long long int, rezultat
je long long int
• inače ako je jedan operand unsigned long int, drugi se pretvara u unsigned long int,
rezultat je unsigned long int
• inače ako je jedan operand long int, drugi se pretvara u long int, rezultat je long int
• inače ako je jedan operand unsigned int, drugi se pretvara u unsigned int, rezultat
je unsigned int
• inače ako bilo koji operand nije int, pretvara se u int, rezultat je int
• inače (svi operandi su int), nema pretvorbe, rezultat je int
Valja primijetiti da će tip rezultata izraza uvijek poštovati gornja pravila, bez obzira na to
što će se tim rezultatom kasnije dogoditi. U sljedećem primjeru
int i;
i = 5. / 2;
rezultat izraza dijeljenja je 2.5, ali se kod primjene operatora pridruživanja obavlja
implicitna pretvorba tipa po Prokrustovom 67 pravilu: duljina i oblik rezultata
prilagođavaju se varijabli s lijeve strane operatora pridruživanja. U ovom slučaju odsjeći
će se decimalni dio rezultata a cjelobrojni dio konvertirat će se iz prikaza po standardu
IEEE 754 u potencije broja 2.
Kod primjene operatora dijeljenja, ako su operandi cjelobrojnog tipa i rezultat je
cjelobrojnog tipa, pa će dijeljenje 10 i 3 kao rezultat dati 3. U sljedećem primjeru, rezultat
pohranjen u varijablu r bit će također 3.0, jer se rezultat izračunava kao int, a tek kasnije
implicitno pretvara u realni broj.
float r;
r = 10 / 3;
Očigledno, da bi se kao rezultat dobio realni broj, barem jedan od operanada mora biti
realni broj. Ako je jedan od operanada konstanta, najlakše ga je napisati kao realnu
konstantu. U sljedećem primjeru rezultat pohranjen u r iznosi 3.333333:
float r;
r = 10 / 3.; // ili 10. / 3 ili 10. / 3.
Ako su oba operanda varijable, treba osigurati da jedan operand u izrazu bude realan.
Trivijalno, to se može napraviti množenjem s realnom konstantom 1. kao u primjeru

67Prokrust (grč. Προκρούστης, Prokroustes), lik iz grčke mitologije. U svome prenoćištu imao je krevet, a
svakoga je namjernika "prilagodio" duljini toga kreveta: niske je rastegnuo, a visokima odrezao noge.

74
Uvod u programiranje Operatori i izrazi

int a = 10, b = 3;
float r;
r = 1. * a / b;
ali korektnije je upotrijebiti operator eksplicitne pretvorbe tipa (operator cast).
Operator eksplicitne pretvorbe tipa je unarni operator u prefiksnom obliku koji se u
općenitom primjeru sastoji od, u zagradama napisanog, naziva tipa u koji treba pretvoriti
vrijednost operanda:
(tip)
U prethodnom primjeru, realno dijeljenje osigurat će se ovako:
int a = 10, b = 3;
float r;
r = (float)a / b;
Dakle, uzet će se vrijednost varijable a pretvorena u broj tipa float, čime će se osigurati da
se i vrijednost drugog operanda pretvori u float i da rezultat bude float.
Rezultat može premašiti maksimalnu vrijednost za na gornji način definirani tip rezultata.
Posljedica može biti neočekivana vrijednost rezultata kao u sljedećem primjeru:
int v = 2147483647; // INT_MAX
v = v + 1;
printf("%d\n", v);
Ispisat će se -2147483648 jer se najvećem mogućem podatku tipa int, binarno
spremljenom kao
01111111111111111111111111111111
dodaje jedinica pa je rezultat
10000000000000000000000000000000
dakle negativan broj -2147483648, što se lako može provjeriti tehnikom dvojnog
komplementa.
Više kao kuriozitet, ovdje možemo spomenuti i kako se uz primjenu aritmetičkih
operatora može riješiti problem zamjene vrijednosti dviju varijabli:
x y
int x = 3, y = 5 3 5
y = y + x 3 8
x = y - x 5 8
y = y - x 5 3
Međutim, postoje tri vrlo čvrsta razloga zbog kojih se to tako ne radi:
• Iako se čini da se ovako uštedjelo 4 bajta memorije jer je izbjegnuta rezervacija za
pomoćnu varijablu, osim za 3 pridruživanja, koliko ih ima u rješenju s pomoćnom
varijablom, negdje je potrebno spremiti i izvršni kôd jednog zbrajanja i dva
oduzimanja, tako da je, računajući ukupan prostor potreban za pohranu instrukcija
i podataka, ovaj program veći.
• Dodatno na pridruživanje, moraju se obaviti zbrajanje i oduzimanje, što povećava
ukupno trajanje postupka.
• Program je znatno teže čitljiv.

75
Uvod u programiranje Operatori i izrazi

Numeričke greške
U realnoj aritmetici česta su pojava numeričke greške. Numeričke greške nastaju zbog
ograničene preciznosti registra u kojem se realna varijabla pohranjuje.
Sljedeći odsječak (pod uvjetom da je u prevođenje uključena datoteka <float.h>)
float f;
f = FLT_MAX;
printf("%f\n", f);
f = f + 1;
printf("%f\n", f);
f = f * 10;
printf("%f\n", f);
ispisuje
340282346638528859811704183484516925440.000000
340282346638528859811704183484516925440.000000
inf
Nakon dodavanja jedinice varijabla f ostaje nepromijenjena, jer ta jedinica nakon
normalizacije ne stane u mantisu. Nakon množenja s 10 karakteristika postaje veća od
maksimalne dozvoljene pa se postavlja na vrijednost koja po konvenciji predstavlja ∞
(infinity).
Slična anomalija može se primijetiti i na mnogo manjem broju:
float f = 6000000.0f;
float malif = 0.25f;
f = f + malif;
f = f + malif;
f = f + malif;
f = f + malif;
printf("%f\n", f);
f = f + 1;
printf("%f\n", f);
Ispisat će se
6000000.000000
6000001.000000
jer se pri svakom dodavanju 0.25 sadržaj varijable f zaokružuje na vrijednost najbližu
vrijednosti 60000000.25, a koju je moguće prikazati u standardnoj točnosti. Ta vrijednost
je 60000000.0, stoga niti jedna od četiri naredbe uvećavanja za 0.25 ne uspijeva
promijeniti vrijednost varijable. Dodavanjem jedinice dobije se vrijednost 60000001.0, a
tu vrijednost moguće je u standardnoj točnosti prikazati bez greške.
U sljedećem primjeru:
float f = 0.1;
printf("%f\n", f);
sve je naizgled u redu, jer program ispisuje
0.100000
Međutim, ako u ispisu modifikacijom konverzijske specifikacije zatražimo ispis na 10
decimalnih mjesta, umjesto pretpostavljenih 6, naredbom
printf("%.10f\n", f);

76
Uvod u programiranje Operatori i izrazi

ispisat će se
0.1000000015
U varijabli je, dakle pohranjena nešto veća vrijednost od 0.1, što je posljedica činjenice da
je dekadskih 0.1 beskonačni periodički binarni razlomak koji se u slučaju varijable tipa
float pohranjuje u 24 binarne znamenke mantise, pri čemu se posljednja binarna
znamenka zaokružuje tako da ukupna greška bude manja. U ovom slučaju došlo je do
zaokruživanja na više pa je rezultat pohranjen u f veći od očekivanja.
U primjeru
printf("%.10f\n", 0.9);
ispisat će se
0.8999999762
jer je došlo do zaokruživanja na niže.
Ponekad, posebno nakon oduzimanja sličnih brojeva, takva greška može postati
dramatična:
float x = 1.0000001;
x = 1 / (x - 1);
printf("%f\n", x);
Ispisuje se
8388608.000000
što je greška od više od 16% u odnosu na pravi rezultat, koji bi trebao biti točno
10,000,000. U apsolutnom iznosu greška iznosi preko milijun i šesto tisuća.
Problem je u tome što izvorna vrijednost u x zbog ograničene mantise i zaokruživanja
zapravo iznosi približno 1.0000001192, pa oduzimanjem jedinice rezultat ni približno
nije očekivanih 0.0000001.
Greška se, naravno, značajno može smanjiti korištenjem preciznijeg tipa (double, long
double). Definicijom varijable x kao double rezultat će biti 9999999.994161 što je
približno, ali još uvijek ne potpuno točno.
Osim u ekstremnim slučajevima poput ovdje opisanog, numeričke greške same po sebi ne
bi trebale predstavljati veliki problem, jer su gotovo uvijek manje od točnosti stvarnih
izmjerenih veličina. Težinu, duljinu, napon i slične fizikalne veličine rijetko će biti moguće
izmjeriti na 7 decimalnih mjesta, kolika je približno preciznost varijable tipa float, a
posebno ne na približno 16 koliko je preciznost tipa double.
Jedina primjena u kojoj se naizgled pojavljuju realni brojevi, a u kojoj je potrebna
apsolutna točnost su financije. Međutim, ako se novčani iznosi interno pohranjuju i čuvaju
u lipama, a tek kod ispisa prikazuju kao kune, za tu se svrhu može koristiti cjelobrojni,
apsolutno točni podaci i aritmetika. Pri tome treba voditi računa o tome da, koristi li se
cjelobrojni tip veličine 4 bajta, a postoje i pozitivni i negativni novčani iznosi, najveći
dopušteni novčani iznos bit će tek malo veći od 20 milijuna kuna (dvije milijarde lipa).
Rješenje je, naravno, u korištenju "većeg" tipa, npr. long int ili long long int.
Kod financijskih podataka naročite dolaze do izražaja greške zaokruživanja.
Pretpostavimo da dio cijene nekog proizvoda sačinjava trgovačka marža od 5.13%. Već na
malom primjeru s dva artikla, zaokruživanjem nastaje razlika od 1 lipe:
Artikl Cijena Marža 5.13 %

77
Uvod u programiranje Operatori i izrazi

A 1234.56 kn 63.33 kn (točan iznos marže: 63.332928 kn)


B 8763.43 kn 449.56 kn (točan iznos marže: 449.563959 kn)
Suma 9997.99 kn 512.89 kn
Točan iznos ukupne marže nastao sumacijom nezaokruženih marži ili izračunavanjem
marže na razini sume (što je moguće samo ako se marže na razini artikala ne razlikuju)
jest 512.896887, što bi trebalo zaokružiti na 512.90.

Unarni operatori
Unarni operatori djeluju nad jednim operandom:
Operator Notacija Značenje Primjer Rezultat
v = 7;
+ prefiksna
… +v … 7
v = 7;
- prefiksna negacija
… -v … -7
uvećanje operanda za 1 prije v = 7; v = 8;
++ prefiksna
korištenja vrijednosti r = ++v; r = 8;
uvećanje operanda za 1 v = 7; v = 8;
++ postfiksna
nakon korištenja vrijednosti r = v++; r = 7;
umanjenje operanda za 1 v = 7; v = 6;
-- prefiksna
prije korištenja vrijednosti r = --v; r = 6;
umanjenje operanda za 1 v = 7; v = 6;
-- postfiksna
nakon korištenja vrijednosti r = v--; r = 7;
veličina operanda izražena u int i;
sizeof prefiksna
bajtovima r = sizeof(i); r = 4;
(tip) prefiksna pretvorba u tip …(int)1.5 … 1

Operatori uvećanja i umanjenja za 1


Operatori uvećanja i umanjenja za 1 uvedeni su u programski jezik C jer je primijećeno
da je brojanje unaprijed ili unazad jedna od najčešćih operacija u elektroničkom računalu,
pa je umjesto
i = i + 1;
uvećanje varijable i za 1 moguće napisati u kraćem obliku
++i;
U ovom je primjeru potpuno svejedno hoće li se operator napisati u postfiksnoj ili
prefiksnoj notaciji, jer se vrijednost varijable u toj naredbi više ne koristi. U primjerima u
kojima se rezultat primjene operatora koristi u istom izrazu, prefiksni odnosno postfiksni
oblik određuje hoće li se u izrazu koristiti stara ili nova vrijednost operanda. U prefiksnom
obliku u izrazu se odmah koristi uvećana odnosno umanjena vrijednost operanda, dok se
u postfiksnom obliku koristi stara vrijednost operanda.
Ovo se može ilustrirati sljedećim naredbama i njihovim ekvivalentnim kôdom:

78
Uvod u programiranje Operatori i izrazi

r = ++i; → i = i + 1; r = --i; → i = i - 1;
r = i; r = i;
r = i++; → r = i; r = i--; → r = i;
i = i + 1; i = i - 1;

Operator sizeof
Operator sizeof vraća veličinu operanda izraženu u bajtovima. Operand može biti naziv
tipa podatka, npr. int, naziv konkretne varijable, ali i bilo koji izraz, u kojem slučaju sizeof
vraća veličinu rezultata:
char a=1, b=2;
double d;
printf("%d %d %d %d %d\n",
sizeof(int), sizeof(d),
sizeof (a), sizeof(a+b), sizeof('A'));
Ispisat će se:
4 8 1 4 4
Podaci tipa char se prema pravilima o implicitnoj konverziji prije zbrajanja pretvaraju u
podatke tipa int, pa veličina rezultata iznosi 4 bajta. Konstanta 'A', iako je napisana
slovom, zapravo je konstanta tipa int vrijednosti 65.
Bajt u modernim računalima sadrži 8 bita, ali tijekom povijesti pojavljivale su se i i druge
veličine bajta. Za konkretni prevodilac veličina bajta izražena u bitovima zapisana je u
simboličkoj konstanti CHAR_BIT definiranoj u <limits.h>. Program preveden gcc-om
#include <limits.h>

printf("%d\n", CHAR_BIT);
ispisat će 8.

Prioritet i asocijativnost aritmetičkih operatora


Aritmetički operatori programskog jezika C slijede logiku prioriteta poznatu iz
matematike: prvo se obavljaju unarne operacije, zatim operacije višeg prioriteta
(množenje i dijeljenje), a tek tada operacije nižeg prioriteta (zbrajanje i oduzimanje). Pri
tome je redoslijed operacija jednakog prioriteta određen asocijativnošću68 odgovarajućeg
operatora, koja je u slučaju binarnih operatora uvijek s lijeva na desno.
U sljedećem primjeru
r = -5 * 3 + 6 / 4;
u prvom koraku evaluiraju se izrazi -5 * 3 i 6 / 4 te se u sljedećem koraku izračunava
r = -15 + 1;
Operator ostatka dijeljenja cijelih brojeva ima jednak prioritet kao i dijeljenje pa se u
primjeru
r = 10 % 3 * 4;

68Asocijativnost operatora određuje kojim će se redom obavljati operacije istog prioriteta u odsutnosti
zagrada

79
Uvod u programiranje Operatori i izrazi

prvo izračuna ostatak (% ima isti prioritet kao i * odnosno / ali je ovdje pozicioniran lijevo
od *) pa se zatim ostatak množi s 4. Rezultat ovoga izraza je 4.
Redoslijed izračunavanja uvijek se može promijeniti primjenom zagrada, pa se sljedeći
izraz
r = -5 * (3 + 6) / 4;
evaluira kao
r = -5 * 9 / 4;
Sada se -5 množi s 9 što daje -45 nakon čega slijedi cjelobrojno dijeljenje s 4. Rezultat je -
11;
Redoslijed prioriteta i asocijativnost do sada nabrojanih operatora izgleda ovako, od
najvišeg prema najnižem prioritetu:
Prioritet Operatori Asocijativnost
1 ++ i -- u postfiksnom obliku s lijeva na desno
2 ++ i -- u prefiksnom obliku s desna na lijevo
unarni +, unarni -, (tip), sizeof
3 *, /, % s lijeva na desno
4 +, - s lijeva na desno
5 = s desna na lijevo

Napomena:
Iako bi se iz prethodne tablice moglo bi se zaključiti da je moguće napisati nešto poput
-- --x što bi se evaluiralo kao -- (--x), operatori uvećanja za jedan i umanjenja za jedan
moraju biti napisani neposredno uz ime varijable kojoj će promijeniti vrijednost. Nije
moguće zato napisati ni nešto poput -- -x, ali je moguće napisati, na primjer - --x:
operatori su jednakog prioriteta, a njihova je asocijativnost s desna na lijevo, pa će se prvo
smanjiti vrijednost od x i tako smanjena pomnožiti s -1 prije korištenja u ostatku izraza ili
kod pridruživanja.

Relacijski operatori
Relacijski operatori uspoređuju međusobno vrijednosti dvaju izraza i tvore logičke
sudove čija vrijednost može biti istina ili laž.
Operator Značenje Primjer Rezultat
> veće 3 > 5 laž
>= veće ili jednako 3 >= 3 istina
< manje 3 < 5 istina
<= manje ili jednako 3 <= 5 istina
== jednako 5 == 5 istina
!= različito 5 != 5 laž
Istina se u C-u prikazuje numeričkom vrijednošću 1, a laž numeričkom vrijednošću 0 što
se može provjeriti sljedećim primjerom:
80
Uvod u programiranje Operatori i izrazi

printf("%d %d\n", 1 > 2, 1 <= 2);


Ispisuje se
0 1
Konverzijska specifikacija kojom se ispisuju logičke vrijednosti ne postoji pa se za tu
namjenu koristi %d.
Rezultat primjene relacijskog operatora može se pohraniti u varijablu bilo kojeg tipa, ali
je semantički najkorektnije pohraniti ga u varijablu tipa _Bool.
_Bool sud;
sud = 1 > 2;
Već prije je bilo rečeno da je tip _Bool u novijem standardu C-a dobio ime koje počinje
podvlakom da se izbjegne konflikt s naslijeđenim (legacy) programima u kojemu su
programeri radi semantičke jasnoće uvodili vlastiti tip bool. Za nove programe svakako je
poželjno uključiti u prevođenje datoteku <stdbool.h> u kojoj osim naredbe
#define bool _Bool
koja omogućava prirodniju definiciju logičkih varijabli postoje i naredbe
#define true 1
#define false 0
koje također doprinose semantičkoj jasnoći programa. Naime, čitajući u nekom većem
programu naredbu
x = 0;
a da pri tome nije u vidokrugu i deklaracija varijable x, nije jasno radi li se o postavljanju
neke numeričke vrijednosti na nulu ili o postavljanju logičke vrijednosti na laž. U
sljedećem primjeru to je očigledno:
x = false;
Treba primijetiti da C razlikuje operator pridruživanja = od operatora ispitivanja
jednakosti == što ponekad može dovesti do semantičke greške. U sljedećem primjeru
int v = 6;
if (v = 3) {
printf("v je jednako 3");
} else {
printf("v je različito od 3");
}
ispisat će se v je jednako 3 jer će se prvo obaviti pridruživanje vrijednosti 3 varijabli v pa
potom rezultat ispitati je li rezultat te operacije istina ili laž. Rezultat primjene operatora
pridruživanja je vrijednost pohranjena u l-value, u ovom slučaju 3, što je odgovara logičkoj
vrijednosti istina.

Logički operatori
Logički operatori djeluju nad logičkim sudovima tvoreći složenije logičke izraze i
implementacija su operatora logičke ili Booleove algebre.
Logička algebra poznaje sljedeće glavne operacije: konjunkciju (˄, i, and), disjunkciju (˅,
ili, or), negaciju (¬, ne, not) te ekskluzivnu disjunkciju (⊕ ili ⊻, exili, xor)
Rezultati ovih operacija nad logičkim sudovima prikazani su sljedećim tablicama:

81
Uvod u programiranje Operatori i izrazi

Konjunkcija Disjunkcija
x y x˄y x y x˅y
0 0 0 0 0 0
1 0 0 1 0 1
0 1 0 0 1 1
1 1 1 1 1 1

Negacija Ekskluzivna disjunkcija


x ¬x x y x⊕y
0 1 0 0 0
1 0 1 0 1
0 1 1
1 1 0

Korisne ekvivalencije u izrazima s jednim logičkim sudom:


¬ (¬x) = x (dvostruka negacija)
x˄x=x (idempotentnost konjunkcije
x˄1=x
x˄0=0
x ˄ ¬x = 0
x˅x=x (idempotentnost disjunkcije)
x˅1=1
x˅0=x
x ˅ ¬x = 1
Korisne ekvivalencije u izrazima s dva logička suda:
x˅y=y˅x (komutativnost disjunkcije)
x˄y=y˄x (komutativnost konjunkcije)
¬(x ˄ y) = ¬y ˅ ¬x (De Morganov69 zakon)
¬(x ˅ y) = ¬y ˄ ¬x (De Morganov zakon)
x ˄ (x ˅ y) = x (apsorpcija)
x ˅ (x ˄ y) = x (apsorpcija)
Korisne ekvivalencije u izrazima s tri logička suda:
x ˄ (y ˄ z) = (x ˄ y) ˄ z asocijativnost konjunkcije
x ˅ (y ˅ z) = (x ˅ y) ˅ z asocijativnost disjunkcije
x ˄ (y ˅ z) = (x ˄ y) ˅ (x ˄ z) distributivnost konjunkcije
x ˅ (y ˄ z) = (x ˅ y) ˄ (x ˅ z) distributivnost disjunkcije

69 Augustus De Morgan (27.6. 1806., Madurai – 18.3.1871, London), britanski matematičar i logičar.

82
Uvod u programiranje Operatori i izrazi

Programski jezik C poznaje samo logičke operatore && (konjunkcija), || (disjunkcija) i !


(negacija):
Operator Značenje Primjer Rezultat
&& konjunkcija, ˄ (1 > 2) && (3 < 4) 0
|| disjunkcija, ˅ (1 > 2) || (3 < 4) 1
! negacija, ¬ !(1 > 2) 1
Ekskluzivna disjunkcija x ⊕ y može se ostvariti kao
(x && !y) || (!x && y)
U logičkoj vrijednosti postavljen je samo jedan, najdesniji bit, pa je ekskluzivnu disjunkciju
moguće ostvariti i primjenom operatora ekskluzivne disjunkcije nad bitovima ^ o čemu
će biti riječi kasnije.
Pokušajmo sada napisati izraz koji će rezultirati istinom ako je vrijednost varijable x
unutar zatvorenog intervala [a, b]. x, dakle, mora biti veći ili jednak a i istovremeno manji
ili jednak b.
Doslovno prepisivanje ove tvrdnje u obliku
x >= a && <= b
je sintaktička greška. && i <= su binarni operatori i moraju se napisati između valjanih
izraza, a izraz x >= a && s lijeve strane operatora <= to nije, kao ni <= b s desne strane
operatora &&.
Ovaj izraz ispravno napisan glasi
x >= a && x <= b
Do suprotnog izraza, izraza koji će rezultirati istinom ako x nije unutar zatvorenog
intervala [a, b], može se doći izricanjem tvrdnje da x u tome slučaju mora biti manji od a
ili mora biti veći od b:
x < a || x > b
Do istoga se izraza može doći i negacijom izvorne tvrdnje da je x unutar zatvorenog
intervala [a, b]:
!( x >= a && x <= b).
Doslovnom primjenom de Morganovog zakona taj je izraz ekvivalentan izrazu
!(x >= a) || !(x <= b)
Suprotno od >= je <, a suprotno od <= je >, pa se izraz dalje može razviti u već poznati
x < a || x > b
Važno je primijetiti da se pri evaluaciji logičkih izraza obavlja tzv. skraćena evaluacija
(short circuit evaluation) koja se prekida kad se sa sigurnošću može zaključiti da je logički
izraz istinit ili lažan.
U sljedećem primjeru
a = 3;
b = 4;
rez = a > 3 && b > 3;
evaluacija će se prekinuti čim se ustanovi da je a > 3 neistina, jer se desnim dijelom
konjunkcije, bez obzira na rezultat, ne može postići istinitost cijeloga izraza.

83
Uvod u programiranje Operatori i izrazi

Slično,
rez = a == 3 || b == 4;
prestaje se evaluirati čim se ustanovi da je a == 3, jer se desnim dijelom disjunkcije, bez
obzira na rezultat, nikako ne može postići da cijeli izraz bude lažan.

Prioritet i asocijativnost logičkih i relacijskih operatora


Relacijski operatori imaju niži prioritet od aritmetičkih. Drugim riječima, da bi se neke
vrijednosti mogle međusobno usporediti, moraju prethodno biti izračunate. Izraz
3 + 4 > 2 -1
ekvivalentan je izrazu
(3 + 4) > (2 – 1)
i rezultira vrijednošću 1.
Logički operatori djeluju nad logičkim sudovima, pa je logično da te sudove treba
prethodno treba formirati:
1 > 2 && 4 < 5
ekvivalentno je izrazu
(1 > 2) && (4 < 5)
što iznosi 0.
Relacijski operatori, dakle, po prioritetu su viši od logičkih, pri čemu vrijedi napomenuti
da su >, >=, <, <= višega prioriteta od == i !=.
Logički operatori, međutim, nisu jednakog prioriteta. Negacija, kao unarni operator, ima
asocijativnost s desna na lijevo i najviši prioritet (u rangu unarnih + i -), dok konjunkcija
ima veći prioritet nego disjunkcija, kao što množenje ima veći prioritet od zbrajanja. Svi
binarni relacijski operatori imaju asocijativnost s lijeva na desno.
Naravno, čitkosti programa neće doprinijeti oslanjanje na poznavanje prioriteta. Izraz
1 + 3 == 3 < 4 && 5 * 6 – 2 > 3 || 7 + 3 < 8
ima jasan i dobro definirani redoslijed evalucije
(((1 + 3) == (3 < 4)) && ((5 * 6 – 2) > 3))) || ((7 + 3) < 8)
ali sigurno ga je bolje i čitkije upravo tako napisati.
Prethodni izraz zaslužuje još jedan komentar. Logički sud uvijek poprima numeričke
vrijednosti 0 ili 1 koje se kasnije mogu koristiti u aritmetičkim ili relacijskim izrazima.
3 < 4 iznosi 1, što se operatorom ispitivanja jednakosti uspoređuje s 1 + 3, s rezultatom 0.
Moguće je zato napisati i nešto poput ovoga:
int i;
i = (1 > 2) + (3 > 2) + (4 > 3);
što će u varijablu i pohraniti vrijednost 2.
Za usporedbu, isti izraz napisan bez zagrada
i = 1 > 2 + 3 > 2 + 4 > 3;
rezultira s 0 jer se, zbog većeg prioriteta aritmetičkih operatora i asocijativnosti s lijeva
na desno operatora > izračunava kao
i = ((1 > (2 + 3)) > (2 + 4)) > 3;

84
Uvod u programiranje Operatori i izrazi

Operatori nad bitovima


Ponekad je u programiranju potreban pristup do svakog pojedinog bita nekog registra.
Najmanja varijabla koja se u C-u može definirati (char) zauzet će cijeli bajt, što je
nepotrebno razbacivanje memorijom ako je, primjerice, potrebno pamtiti samo podatke
koji mogu iznositi samo 0 ili 1. Pristup bitovima potreban je ne samo radi štednje: u
programiranju mikrokontrolera status 8 ulaznih ili izlaznih nožica često će biti pohranjen
u jednom registru veličine jednoga bajta, pa će za detekciju stanja na nožici ili postavljanje
naponske razine pojedine nožice također biti potrebno pristupati pojedinačnim bitovima.
Operatori nad bitovima su sljedeći:
Operator Značenje
<< Posmak ulijevo
>> Posmak udesno
& i (and) nad bitovima
| ili (or) nad bitovima
^ ekskluzivni ili (xor) nad bitovima
~ negacija (not) nad bitovima
Operatori posmaka posmiču neku vrijednost za zadani broj bitova lijevo ili desno. Ova
operacija suštinski odgovara množenju ili dijeljenju s odgovarajućom potencijom broja 2:
U sljedećem primjeru pomaknut će bitovi pohranjene u varijabli c za 2 bita ulijevo, što
odgovara množenju s 22 (1002):
c (binarno) c (dekadski)
unsigned char c = 8; 00001000 8
c = c << 2; 00100000 64
Ovdje treba naglasiti posmak, kao i svi operatori nad bitovima, djeluje nad kopijom
vrijednosti varijable. Sljedeća naredba
b = c >> 3;
neće promijeniti vrijednost varijable c, nego će u varijablu b upisati vrijednost od c
posmaknutu desno za 3 mjesta (podijeljenu s 23 = 8).
Bitovi posmakom mogu napustiti registar i tako biti izgubljeni.
c (binarno) c (dekadski)
unsigned char c = 10; 00001010 10
c = c >> 2; 0000001010 2
c = c << 8; 1000000000 0
Kod posmaka u desno varijabli s predznakom, ako je bit predznaka postavljen na 0,
registar se s lijeve strane puni nulama. Ako je bit predznaka postavljen na 1 (broj je
negativan), registar se s lijeve strane puni jedinicama:
a (binarno) b (binarno)
char a = -1, b = 127; 11111111 01111111
a = a >> 5; 11111111 01111111

85
Uvod u programiranje Operatori i izrazi

b = b >> 2; 11111111 00011111


Kod varijabli bez predznaka (unsigned), registar se s lijeve strane uvijek puni nulama:
c (binarno)
unsigned char c = 255; 11111111
c = c >> 3; 00011111
Operatori nad bitovima i, ili i exili obavljaju odgovarajuću logičku operaciju nad
korespondentnim bitovima svojih operanada. Negacija pretvara jedinice u nule i obratno:
11001100 11001100 11001100 ~ 11001100
& 00001111 | 00001111 ^ 00001111 00110011
00001100 11001111 11000011
Za pristup bitu ili kombinaciji bita obično će biti potrebno napraviti neku kombinaciju
posmaka i logičkih operatora nad bitovima.
Vrijednost bita varijable v koja odgovara n-toj potenciji broja 2 (n-tog bita s desna, počevši
od 0) može se izdvojiti sljedećom naredbom
r = (v >> n) & 1;
Za n=3 to izgleda ovako:
xxxxxxxx >> 3
000xxxxx
& 00000001
0000000x
n-ti bit zdesna može se postaviti na 1 sljedećom naredbom
v = v | (1 << n);
Za n=3:
00000001 << 3
00001000
| xxxxxxxx
xxxx1xxx
n-ti bit zdesna može se postaviti na 0 sljedećom naredbom
v = v & ~(1 << n);
Za n=3:
00000001 << 3
00001000 ~
11110111
& xxxxxxxx
xxxx0xxx
Postavljanje n-tog bita na neku zadanu vrijednost b može se napraviti tako da se n-ti bit
postavi na 0 na prethodno opisani način i zatim obavi logički ili s vrijednošću b
posmaknutom za n:
v = (v & ~(1 << n)) | (b << n);
Za n=3, nastavljeno na prethodni primjer:
86
Uvod u programiranje Operatori i izrazi

0000000b << 3
0000b000 ~
| xxxx0xxx
xxxxbxxx
Operatorima nad bitovima moguće je jednostavno pretvoriti znamenku zapisanu ASCII
kôdom u broj i obratno. ASCII kôd znamenke 0 je 48, znamenke 1 49 itd. Općenito vrijedi
izraz
znamenka = broj + 48;
odnosno
broj = znamenka – 48;
Očigledno, znamenke imaju postavljene bitove koji odgovaraju vrijednostima 32 i 16
(32+16=48), dok brojevi to nemaju. Brojke će biti pohranjene u obliku 0000bbbb, a
znamenke u obliku 0011bbbb, gdje je bbbb binarna vrijednost brojke. Bitove uz 32 i 16
može se, dakle, ukloniti naredbom
broj = znamenka & 0xf; // ili 0b00001111
a postaviti naredbom
znamenka = broj | 0x30; // ili 0b00110000

Skraćeno pridruživanje
U programiranju se često vrijednost varijable mijenja tako da se primjenom nekog
operatora nad starom vrijednošću varijable izračuna njena nova vrijednost, kao u
primjeru brojanja
v = v + 1;
ili
v = v – 1;
Za ove je posebne slučajeve već uveden skraćeni način pisanja operatorima ++ i --.
U općenitom slučaju varijabli je moguće dodati bilo koju vrijednost
suma = suma + placa;
pomnožiti je s nekom vrijednošću
potencija = potencija * 2;
i sl.
Zato se bilo koji izraz s binarnim operatorom oblika
varijabla = varijabla operator operand;
može napisati u obliku
varijabla operator= operand;
pa se prethodni primjeri mogu napisati kao
i += 1;
i -= 1;
suma += placa;
potencija *= potencija;
Slijedi popis svih operatora skraćenog pridruživanja :

87
Uvod u programiranje Operatori i izrazi

+=, -=, *=, /=, %=, <<=, >>=, &=, ^=, |=

Ternarni operator
Izraz s ternarnim operatorom piše se u obliku
logički_izraz ? alternativa_1 : alternativa_2
a treba ga čitati na sljedeći način: ako je logički_izraz istinit, rezultat cijelog izraza je ono
što se izračuna u alternativi_1. Inače, rezultat je vrijednost alternative_2.
U jednostavnijem primjeru, u varijablu r pohranit će se veća od vrijednosti a i b:
r = a > b ? a : b;
U nešto složenijem primjeru, u r će se pohraniti najveća od vrijednosti a, b ili c:
r = a > b ? (a > c ? a : c) : (b > c ? b : c);

Odjeljivanje naredbi zarezom


Nekoliko naredbi odijeljenih zarezima tretiraju se kao cjelina sa zajedničkim rezutatom.
Naredbe se obavljaju s lijeva na desno. U sljedećem primjeru
r = (a = 1, b = 2*a, c = 3*b);
varijabli a pridružuje se vrijednost 1, zatim se varijabli b pridružuje udvostručena
vrijednost od a, a varijabli c utrostručena vrijednost od b. Rezultat svega je vrijednost
posljednjeg izraza u nizu, vrijednost pohranjena u c, pa će ispis
printf("%d %d %d %d\n", r, a, b, c);
rezultirati s
6 1 2 6
U vrlo sličnom primjeru
r = a = 1, b = 2*a, c = 3*b;
printf("%d %d %d %d\n", r, a, b, c);
ispisat će se
1 1 2 6
jer se ovdje jedinica pohranjuje u a, rezultat toga je nova vrijednost a koja se pohranjuje
u r. Daljna evalucija ista je kao u prethodnom primjeru. Rezultat svega je vrijednost na
kraju pohranjena u c, ali se taj rezultat nigdje ne koristi.

88
Uvod u programiranje Kontrola programskog toka

Kontrola programskog toka


Pretpostavljeni slijed izvođenja naredbi, naredba za naredbom, moći će zadovoljiti samo
u najjednostavnijim poslovima. U gotovo svakom programu naći će se naredbe koji će taj
pretpostavljeni slijed pod nekim uvjetima promijeniti.

Naredba if
Naredba if, nazvana još i selekcijom jer odabire koje će se naredbe sljedeće obaviti,
spomenuta je već u uvodnom poglavlju. Može se naći u jednom od tri moguća oblika.
Jednostrana selekcija odlučuje o tome hoće li naredba koja slijedi iza naredbe if biti
obavljena ili neće. Općenito se može napisati kao
if (izraz) naredba_1;
nastavak;

Ako se evaluacijom logičkog izraza izraz dobije logička vrijednost istina, obavit će se
naredba_1 i nakon toga nastaviti s izvođenjem nastavka. Ako se evaluacijom logičkog
izraza dobije vrijednost laž, naredba_1 neće se obaviti već će program nastaviti od
nastavka.
Ovo se može ilustrirati sljedećim dijagramom toka:

istina
izraz

laž naredba_1

nastavak

Slika 17. Jednostrana selekcija

U praksi će se naredba_1 uglavnom sastojati od više pojedinačnih naredbi, pa je poželjno


uvijek koristiti oblik
if (izraz) {
naredba _1;
}
nastavak;

čime se izbjegava jedna od češćih logičkih pogrešaka u programiranju koja nastaje kad se
proširuje funkcionalnost naredbe if kao u sljedećem primjeru u kojem se
if (a > 0) b = 0; ili if (a > 0)
b = 0;

89
Uvod u programiranje Kontrola programskog toka

pokušava prepraviti na
if (a > 0) b = 0; c = 0; ili if (a > 0)
b = 0;
c = 0;
if djeluje samo nad sljedećom naredbom, a ona je u ovom slučaju samo b = 0; Naredba c =
0 obavit će se bez obzira na to kakva je vrijednost varijable a.
Dvostrana selekcija grana program u dvije grane, jednu koja će se obaviti ako je izraz
istinit, a drugu ako nije:
if (izraz) {
naredba_1;
} else {
naredba_2;
}
nastavak;
kao u sljedećem dijagramu toka

istina laž
izraz

naredba_1 naredba_2

nastavak

Slika 18. Dvostrana selekcija

Višestrana selekcija ima sljedeći oblik


if (izraz_1) {
naredba_1;
} else if (izraz_2) {
naredba_2;
} else if (…

} else if (izraz_n) {
naredba_n+1;
} else {
naredba_n+1;
}
nastavak;

Ovdje će se redom analizirati izrazi sve dok se ne naiđe prvi koji daje rezultat istina, nakon
čega će se obaviti pripadna naredba i zatim nastaviti s nastavkom. Ako ni jedan od izraza
90
Uvod u programiranje Kontrola programskog toka

nije istinit, obavit će se naredba_n+1 i zatim nastavak kako je ilustrirano sljedećim


dijagramom:

laž
izraz_1

istina
laž
izraz_2
naredba_1
istina

naredba_2

laž
izraz_n
istina

naredba_n

naredba_n+1

nastavak

Slika 19. Višestrana selekcija

Naredba else s pripadajućom naredbom_n+1 ne mora postojati u višestranoj selekciji.


Primjer:
Vrijednosti varijabli a, b i c treba pohraniti u x, y i z tako da u x bude najmanja, a u z najveća
vrijednost
Rješenje:
#include <stdio.h>
int main (void) {
int a, b, c, x, y, z;
scanf("%d %d %d", &a, &b, &c);
if (a < b) { // poredati prva dva broja

91
Uvod u programiranje Kontrola programskog toka

x = a;
y = b;
} else {
x = b;
y = a;
}
#if 1 // varijanta 1
if (c < x) { // c je najmanji pa treba posmaknuti x i y
z = y;
y = x;
x = c;
} else if (c < y) { // c je srednji pa treba posmaknuti y
z = y;
y = c;
} else { // c je najveći pa treba doći na kraj
z = c;
}
#else //varijanta 2
z = c; // pretpostavimo da je c najveći
if (c < y) { // c je manji od y pa y treba posmaknuti
z = y;
y = c;
}
if (c < x) { // c je manji i od x pa i x treba posmaknuti
y = x;
x = c;
}
#endif
printf("%d %d %d\n", x, y, z);
return 0;
}
Program 4. Sort3Vrijednosti.c

U prethodnom programu ponuđene su dvije varijante rješenja. Druga varijanta rješenja


predstavlja doslovnu implementaciju algoritma koji se zove sort umetanjem (insertion
sort), ali u toj varijanti treba obaviti najviše elementarnih naredbi. U prvoj varijanti, ako
je c najmanji od 3 broja, obavljaju se 3 elementarne instrukcije, dok se u drugoj varijanti
obavi njih 5. Broj uspoređivanja jednak je u oba slučaja.
Još složeniji primjer ispitivanja selekcijom ilustrirat će na određivanju presjeka dva
intervala [a1, a2] i [b1, b2].
Najteži dio problema je identifikacija svih mogućih slučajeva, a ima ih ukupno 6.

92
Uvod u programiranje Kontrola programskog toka

1. 2. a1 a2
a1 a2
b1 b2 b1 b2

3. a a2 4. a1 a2
1

b1 b2 b1 b2

5. a a2 6. a1 a2
1

b1 b2 b1 b2

Slika 20. Mogući presjeci dvaju intervala

U slučaju 1. i 2. ne postoji presjek ta dva intervala. U slučaju 3, presjek je interval [b1, a2] i
tako dalje.
Rješenje:
#include <stdio.h>
int main (void) {
int a1, a2, b1, b2;
scanf("%d %d %d %d", &a1, &a2, &b1, &b2);
if (b1 > a2 || a1 > b2 ) { // slučajevi 1. i 2.
printf("Intervali nemaju presjeka\n");
} else if (b1 >= a1) { // slučajevi 3. ili 5., ali koji?
if (b2 <= a2) { // slučaj 5.
printf("Presjek je [%d, %d]\n", b1, b2);
} else { // Slučaj 3
printf("Presjek je [%d, %d]\n", b1, a2);
}
} else { // slučajevi 4 ili 6, ali koji?
if (a2 <= b2) { // slučaj 6.
printf("Presjek je [%d, %d]\n", a1, a2);
} else { // Slučaj 4
printf("Presjek je [%d, %d]\n", a1, b2);
}
}
return 0;
}
Program 5. Intervali.c

U ovom primjeru uveli smo ugniježdenu selekciju: if započet u nekoj od grana drugoga if-
a. U takvim primjerima uvijek je dobro vitičastim zagradama konzistentno ograničavati
blok naredbi koji se izvodi u slučaju istinitosti ili neistinitosti logičkog izraza. U
suprotnom, lako se može dogoditi semantička greška poput sljedeće
int a = 6;
if (a > 5)
if (a > 10)
printf("a je veće od 10");
else
printf("a je manje ili jednako 5");

93
Uvod u programiranje Kontrola programskog toka

Program 6. KrivoNapisaniIf.c

Ovaj program ispisat će a je manje ili jednako 5 jer je else dio bližeg if-a, bez obzira na
ovako napisano uvlačenje.

Skretnica
Skretnica (switch) na prvi pogled sliči višestranoj selekciji:
switch (izraz) {
case vrijednost_1:
naredba_1;
case vrijednost_2;
naredba_2;
case …

case vrijednost_n;
naredba_n;
default:
naredba_n+1;
}
nastavak;
Postoji, međutim, bitna razlika. Cjelobrojni izraz napisan u zagradama uz ključnu riječ
switch uspoređuje se redom s vrijednostima napisanim uz case. Kad se naiđe na prvu
vrijednost jednaku izrazu, obavi se pripadna naredba i sve naredbe u svim case granama
nakon nje. Ako skretnica sadrži granu default, ta će se grana obaviti u svakom slučaju.
Skretnica se može ilustrirati sljedećim dijagramom toka u kojem je tekst vrijednost_i
skraćen na v_i:

94
Uvod u programiranje Kontrola programskog toka

laž
izraz==v_1

istina
laž
naredba_1 izraz==v_2

istina

naredba_2 …

laž
… izraz==v_n

istina

naredba_n

naredba_n+1

nastavak

Slika 21. Skretnica

Ovakva funkcionalnost omogućuje pisanje zajedničkog odsječka za više vrijednosti kao u


sljedećem primjeru u kojem je potrebno dojaviti odgovarajuću poruku ako je učitan
samoglasnik.
#include <stdio.h>
int main (void) {
char c;
scanf("%c", &c); // konverzijskom specifikacijom %c
// učitavaju se varijable tipa char
switch (c) {
case 'A': case 'E': case 'I': case 'O': case 'U':
printf("Učitan je samoglasnik\n");
}
return 0;
}
Program 7. SkretnicaSamoglasnici.c

95
Uvod u programiranje Kontrola programskog toka

Modifikacija ovog programa u kojemu bi se ispisivala i alternativna poruka ("Učitan je


suglasnik") na prvi pogled izgledala bi ovako:

switch (c) {
case 'A': case 'E': case 'I': case 'O': case 'U':
printf("Učitan je samoglasnik\n");
default:
printf("Učitan je suglasnik\n");
}
return 0;

Ovaj program, doista, ispisuje odgovarajuću poruku ako se učita suglasnik, ali nakon
učitavanja samoglasnika ispisat će se obje poruke. To se može izbjeći dodavanjem
naredbe break; nakon koje će se nastaviti izvoditi prva naredba koja slijedi iza završetka
bloka započetog sa switch, u ovom slučaju return:

switch (c) {
case 'A': case 'E': case 'I': case 'O': case 'U':
printf("Učitan je samoglasnik\n");
break;
default:
printf("Učitan je suglasnik\n");
}
return 0;

Slijedi općeniti oblik skretnice koji po svojoj funkcionalnosti odgovara višestranoj
selekciji, s ekvivalentnim kôdom uz korištenje if-a.
switch (izraz) {case if (izraz == vrijednost_1) {
vrijednost_1: naredba_1;
naredba_1;
break; } else if (izraz == vrijednost_2) {
case vrijednost_2: naredba_2;
naredba_2;
break; } else if (izraz == …
case … …
… } else if (izraz == vrijednost_n) {
case vrijednost_n: naredba_n;
naredba_n;
break; } else {
default: naredba_n+1;
naredba_n+1;
break; }
} nastavak;
nastavak;
break u grani default nije potreban, ali se često stavlja radi uniformnosti.
Odluka o izboru selekcije ili skretnice više je semantičko pitanje, iako je skretnica
uglavnom učinkovitije implementirana na razini izvršnog kôda. Naravno, pod uvjetom da
je problem uopće moguće riješiti skretnicom s obzirom da je ograničena na ispitivanje
uvijek iste vrijednosti, što kod selekcije nije slučaj.

96
Uvod u programiranje Kontrola programskog toka

U primjeru poput sljedećeg, u kojem se realizira primitivni kalkulator, skretnica je


prirodni odabir. S tipkovnice treba pročitati trojku operand operator operand i ispisati
odgovarajući rezultat ili poruku ako je utipkan nepoznati operator:
#include <stdio.h>
int main (void) {
float a, b;
char op;
scanf("%f %c %f", &a, &op, &b);
switch (op) {
case '+':
printf("%f\n", a + b);
break;
case '-':
printf("%f\n", a - b);
break;
case '/':
printf("%f\n", a / b);
break;
case '*':
printf("%f\n", a * b);
break;
default:
printf("Nepoznati operator %c\n", op);
}
return 0;
}
Program 8. Kalkulator.c

Programske petlje
Primitivni kalkulator iz prethodnog poglavlja treba pokrenuti iz operacijskog sustava za
svaki novi izračun. To je, naravno, nepraktično ako se želi obaviti više uzastopnih
operacija. Zato su u svim programskim jezicima ugrađene naredbe koje određeni
programski odsječak pod određenim uvjetima obavljaju više puta – programske petlje.
U teoriji programiranja, razlikujemo tri vrste programskih petlji:
• petlja s ispitivanjem uvjeta ponavljanja na početku petlje
• petlja s ispitivanjem uvjeta ponavljanja na kraju petlje
• petlja s poznatim brojem ponavljanja

Petlja s ispitivanjem uvjeta ponavljanja na početku petlje – naredba while


Petlja s ispitivanjem uvjeta ponavljanja na početku petlje u pseudokodu se obično piše
kao
dok je (izraz)
naredbe
nastavak
Ispituje se je li izraz istinit, pa ako jest, obave se naredbe koje zovemo tijelom petlje,
nakon čega se ponovno ispituje istinitost izraza.
Ako izraz nije istinit, nastavlja se izvođenje s nastavkom programa.
Ovaj pseudokod doslovno se preslikava u C uz korištenje naredbe while koja se u
općenitom slučaju piše kao
while (izraz) {

97
Uvod u programiranje Kontrola programskog toka

naredbe;
}
nastavak;
Dijagram toka koji ilustrira petlju while izgleda ovako:

laž
izraz

istina

naredbe

nastavak

Slika 22. Petlja while

Petlja kod koje je izraz uvijek istinit naziva se beskonačnom petljom. Beskonačna petlja
može se, primjerice, se ugraditi u primitivni kalkulator iz prethodnog poglavlja:
#include <stdio.h>
int main (void) {
float a, b;
char op;
while (1) {
scanf("%f %c %f", &a, &op, &b);
switch (op) {
case '+':
printf("%f\n", a + b);
break;
case '-':
printf("%f\n", a - b);
break;
case '/':
printf("%f\n", a / b);
break;
case '*':
printf("%f\n", a * b);
break;
default:
printf("Nepoznati operator %c\n", op);
}
}
return 0;
}
Program 9. KalkulatorPetlja.c

Izraz u zagradama uz naredbu while uvijek je istina tako da ovaj program nikad ne
završava, što je za kalkulator normalno ponašanje. Takav program treba prekinuti tako
da se zatvori prozor u kojemu se izvodi ili da se na tipkovnici utipka kombinacija Ctrl-C
(stisne se tipka Ctrl i, dok je pritisnuta, stisne još i C). Beskonačne petlje uobičajene su u

98
Uvod u programiranje Kontrola programskog toka

mnogim procesima, poput npr. kontrole i automatizacije u kojima program ispituje stanje
senzora, ovisno o stanju poduzima neku akciju i tako zauvijek, ili barem do gašenja
sustava.
Većina petlji, ipak, obavljat će se ograničeni broj puta. Pokušajmo, primjerice, izračunati
sumu znamenki nekog cijelog dekadskog broja. Broj znamenki nije unaprijed poznat, i taj
program mora ponavljati posao sumacije sve dok sve znamenke nisu obrađene. Desna
znamenka bilo kojeg broja može se dobiti kao cjelobrojni ostatak dijeljenja s 10, a
cjelobrojno dijeljenje s 10 iz broja uklanja zadnju znamenku. Postupak se ponavlja sve dok
vrijednost broja ne padne na nulu:
#include <stdio.h>
int main (void) {
int a, suma = 0;
scanf("%d", &a);
while (a != 0) {
suma = suma + a % 10;
a = a / 10;
}
printf("%d\n", suma);
return 0;
}
Program 10. SumaZnamenki.c

Ovdje je važno napomenuti da varijabla u koju će se obavljati sumacija treba obvezno biti
inicijalizirana na neutralni element za zbrajanje (nulu).
Stanje varijabli u trenutku ispitivanja uvjeta ponavljanja može se pratiti tablično. Uz
pretpostavku da je s tipkovnice uneseno 1234, program će ispitati uvjet ponavljanja 5
puta:
a a % 10 suma
1234 4 0
123 3 4
12 2 7
1 1 9
0 0 10

Petlja s ispitivanjem uvjeta ponavljanja na kraju petlje – naredba do


Ako je u prethodnom primjeru početna vrijednost varijable a bila 0, tijelo petlje neće se
obaviti ni jednom. Rezultat će, međutim, biti korektan jer je suma znamenki broja 0
jednaka inicijalnoj vrijednosti varijable suma koja iznosi 0.
Pokušajmo sada riješiti sljedeći problem: potrebno je sumirati brojeve unesene s
tipkovnice sve dok se ne unese 0.
Primjenom petlje s ispitivanjem uvjeta ponavljanja na početku potrebno je osigurati da se
obavi prvo učitavanje. Moglo bi se to napisati ovako:

int a, suma = 0;
scanf("%d", &a);
while (a != 0) {
suma = suma + a;
scanf("%d", &a);
}

99
Uvod u programiranje Kontrola programskog toka


Ovdje se, međutim, isti posao obavlja na dva mjesta što nikada nije preporučljivo. U
nekom scenariju u kojem je potrebno promijeniti tip varijable koja se učitava iz int u float,
konverzijsku specifikaciju %d trebat će promijeniti u %f dva puta, što se možda nećemo
sjetiti.
Alternativno, mogli bismo se poslužiti trikom i inicijalizirati varijablu a na vrijednost
različitu od nule tako da se osigura ulazak u petlju, ali treba paziti da ta vrijednost ne
uđe u sumaciju:

int a = 1, suma = 0;
while (a != 0) {
scanf("%d", &a);
suma = suma + a;
}

Korektnije je u ovakvom primjeru koristiti programsku petlju s ispitivanjem uvjeta
ponavljanja na kraju.
Takva petlja u pseudokodu se obično piše
radi
naredbe
dok ne bude (izraz)
nastavak
Istinitost logičkog izraza u takvoj petlji uzrokuje prekid petlje i nastavak rada od sljedeće
naredbe.
C, međutim, poznaje samo naredbu
do {
naredbe;
} while (izraz);
nastavak;
u kojoj izraz mora biti istinit kako bi naredbe u tijelu petlje ponovile. To se jednostavno
postiže negiranjem logičkog izraza iz pseudokoda. Dijagram toka koji ilustrira naredbu do
izgleda ovako:

naredbe

istina
izraz

laž

nastavak

Slika 23. Petlja do

100
Uvod u programiranje Kontrola programskog toka

Ovom petljom prethodni se primjer može elegantnije postaviti:



int a, suma = 0;
do {
scanf("%d", &a);
suma = suma + a;
} while (a != 0);

U ovom primjeru sumirala se i posljednja unesena vrijednost varijable a, nula, ali ona neće
promijeniti vrijednost sume.

Programska petlja s poznatim brojem ponavljanja


Često se u programima neki posao obavlja unaprijed poznati broj puta, primjerice ispis
svih brojeva od 1 do 10, traženje najvećeg u skupu brojeva poznate kardinalnosti itd.
U pseudokodu takva se petlja obično piše ovako:
za varijabla = početak do kraj s korakom korak
naredbe
nastavak
Nekoj zadanoj varijabli vrijednost se mijenja od početka do kraja. Ako korak promjene nije
zadan, pretpostavlja se da je 1. Takva se varijabla zove kontrolnom varijablom petlje. U
nekim jezicima promjena kontrolne varijable u tijelu petlje (osim implicitno, za zadani ili
pretpostavljeni korak) nije dozvoljena, što je semantički sasvim korektno. Sljedeći primjer
za i= 1 do 10
naredbe
ako je i = 7
i=9
sigurno se ne bi trebao realizirati petljom s poznatim brojem ponavljanja jer na prvi
pogled zavarava: prvom naredbom rečeno je da će se petlja obaviti upravo 10 puta, da
bismo kasnije, negdje u dubini programa, preskočili dva obavljanja.
U programskom jeziku C postoji naredba for, koja, striktno uzevši, nije petlja s poznatim
brojem ponavljanja, već samo elegantniji način pisanja petlje s ispitivanjem uvjeta
ponavljanja na početku.
Sintaksa naredbe je sljedeća:
for (naredba_1; izraz; naredba_2) {
naredbe;
}
nastavak;
Naredba_1 (koja se može sastojati od više izraza odvojenih zarezom) obavlja se samo
jednom, prije ulaska u petlju. Odmah nakon toga ispituje se vrijednost logičkog izraza koji
mora biti istinit kako bi se obavile naredbe. Nakon toga obavlja se naredba_2 (koja se može
sastojati od više izraza odvojenih zarezom) i nastavlja s ispitivanjem izraza. Ako izraz nije
istinit, nastavlja se s nastavkom.
Gornja je petlja, dakle, ekvivalentna sljedećoj petlji while:
naredba_1;
while (izraz) {

101
Uvod u programiranje Kontrola programskog toka

naredbe;
naredba_2;
}
nastavak;
što se može ilustrirati i sljedećim dijagramom toka:

naredba_1

laž
izraz

istina
a
naredbe

naredba_2

nastavak

Slika 24. Petlja for

Bilo koja od komponenti može se izostaviti, pa je tako sljedeća for naredba beskonačna
petlja koja ne radi ništa:
for (;;);
Nema naredbi inicijalizacije, nema izraza koji uvjetuje ponavljanje (pa se, pomalo
neočekivano, pretpostavlja da uvjet u za ponavljanje istinit), tijelo petlje je prazna
naredba, a nema niti naredbi koje treba obaviti iza tijela petlje prije sljedećeg ispitivanja
uvjeta ponavljanja.
U praksi, naravno, iz semantičkih razloga, for naredbu treba koristiti samo tamo gdje je
broj ponavljanja poznat, kao u sljedećem primjeru, u kojem se izračunava n! uzastopnim
množenjem primjenom izraza
𝑛! = ∏𝑛𝑖=1 𝑖
U inicijalizaciji for naredbe, uz postavljanje brojača i na početnu vrijednost, i varijabla f u
koju se kumulativno množi postavlja se na neutralni element za množenje (1):
#include <stdio.h>
int main(void) {
int f, i, n;
scanf("%d", &n);
for (f = 1, i = 1; i <= n; i++) {
f *= i;

102
Uvod u programiranje Kontrola programskog toka

}
printf("%d! = %d\n", n, f);
return 0;
}
Program 11. Faktorijeli.c

Treba primijetiti da ovaj program korektno radi i za 0! jer se petlja neće obaviti ni jednom
pa će u f ostati zapamćena početna vrijednost 1. Tehnički, za 1! obavlja se jedno
nepotrebno množenje, ali taj je program ipak semantički jasniji od programa u kojem bi
se u petlji varijabla i inicijalizirala na 2.
Ovdje moramo primijetiti da će se, zbog brzog rasta n!, vrlo brzo dogoditi preliv koji bi
mogao ostati neprimijećen. Rezultati koje će program ispisivati su:
5! = 12010
10! = 3628800
11! = 39916800
12! = 479001600
13! = 1932053504
Već 13! je izračunato pogrešno, jer je 479001600 * 13 = 6227020800.
Prelazak u domenu double dozvolit će računanje do 170!, ali s ograničenom preciznošću:
170! =
72574156153079940000000000000000000000000000000000000000000000000000
00000000000000000000000000000000000000000000000000000000000000000000
00000000000000000000000000000000000000000000000000000000000000000000
00000000000000000000000000000000000000000000000000000000000000000000
00000000000000000000000000000000000.000000
171! = 1.#INF00
Ovdje je smislenije rezultat ispisivati eksponencijalnim prikazom nego, kao gore, velikim
brojem nula. Za to je pogodna konverzijska specifikacija %g koja automatski prelazi na
eksponencijalni prikaz za velike ili male brojeve, a ne prikazuje decimale ako ih nema:

double f;

printf("%d! = %g\n", n, f);
Ispis je
170! = 7.25742e+306
171! = inf
S obzirom na preciznost tipa double od oko 16 decimalnih mjesta, tu sad ima smisla još
povećati broj decimala u ispisu s pretpostavljenih 6 na 16. I %f i %g konverzijske
specifikacije dozvoljavaju postavljanje preciznosti ispisa na n decimalnih mjesta u obliku
%.nf ili %.ng, s time da n kod specifikacije %.nf određuje broj decimala iza decimalne
točke, a kod specifikacije %.ng ukupan broj znamenki, uključujući decimalnu točku i cijeli
dio:

printf("%d! = %.16g\n", n, f);
Ispis je
170! = 7.25741561530799e+306
171! = inf

103
Uvod u programiranje Kontrola programskog toka

Još više faktorijela može se izračunati samo prelaskom u domenu long double brojeva uz
korištenje odgovarajuće konverzijske specifikacije %Lg, uz malo povećanje preciznosti
(16 na 19 decimala), ali ogromno povećanje raspona:

long double f;

printf("%d! = %.19Lg\n", n, f);
Ispis je
1754! = 1.97926189010501006e+4930
1755! = inf
U složenijem primjeru za for petlju učitavat će se cijeli brojevi i ispisivati binarni zapis
sadržaja učitanog broja sve dok se ne unese 0. Bitovi bi se mogli izdvajati i sukcesivnim
dijeljenjem s dva, ali zbog činjenice da se ostaci dijeljenja zapisuju s desna, te bi ostatke
prije ispisa trebalo negdje pohranjivati kako bi ih se kasnije ispisalo s lijeva. Zato će se u
ovom trenutku za izdvajanje bitova koristiti operatore nad bitovima. Sljedeći primjer
radit će na bilo kojem računalu jer se ne pouzdaje u činjenicu da je veličina podatka tipa
int 4 bajta niti da je bajt veličine 8 bita, nego veličinu podatka ispituje primjenom
operatora sizeof, a veličinu bajta čita iz simboličke konstante CHAR_BIT definirane u
<limits.h>. Bitovi se ispisuju od najsignifikantnijeg prema najmanje signifikantnom, a
nakon svakog bajta ispisuje se, radi preglednosti, jedna praznina70.
#include <stdio.h>
#include <limits.h>
int main(void) {
int a, i;
do {
scanf("%d", &a);
for (i = sizeof(a) * CHAR_BIT - 1; i >= 0; i--) {
printf("%d", (a >> i) & 1);
if (i % CHAR_BIT == 0) {
printf(" ");
}
}
printf("\n");
} while (a != 0);
return 0;
}
Program 12. Bitovi.c

Slijedi primjer izvođenja ovoga programa, s ulaznim podacima i ispisom rezultata:


10↵
00000000 00000000 00000000 00001010
32767↵
00000000 00000000 01111111 11111111
-1↵
11111111 11111111 11111111 11111111
0↵

70Ispis binarnog sadržaja realnog broja treba realizirati drukčije. Nad realnim varijablama nisu definirani
operatori posmaka, pa će realni broj trebati rastaviti na bajtove, kako je to kasnije pokazano u poglavlju
Pokazivači.

104
Uvod u programiranje Kontrola programskog toka

00000000 00000000 00000000 00000000

Naprednijim korištenjem mogućnosti C-a, naredbe


do {
scanf("%d", &a);

} while (a !=0);
mogle bi se zamijeniti sljedećom naredbom:
for (; scanf("%d", &a), a != 0;) {
Cijeli posao obavlja se u jednom retku: for nema dijela inicijalizacije, uvjet ispitivanja
sastoji se od dvije naredbe odvojene zarezom koje se izvode s lijeva na desno ali je rezultat
cijelog izraza jednak rezultatu desne naredbe, ne postoje ni naredbe koje se izvode nakon
što se obavi tijelo petlje. Za razliku od prethodnog programa, ovdje se neće ispisati
binarna vrijednost nule jer se izvođenje petlje prekida odmah čim se varijabla a
učitavanjem postavi na vrijednost nula.
U dijelu for naredbe koji obavlja inicijalizaciju prije ulaska u petlju moguće je načiniti i
definiciju varijabli. U sljedećem primjeru
for (int i = 0; i <= 100; i++) {

}
for (int i = 0; i <= 10; i++) {

}
naziv i nosit će dvije različite varijable vidljive samo unutar petlje unutar koje su
definirane.
Bilo koji pokušaj korištenja takve varijable izvan petlje rezultirat će greškom, što nije
slučaj ako je kontrolna varijabla petlje bila definirana prije.

Naredba break
Mogućnost prekida izvođenja petlje samo na početku ili na kraju ponekad zahtijeva
dupliciranje ili višak naredbi. Pokušajmo samo s petljom s ispitivanjem uvjeta na početku
riješiti problem u kojem se ispisuju apsolutne vrijednosti učitanih negativnih brojeva, a
program završava kad se unese nenegativan broj, pri čemu se njegova apsolutna
vrijednost ne ispisuje.
Rješenje s petljom s ispitivanjem uvjeta ponavljanja na početku zahtijeva da se prije
ulaska u petlju obavi prvo učitavanje, a svako sljedeće učitavanje ponovi u petlji:
int x;
scanf("%d", &x);
while (x < 0) {
printf("%d", -x);
scanf("%d", &x);
}
Ovdje se ponavlja isti posao (učitavanje), što nije dobra praksa.
Alternativno, poslije učitavanja treba ispitati je li uneseni broj negativan, uz postavljanje
početne vrijednosti varijable x na negativnu vrijednost koja služi samo za to da se petlja
obavi prvi put:

105
Uvod u programiranje Kontrola programskog toka

int x = -1;
while (x < 0) {
scanf("%d", &x);
if (x < 0) {
printf("%d", -x);
}
}
I ovdje ponavljamo isti posao (ispitivanje vrijednosti varijable x), uz dodatan trik kojim
osiguravamo ulazak u petlju.
Rješenje s petljom s ispitivanjem uvjeta ponavljanja na kraju ima sličan nedostatak, ali
barem nije potrebno koristiti trik za ulazak u petlju:
int x;
do {
scanf("%d", &x);
if (x < 0) {
printf("%d", -x);
}
} while (x < 0);
Dobro je zato poznavati naredbu break koja odmah prekida izvođenje petlje i nastavlja s
prvom naredbom iza petlje. Korištenjem te naredbe može se postići da se u svakom
koraku obavi jedno ispitivanje manje:
int x;
while (1) {
scanf("%d", &x);
if (x < 0) break;
printf("%d", -x);
}
U ovom konkretnom primjeru ušteđeno vrijeme ispitivanja zanemarivo je u odnosu na
vrijeme potrebno za ispis rezultata, a pogotovo na vrijeme čekanja na korisnika koji treba
unijeti sljedeću vrijednost, što ne mora biti tako ako su ostale naredbe u petlji također vrlo
brze.
Treba, ipak, naglasiti da je ovdje program nešto manje semantički jasan (u prethodnim
primjerima jasno je što je uvjet ponavljanja, ovdje ga treba tražiti u tijelu petlje), ali je zato,
uz to što je učinkovitiji, i robusniji na eventualne izmjene koje će, ako je potrebno, trebati
načiniti samo na jednom mjestu.
Slijedi još jedan primjer smislene uporabe naredbe break u kojemu treba odrediti je li
upisani prirodni broj prost71 ili nije. Bez break izvedba bi izgledala ovako:
#include <stdio.h>
#include <stdbool.h>
int main(void) {
bool prost;
int i, n;
scanf("%d", &n);
for (i = 2, prost = true; prost && i < n; i++) {
if (n % i == 0) prost = false;

71Prost broj djeljiv je samo s jedinicom i samim sobom, pa u ovom primitivnom primjeru ispitujemo redom
djeljivost sa svim ostalim brojevima. U praksi je dovoljno ispitivati djeljivost do √𝑛 , ali još ne znamo
izračunati kvadratni korijen pozivom funkcije sqrt.

106
Uvod u programiranje Kontrola programskog toka

}
if (prost) {
printf("%d je prost broj.\n", n);
} else {
printf("%d nije prost broj.\n", n);
}
return 0;
}
Program 13. ProstBezBreak.c

Uz break možemo izbjeći korištenje zastavice prost, a i izvođenje je kraće jer nema stalnog
ispitivanja je li broj još uvijek prost. Mora se, međutim, ispitati je li petlja obavljena do
kraja ili se iz petlje iskočilo. Informacija o tome sadržana je u vrijednosti kontrolne
varijable i:
#include <stdio.h>
int main(void) {
int i, n;
scanf("%d", &n);
for (i = 2; i < n; i++) {
if (n % i == 0) break;
}
if (i == n) {
printf("%d je prost broj.\n", n);
} else {
printf("%d nije prost broj.\n", n);
}
return 0;
}
Program 14. ProstSBreak.c

Naredba continue
Naredba continue preskače sve naredbe do kraja petlje u kojoj je napisana i nastavlja s
ispitivanjem uvjeta ponavljanja. Zadatak u kojem treba učitavati cijele brojeve sve dok se
ne unese 0, a u prozor ispisivati samo parne brojeve mogao bi se riješiti na sljedeći način:
int x;
while (1) {
scanf("%d", &x);
if (x == 0) break;
if (x % 2 == 0) {
printf("%d", x);
}
}
Istu funkcionalnost moglo bi se postići i naredbom continue:
int x;
while (1) {
scanf("%d", &x);
if (x == 0) break;
if (x % 2 != 0) continue;
printf("%d", x);
}
U ovako jednostavnom slučaju, gdje je kraj petlje neposredno iza ispisa, prednost bi
trebalo dati prvome rješenju. Međutim, u primjeru u kojem se za parne brojeve izvodi
107
Uvod u programiranje Kontrola programskog toka

neka kompleksna kalkulacija koja se proteže kroz nekoliko desetaka redova, smislenije je
ne započinjati dugi if, nego odmah jasno reći: za neparne se brojeve u ovoj petlji nema više
što raditi.

Naredba goto
Naredba goto koristi se na sljedeći način:
goto oznaka;
Nailaskom na ovu naredbu program nastavlja s radom naredbom koja neposredno slijedi
iza oznake. Na mjestu na koje naredbom goto treba otići iza oznake mora biti znak
dvotočke (:).
Korištenje naredbe goto često se apriorno destimulira, ali postoje slučajevi u kojima ova
naredba ima svoju primjenu. Jasno je da se

for (i = 0; i < 10; i++) {
naredbe;
}
nastavak;
nikad neće i ne smije realizirati ekvivalentim kôdom koji koristi goto:

i = 0;
opet:
if (i >= 10) goto gotovo;
naredbe;
i++;
goto opet;
gotovo:
nastavak;
jer to program čini znatno nečitljivijim, iako je na najnižoj, asemblerskoj razini for
naredba realizirana upravo ovako.
U sljedećem primjeru goto itekako ima smisla:
for (...; uvjet1; ...) {
while (uvjet2) {
do {
...
if (uvjet4) prekinuti sve tri petlje;
...
} while (uvjet3);
nastavak3;
}
nastavak2;
}
nastavak1;
break, nažalost, ovdje ograničeno pomaže jer prekida rad samo unutrašnje petlje, pa je
bez korištenja goto nužno uvesti zastavicu npr. gotovo i stalno ispitivati je li postavljena:
bool gotovo = false;
for (...; uvjet1; ...) {

108
Uvod u programiranje Kontrola programskog toka

while (uvjet2) {
do {
...
if (uvjet4) {
gotovo = true;
break;
}
...
} while (uvjet3);
if (gotovo) break;
nastavak3;
}
if (gotovo) break;
nastavak2;
}
nastavak1;
Rješenje s goto elegantnije je i znatno učinkovitije jer nema stalnih ispitivanja zastavice:
for (...; uvjet1; ...) {
while (uvjet2) {
do {
...
if (uvjet4) goto van;
...
} while (uvjet3);
nastavak3;
}
nastavak2;
}
van:
nastavak1;

109
Uvod u programiranje Funkcije

Funkcije
Često je u programiranju neki posao potrebno obaviti više puta na više različitih mjesta.
Školski primjer za to je problem izračunavanja izraza (𝑛𝑘) . Ovaj izraz daje binomni
koeficijent, tj. koeficijent uz xk iz razvoja (1+x)n, ali i broj kombinacija, tj. načina na koji se
iz n elemenata nekog skupa može odabrati njih k.
Ovaj se izraz može napisati i kao
𝑛!
(𝑛𝑘) = .
𝑘!(𝑛−𝑘)!

Faktorijeli se izračunavaju na jednostavan način, kako je prikazano u prethodnom


poglavlju, pa bi se program koji računa ovaj izraz mogao primitivno napisati na sljedeći
način:
#include <stdio.h>
int main(void) {
int i, n, k;
double nf, kf, nmkf; // faktorijeli
scanf("%d %d", &n, &k);
for (i = 1, nf = 1; i <= n; i++) {
nf *= i;
}
for (i = 1, kf = 1; i <= k; i++) {
kf *= i;
}
for (i = 1, nmkf = 1; i <= n-k; i++) {
nmkf *= i;
}
printf("%d povrh %d = %g\n", n, k, nf / (kf*nmkf));
return 0;
}
Program 15. PovrhBezFunkcije.c

Program radi korektno, pa tako na primjer za (45


6
), što daje broj mogućih ishoda izvlačenja
lota 6/45, izračunava
45 povrh 6 = 8.14506e+006
a za (39
7
) izračunava
39 povrh 7 = 1.53809e+007
Napravljen je, međutim, vjerojatno najveći grijeh koji se u programiranju može napraviti:
jedan te isti posao realizira se više puta na različitim mjestima u programu. Problem nije
toliko u povećanom utrošku memorije za duplicirane naredbe, koliko u povećanoj
vjerojatnosti pogreške u programiranju i otežanom održavanju programa.
I u ovako jednostavnom programu utipkana je samo prva petlja, a druge dvije su upisane
služeći se poznatom metodom kopiraj/ulijepi (copy/paste), pa je nakon svakog kopiranja
bilo potrebno promijeniti varijablu u koju se kumulativno množi i varijablu koja označava
broj koraka petlje. U tome se lako može pogriješiti. Povećanjem broja naredbi koje se tako
kopiraju vjerojatnost pogreške raste.
Također, uz pretpostavku da se u tako kopiranom programskom odsječku naknadno
pronađe greška, što je česta pojava pri razvoju softvera, istu će grešku biti potrebno
popravljati na više mjesta.
110
Uvod u programiranje Funkcije

Zato je najvažnije pravilo dobrog programiranja rastaviti program na cjeline koje za neke
ulazne vrijednosti daju odgovarajući rezultat na izlazu:

ulaz izlaz

Ovaj je princip istovjetan principu matematičke funkcije koja vrijednost iz neke domene
preslikava u neku drugu vrijednost iz druge ili iste domene, y = f(x), što se može prikazati
slikom

x y
f

pa se zato takve cjeline u programskim jezicima obično zovu funkcijama.


Funkcija se u programskom jeziku C općenito definira na sljedeći način
tip_rezultata naziv_funkcije(popis_parametara) {
naredbe;
return rezultat;
}
U primjeru faktorijela, funkcija (zbog veličine rezultata) vraća rezultat tipa double, a jedini
je ulazni parametar n:
double fakt(int n) {
int i;
double f;
for (i = 1, f = 1; i <= n; i++) {
f *= i;
}
return f;
}
Funkcija se poziva tako da se u nekom izrazu navedu ime i konkretni ulazni argumenti
koji će se u funkciji supstituirati na mjesta parametara funkcije. Ulazni argumenti funkcije
mogu biti i izrazi
x = fakt(5);
y = fakt(m) + fakt(3*n);
U prvom primjeru, parametar n poprima vrijednost 5 i rezultat se izravno pohranjuje u
varijablu x. U drugom primjeru, parametar n poprimit će vrijednost varijable m iz
pozivajućeg programa, a rezultat funkcije zbrojit će se s rezultatom koji funkcija daje kad
parametar n poprimi vrijednost varijable n iz pozivajućeg programa pomnožene s 3.
Funkcija mora biti definirana prije poziva, pa program za izračunavanje (𝑛𝑘) sada glasi
#include <stdio.h>
double fakt(int n) {

111
Uvod u programiranje Funkcije

int i;
double f;
for (i = 1, f = 1; i <= n; i++) {
f *= i;
}
return f;
}
int main(void) {
int n, k;
scanf("%d %d", &n, &k);
printf("%d povrh %d = %g\n", n, k,
fakt(n) / (fakt(k) * fakt(n-k)));
return 0;
}
Program 16. PovrhSFunkcijom.c

Svaki posao koji ima dobro definirani ulaz i izlaz ima smisla realizirati funkcijom, pa bi
tako ovdje bilo smisleno uvesti i funkciju povrh, s dva parametra, n i k.
#include <stdio.h>
double fakt(int n) {
int i;
double f;
for (i = 1, f = 1; i <= n; i++) {
f *= i;
}
return f;
}
double povrh(int n, int k) {
return fakt(n) / (fakt(k) * fakt(n-k));
}
int main(void) {
int n, k;
scanf("%d %d", &n, &k);
printf("%d povrh %d = %g\n", n, k, povrh(n,k));
return 0;
}
Program 17. PovrhSFunkcijom1.c

Glavni je program ovako mnogo pregledniji.


𝑛!
Nadalje, izraz može se optimirati. S obzirom da mora biti n ≥ k, očito je da se i u
𝑘!(𝑛−𝑘)!
brojniku i nazivniku obavlja izračunavanje k!, koje se može pokratiti i tako skratiti vrijeme
izvođenja, pa dobijemo
(𝑘 + 1)(𝑘 + 2) … (𝑛 − 1)𝑛
(𝑛 − 𝑘)!
Štoviše, ako je (n-k) > k, onda ima više smisla kratiti s (n-k)! pa izraz onda glasi
(𝑛 − 𝑘 + 1)(𝑛 − 𝑘 + 2) … (𝑛 − 1)𝑛
𝑘!
U tu svrhu može se dodati funkcija produkt koja vraća produkt svih cijelih brojeva iz
intervala [od, do]. Treba primijetiti da je do ključna riječ u C-u, tako da će parametar
funkcije trebati nazvati drukčije:

112
Uvod u programiranje Funkcije

double produkt(int brojOd, int brojDo) {


int i;
double r;
for (i = brojOd, r = 1; i <= brojDo; i++) {
r *= i;
}
return r;
}
Funkcija povrh sada glasi
double povrh(int n, int k) {
if (k > n - k) {
return produkt(k+1, n) / fakt(n-k);
} else {
return produkt(n-k+1, n) / fakt(k);
}
}
Funkcija fakt sad je samo poseban slučaj funkcije produkt pa se može realizirati i kraće
double fakt(int n) {
return produkt(1, n);
}
Program 18. PovrhSFunkcijom2.c

Na kraju, ako se uoči da u brojniku izraza


(𝑘 + 1)(𝑘 + 2) … (𝑛 − 1)𝑛
(𝑛 − 𝑘)!
ima upravo n-k multiplikanada, pa se izraz raspiše dalje kao
(𝑘 + 1)(𝑘 + 2) … (𝑛 − 1)𝑛
1 · 2 · … (𝑛 − 𝑘)
i još rastavi na
(𝑘 + 1) (𝑘 + 2) (𝑛 − 1) 𝑛
· ·… ·
1 2 (𝑛 − 𝑘 − 1) (𝑛 − 𝑘)
dobije se
𝑛−𝑘
𝑛 (𝑘 + 𝑖)
( )=∏
𝑘 𝑖
𝑖=1
Ako se slično napravi za
(𝑛 − 𝑘 + 1)(𝑛 − 𝑘 + 2) … (𝑛 − 1)𝑛
𝑘!
dobije se
𝑘
𝑛 𝑛 − (𝑘 − 𝑖)
( )=∏
𝑘 𝑖
𝑖=1
Očigledno je da treba odabrati ono množenje koje se obavi u manje koraka, pa funkcija
povrh optimalno izgleda ovako
double povrh(int n, int k) {
int i;

113
Uvod u programiranje Funkcije

double r;
if (k < n - k) {
for (r = 1, i = 1; i <= k; i++) {
r *= (double)(n - (k - i)) / i;
}
} else {
for (r = 1, i=1; i <= n - k; i++) {
r *= (double)(k + i) / i;
}
}
return r;
}
Program 19. PovrhSFunkcijom3.c

U ovoj izvedbi obavi se ukupno najmanje koraka petlje i manja je vjerojatnost preliva jer
je već tijekom kumulativnog množenja svaki multiplikand podijeljen s i, umjesto da se
jedan veliki umnožak tek naknadno podijeli s (velikim) faktorijelima.

Implicitna konverzija argumenata i rezultata


Prilikom poziva funkcije važno je predati upravo onoliko argumenata koliko funkcija ima
parametara, s time da argumenti moraju odgovarati po tipu odgovarajućim parametrima
ili se u podatke toga tipa moraju moći implicitno pretvoriti, pri čemu treba imati na umu
posljedice takve implicitne konverzije
U sljedećem se primjeru mogu vidjeti posljedice implicitnih konverzija nekih numeričkih
podataka:
int f(char a, int b, float c, double d) {

printf("%c %d %f %f\n", a, b, c, d);

}
Za poziv u kojem se takvoj funkciji redom predaju konstante tipa double, float, int i char
r = f(65.3450, 3.8f, 3, 'A');
ispisat će se
A 3 3.000000 65.000000
Implicitna konverzija obavit će se i u naredbi return. U primjeru funkcije koja vraća
prosječnu vrijednost tri ulazna argumenta, ako je funkcija definirana kao
int f(int a, int b, int c) {
return (a + b + c) / 3.
}
prosjek je izračunat korektno, jer je jedan od operanada realni broj, ali funkcija vraća int
pa će se prije povratka na pozivajuću razinu obaviti konverzija u cijeli broj, tj. odsijecanje
decimala.
Sljedeći primjer također vraća pogrešnu vrijednost:
float f(int a, int b, int c) {
return (a + b + c) / 3
}

114
Uvod u programiranje Funkcije

Ovdje je prosjek izračunat u cjelobrojnoj domeni, pa će se prilikom povratka, prekasno,


obaviti konverzija u realni broj. Zato će
printf("%f", f(1, 1, 2));
ispisati 1.000000 umjesto 1.333333.
Ispravna funkcija,naravno, glasi
float f(int a, int b, int c) {
return (a + b + c) / 3.
}

Funkcije bez rezultata


Funkcija tipa void ne vraća nikakav rezultat, već samo obavlja neki posao. Takva funkcija
u općenitom obliku izgleda ovako:
void naziv_funkcije(popis_parametara) {
naredbe;
return;
}
Naredba return u ovom slučaju ne mora postojati. Sljedeći jednostavni primjer
formatirano ispisuje koordinate neke točke na izlaz:
void ispisKoordinata(int x, int y) {
printf("x=%d, y=%d", x, y);
}
Ne treba dvojiti je li potrebno i ovako jednostavan posao (jednu naredbu programa)
izdvojiti u posebnu funkciju. Odgovor se sam po sebi nameće: ako korisnik nekog
programa u kojemu se koordinate ispisuju na 1000 mjesta zatraži da ispis umjesto u
obliku x=x, y=y bude u obliku (x,y), postojanje funkcije omogućava da se takva izmjena
napravi na jednom jedinom mjestu.

Funkcije bez parametara


Funkcije bez parametara u općenitom obliku izgledat će ovako:
tip_rezultata naziv_funkcije(void) {
naredbe;
return rezultat;
}
Za primjer može poslužiti funkcija koja će prebrojati koliko je s tipkovnice učitano
pozitivnih brojeva prije nego se unijela nula:
int broji(void) {
int brojac = 0, a;
while(1) {
scanf("%d\n", &a);
if (a == 0) return brojac;
if (a > 0) brojac++;
}
}
Moguće i slučaj da funkcija nema parametara ni rezultata:
void naziv_funkcije(void) {

115
Uvod u programiranje Funkcije

naredbe;
return;
}
Pretpostavimo da se s tipkovnice učitava niz cijelih brojeva koje treba početi obrađivati
tek nako što se unese nula. Funkcija koja bi preskočila takvu uvodnu sekvencu i nakon
toga predala kontrolu glavnom programu mogla bi biti
void preskoci(void) {
int a;
while (1) {
scanf("%d", &a);
if (a == 0) return;
}
}

Funkcije s varijabilnim brojem argumenata


Ponekad je potrebno omogućiti da se funkcija pozove s različitim brojem argumenata.
Primjer takve funkcije je već mnogo puta korištena funkcija printf, koja može imati
proizvoljno dugačku listu argumenata. Mehanizam dohvata argumenata u funkciji s
varijabilnim brojem argumenata C-u je relativno primitivan pa se bez dodatne informacije
ne može zaključiti koliko argumenata ima niti koji su im tipovi. U primjeru funkcije printf,
ta je informacija sadržana u formatu: argumenata mora biti onoliko koliko je
konverzijskih specifikacija, a tipovi argumenata zaključuju se iz konkretnih konverzijskih
specifikacija.
Ovdje će kao primjer biti prikazana funkcija koja treba zbrojiti sve cijele brojeve koji joj
se kao argumenti predaju prilikom poziva, s time da se kao prvi argument mora navesti
koliko takvih brojeva ima.
#include <stdio.h>
// definicije tipova podataka i funkcija za rad s
// varijabilnim brojem argumenata
#include <stdarg.h>
int sum (int broj, ...) {
int r, i;
va_list args; // definicija liste argumenata
va_start(args, broj); // inicijalizacija liste argumenata
for (i = 0, r = 0; i < broj; i++) {
// dohvat sljedećeg argumenta iz liste
// (poznato je da je int)
r += va_arg(args, int);
}
va_end(args); // završetak rada s listom argumenata
return r;
}
int main(void) {
// poziv funkcije
// prvi argument je broj preostalih argumenata
printf("%d\n", sum(3, 1, 2, 3));
return 0;
}
Program 20. VarijabilniBrojArgumenata.c

116
Uvod u programiranje Funkcije

Parametri i lokalne varijable funkcije


Vrlo pojednostavnjeno, svaki se parametar i svaka varijabla definirana unutar funkcije
može promatrati kao varijabla vidljiva samo unutar tijela funkcije i koja nema ništa
zajedničko s ostatkom programa. Takve varijable zovu se još i lokalnim varijablama.
Sljedeći se primjer
#include <stdio.h>
void f(int x) {
int y;
y = x; // c)
x = x + 1; // d)
return; // e)
}
int main(void) {
int x = 1; // a)
f(x); // b)
printf("%d\n", x); // f)
return 0;
}
Program 21. LokalnaVarijablaFunkcije.c

može konceptualno i na vrlo visokoj logičkoj razini (detaljnije će to biti objašnjeno u


sljedećem poglavlju) prikazati sljedećim slikama.
Prije poziva funkcije, nakon izvođenja naredbe a), stanje varijabli može se ilustrirati
ovako

main x
1

Funkcija i njene varijable kao da ne postoje.


Tijekom poziva funkcije f (naredba b)), prvo se rezervira prostor za parametar x i varijablu
y funkcije f

main x f x y
1 ? ?

a nakon toga, također tijekom poziva, vrijednost varijable x iz glavnog programa


(argumenta funkcije f) kopira u varijablu x funkcije (parametar funkcije f).

main x f x y
1 1 ?

117
Uvod u programiranje Funkcije

Varijabla y u funkciji još uvijek ima nepoznatu vrijednost. Naredbama c) i d) postavlja se


vrijednost za y te x uvećava za 1:

main x f x y
1 2 1

Treba primijetiti da x iz glavnog programa nije doživjela nikakvu promjenu, jer funkcija
radi samo s varijablama koje su joj u dosegu vidljivosti. return (naredba e)) oslobađa
prostor rezerviran za varijable funkcije, pa ne treba čuditi da ispis f) ispisuje
nepromijenjeno stanje varijable x iz glavnog programa:

main x
1

Ovakav se način pozivanja funkcije zove poziv preko vrijednosti (call by value) i obavlja se
pri svakom posljeđivanju argumenata u parametre. Ako funkcija treba promijeniti
vrijednosti varijable iz pozivajućeg programa, treba ostvariti tzv. poziv preko adrese (call
by reference) predavanjem vrijednosti adrese takve varijable i indirektnom promjenom u
funkciji preko pokazivača, što će biti objašnjeno kasnije.

Mehanizam poziva funkcije


Nakon poziva funkcije, umjesto naredbe koja bi sljedeća po redu trebala biti izvedena,
izvodi se prva naredba funkcije, obave sve naredbe unutar funkcije, pa se nakon povratka
na pozivajuću razinu nastavlja izvoditi prva naredba iza poziva funkcije, ili se nastavi
evaluirati izraz iz kojeg je funkcija pozvana. Ovo se može ilustrirati sljedećom slikom:

int main(void) {
int i;
i = 0;
Operacijski f(); void f(void) {
i = i + 1; int j;
sustav f(); j = 1;
i = i + 1; return 0;
f(); }
i = i + 1;
return 0;
}

Slika 25. Slijed izvođenja naredbi kod poziva funkcije

118
Uvod u programiranje Funkcije

Iz ove slike je vidljivo da je kod poziva funkcije naredba koja se sljedeća treba izvesti
uvijek jednoznačno određena, dok to prilikom povratka na razinu odakle je funkcija
pozvana neće biti slučaj. Očito se iz pozivajućeg programa funkciji treba nekako
proslijediti informacija kojim se putem vratiti. Ova informacija naziva se povratnom
adresom, tj. adresom instrukcije s koje je funkcija pozvana.
Ovo bi bilo lako rješivo kad bi u programu, kao što je to u gornjem prikazu ilustrirano,
egzistirala samo jedna aktivna instanca funkcije. Funkcija bi tada rezervirala u memoriji
prostor za pohranu takve adrese i tu informaciju iskoristila prilikom povratka. Također,
funkcija bi mogla rezervirati unaprijed prostor za pohranu vrijednosti svih parametara i
varijabli koje su definirane unutar funkcije.
Međutim, moguće su situacije kod kojih je ista funkcija istovremeno aktivna na više razina
pozivanja, kao u sljedećem primjeru.
4
void f(int x) {
int main(void) { void g(void) {
if (x == 1) { 3 f(2);
f(1); 1 2 g();
return 0; 6 return;
}
} 5 return; }
} 7

Slika 26. Slijed izvođenja naredbi kod poziva funkcije iz druge funkcije

Funkcija f poziva se iz glavnog programa s argumentom 1 (putanja 1). Parametar x u f


poprima vrijednost 1, izraz u naredbi if je istinit pa je sljedeća naredba poziv funkcije g
(putanja 2). Poziva se g (putanja 3) u kojoj se odmah poziva f s argumentom 2 (putanja 4).
Sada su aktivne dvije instance funkcije f, prva u kojoj x ima vrijednost 1 i druga u kojoj x
ima vrijednost 2. Ovdje izraz u naredbi if nije istinit pa se putanjom 5 ide na return. Očito
za f moraju u tome trenutku biti zapamćene dvije vrijednosti povratnih adresa. Sada se
treba vratiti putanjom 6 u g, odakle dalje putanjom 7 u f pa putanjom 8 u glavni program.
Situacija se još komplicira u slučaju rekurzivnih poziva funkcije. Rekurzija je način
programiranja kod kojeg funkcija poziva samu sebe. U takvom je slučaju istovremeno
aktivno mnogo instanci iste funkcije, što znači da se mora pamtiti isto toliko instanci svih
varijabli u funkciji i isto toliko instanci povratnih adresa. O rekurziji će biti više riječi
kasnije.
S obzirom na činjenicu da će svaka razina poziva funkcije zahtijevati rezervaciju memorije
za svoje parametre, varijable i povratnu adresu, da je broj tih razina praktički
neograničen, a da se nakon povratka iz funkcije rezervirana memorija može osloboditi, za
izvedbu mehanizma poziva funkcija prikladan je stog.
Stog je strukura podataka koja podržava dodavanje podatka na tzv. vrh stoga i uzimanje
podatka s vrha stoga. Posljednji na stog postavljenji podatak prvi je na redu za uzimanje,
pa se zato takve strukture zovu još i strukture tipa posljednji unutra, prvi van (last in, first
out – LIFO). Sljedeća slika ilustrira stanje stoga na koji se redom, počevši od praznog stoga,
stavljaju podaci A, B i C nakon čega se sa stoga ti isti podaci suprotnim redoslijedom
uzimaju.

119
Uvod u programiranje Funkcije

C
B B B
A A A A A

Slika 27. Stog

Prilikom svakog poziva funkcije stog se proširuje za jedan okvir stoga (stack frame), koji
se sastoji od vrijednosti parametara funkcije, povratne adrese, svih lokalnih varijabli
funkcije i nekih registara procesora, između ostalih i registra koji sadrži poziciju vrha
stoga prije poziva funkcije.
Prevodilac tijekom prevođenja za sve parametre i lokalne varijable priprema kôd koji će
ih referencirati relativno unutar okvira stoga.
Pri povratku iz funkcije, sa stoga se uzimaju povratna adresa i pozicija vrha stoga prije
poziva funkcije, vrh stoga vraća se na poziciju na kojoj je bio prije poziva funkcije, a
program nastavlja s radom od povratne adrese.
Ovaj mehanizam može se se ilustrirati sljedećom slikom, u kojoj će se prikazati samo
osnovni elementi okvira stoga. Ovdje je, radi preglednosti, funkcija f napisana dva puta.
Takav prikaz bolje oslikava vremenski slijed poziva, a omogućava i lakše praćenje stanja
stoga. U stvarnosti, naravno, kôd funkcije postoji samo na jednom mjestu i svaki poziv
određene funkcije, s bilo koje razine, programski tok skreće na istu instrukciju.
Primjer na kojem će ovo biti demonstrirano sličan je prethodnom, samo s više parametara
i s lokalnim varijablama. Ilustrirano je stanje stoga neposredno nakon poziva funkcije, a
prije prve izvršne naredbe.

120
Uvod u programiranje Funkcije

1 void f(int x, void f(int x,


int main(void) { int y) { void g(int c) { int y) {
f(1,2); int a, b; int d; 3 int a, b;
return 0; a = x; 2 d = c; a = x;
} if (x == 1) { f(11,12); if (x == 1) {
g(3); return; g(3);
} } }
return; return;
} }
6
5 4

a ?
b ?
3 p.a.
x 11
y 12
4
d ? d 3
2
p.a. p.a.
c 3 c 3
a 5 a a
? 1 ?
1 b b b
? ? ?
p.a. p.a. p.a.
6 x 1 x 1 x 1
y 2 y 2 y 2

Slika 28. Poziv funkcije i stog

Treba primijetiti da je i main funkcija koja se poziva iz operacijskog sustava, što znači da
će se pri pozivu funkcije main i po povratku iz funkcije main obaviti isti proces kao i za
bilo koju funkciju.
Kako se vrh stoga nakon povratka iz funkcije vraća na istu razinu na kojoj je bio prije
poziva funkcije, jasno je da će funkcije koje se pozivaju s iste razine koristiti isti
memorijski prostor. U sljedećem će primjeru
#include <stdio.h>
void f() {
int a;
a = 777;
}
int g() {
int b;
return b;
}
int main(void) {
int r;
f();

121
Uvod u programiranje Funkcije

r = g();
printf("%d\n", r);
return 0;
}
Program 22. DijeljenjeStoga.c

biti ispisano 777 iako se ispisuje rezultat funkcije g koja vraća vrijednost neinicijalizirane
lokalne varijable b. Varijabla b, međutim, zauzima isti prostor koji je tijekom poziva
funkcije f zauzimala varijabla a, pa time i preuzima njenu vrijednost. Sada je razumljivo
zašto se vrijednosti lokalnih varijabli funkcije smatraju nepoznatim: one zauzimaju
prostor koji su prije zauzimale neke druge varijable nekih drugih funkcija, pri čemu je
sadašnji prostor za jednu varijablu možda prije bio samo dio neke veće varijable, ili je to
sad prostor koju je prije zauzimalo više manjih varijabli.

Rekurzivne funkcije
Funkcija koja poziva samu sebe naziva se rekurzivnom fukcijom. Pogledajmo sljedeći
primjer:
#include <stdio.h>
void f(int n) {
printf("%d ", n);
f(n+1);
}
int main(void) {
f(1);
return 0;
}
Program 23. RekurzijaBezZavrsetka.c

Glavni program poziva funkciju f s argumentom 1, funkcija ispisuje vrijednost svoga


jedinog parametra i taj parametar, uvećan za 1, prosljeđuje toj istoj funkciji.
Ovo se može vizualizirati kao da postoji više instanci funkcije i kao da svaka instanca
funkcije ima svoju lokalnu varijablu n:

void f(int n){ void f(int n){ void f(int n){


int main(void) { printf printf printf
f(1); ("%d ", n); ("%d ", n); ("%d ", n);
return 0; …
f(n+1); f(n+1); f(n+1);
} } } }

n n n
1 2 3 …

Slika 29. Rekurzija bez završetka

Funkcija doista ispisuje rastući slijed brojeva, ali nakon što ispiše 64897 dogodi se greška
prilikom izvođenja.

122
Uvod u programiranje Funkcije

Više instanci funkcije ne postoji. Postoji samo jedna instanca funkcije f, ali svaki poziv
funkcije rezervira mjesto za parametre (i lokalne varijable, gdje ih ima) na stogu:

void f(int n){ void f(int n){ void f(int n){


int main(void) { printf printf printf
f(1); ("%d ", n); ("%d ", n); ("%d ", n); …
return 0; f(n+1); f(n+1); f(n+1);
} } } }

p.a.
n 3
p.a. p.a.
n 2 n 2
p.a. p.a. p.a.

n 1 n 1 n 1

Slika 30. Rekurzija bez završetka i stog

Iz slike je vidljivo da stog stalno raste i da će se memorija inicijalno dodijeljena programu


za stog prije ili kasnije potrošiti.
Rekurzivna funkcija zato, pod nekim uvjetima, treba prestati pozivati samu sebe i vratiti
se odakle je pozvana. Pretpostavimo da se rekurzivnom funkcijom želi ispisivati brojeve
od 1 do n.
Očigledna je izvedba
#include <stdio.h>
void f(int n, int g) {
if (n > g) return;
printf("%d ", n);
f(n+1, g);
}
int main(void) {
f(1, 2);
return 0;
}
Program 24. BrojanjeRekurzijom.c

Izvođenje ovog programa može se vizualizirati sljedećom slikom, u kojoj je objedinjen


logički prikaz varijabli funkcije s fizičkim stanjem na stogu.

123
Uvod u programiranje Funkcije

void f(int n, void f(int n, void f(int n,


int main(void) { int g){ int g){ int g){
f(1,2); if (n > g) if (n > g) if (n > g)
return 0; return; return; return;
} printf printf printf
("%d ", n); ("%d ", n); ("%d ", n);
f(n+1, g); f(n+1, g); f(n+1, g);
return; return; return;
} } }

n g n g n g

1 2 2 2 3 2

p.a.
n 3
g 2
p.a. p.a.
n 2 n 2
g 2 g 2
p.a. p.a. p.a.
n n 1 n 1
1
g g 2 g 2
2

Slika 31. Brojanje rekurzijom

Na prvoj razini poziva varijable imaju vrijednosti n=1 i g=2 pa se obavi ispis nakon čega
se pozove funkcija s argumentom 2. Na drugoj razini poziva varijable imaju vrijednosti
n=2 i g=2 pa se obavi ispis nakon čega se pozove funkcija s argumentom 3. Sada je n > g pa
se obavi povratak na prethodnu razinu na poziciju nakon poziva funkcije, odakle return
vraća na prethodnu razinu na isto mjesto, gdje return vraća u glavni program. Ispisuje se,
dakle
1 2
Ispis od 1 do n može se načiniti i s jednim argumentom, n, ako se ispis obavlja po povratku
iz rekurzije, programom
#include <stdio.h>
void f(int n) {
if (n <= 0) return;
f(n-1);
printf("%d ", n);
}
int main(void) {
f(2);
return 0;
}
Program 25. BrojanjeRekurzijomUPovratku.c

124
Uvod u programiranje Funkcije

što se može ilustrirati sljedećom slikom u kojoj je izostavljen prikaz stoga:

void f(int n){ void f(int n){ void f(int n){


int main(void) { if (n <= 0) if (n <= 0) if (n <= 0)
f(2); return; return; return;
return 0; f(n-1); f(n-1); f(n-1);
} printf printf printf
("%d ", n); ("%d ", n); ("%d ", n);
return; return; return;
} } }

n n n
2 1 0

Slika 32. Brojanje rekurzijom u povratku

Na prvoj razini je n=2, pa se obavlja rekurzivni poziv s 1. Na sljedećoj razini vrijednost je


n=1, pa se obavlja rekurzivni poziv s 0. Ovdje je n=0 pa se obavlja povratak na prethodnu
razinu na naredbu iza poziva funkcije – ispis vrijednosti varijable n koja je na toj razini 1.
Nakon toga obavlja se povratak na prethodnu razinu, opet na ispis. Varijabla n na toj razini
iznosi 2. Slijedi povratak u glavni program. Ispis je, dakle,
1 2
Pokušajmo sada riješiti rekurzivno problem izračunavanja n!. Faktorijeli se mogu
definirati sljedećim rekurzivnim izrazom
1 𝑎𝑘𝑜 𝑗𝑒 𝑛 = 0 𝑖𝑙𝑖 𝑛 = 1
𝑛! = {
(𝑛 − 1)! ∙ 𝑛 𝑎𝑘𝑜 𝑗𝑒 𝑛 > 1
Matematički rekurzivni izrazi izrazito se lako pretvaraju u rekurzivnu funkciju:
double fakt(int n) {
if (n <= 1) return 1;
return fakt(n-1)*n;
}
Program 26. FaktorijeliRekurzivno.c

Rad ove funkcije može se vizualizirati sljedećom slikom

125
Uvod u programiranje Funkcije

double f(int n){ double f(int n){ double f(int n){


int main(void) { if (n <= 1) if (n <= 1) if (n <= 1)
double r; return 1; return 1; return 1;
r = fakt(3); return return return 1
return 0; fakt(n-1)*n; fakt(n-1)*n; fakt(n-1)*;
} } } }
2
6

n n n
3 2 1

Slika 33. Faktorijeli rekurzivno

Na prvoj razini n=3 pa se obavi rekurzivni poziv s argumentom 2. Na drugoj razini n=2 pa
se obavi rekurzivni poziv s argumentom 1. Na trećoj razini n=1 pa funkcija na prethodnu
razinu vraća 1. Taj se rezultat na prethodnoj razini množi s n s te razine koji iznosi 2 pa se
na prethodnu razinu vraća 2. Rezultat se na prethodnoj razini množi s n koji tamo iznosi
3 pa se u glavni program vraća 6.

Inline funkcije
Iz prethodnog opisa mehanizma poziva funkcije jasno je da je za poziv funkcije potrebno
obaviti određeni dodatni posao, u engleskoj terminologiji zvan overhead. Treba stvoriti
okvir stoga, kopirati argumente na stog, obaviti naredbu koja će programski tok skrenuti
na prvu naredbu funkcije, na kraju očistiti stog te obaviti naredbu koja će programski tok
vratiti natrag na mjesto poziva. Pokušajmo, bez ulaska u detalje asemblerskih naredbi,
usporediti veličinu asemblerskog kôda nastalog prevođenjem programa koji zbrajanje
dva broja obavlja funkcijom s veličinom asemblerskog kôda programa koji dva broja
zbraja izravno.
int f(int a, int b) { int main(void) {
return a+b; int a = 1, b = 2, c;
} c = a + b;
int main(void) { }
int a = 1, b = 2, c;
c = f(1,2);
}

LFB0: .cfi_startproc
.cfi_startproc pushl %ebp
pushl %ebp .cfi_def_cfa_offset 8
.cfi_def_cfa_offset 8 .cfi_offset 5, -8
.cfi_offset 5, -8 movl %esp, %ebp
movl %esp, %ebp .cfi_def_cfa_register 5
.cfi_def_cfa_register 5 andl $-16, %esp
movl 8(%ebp), %edx subl $16, %esp
movl 12(%ebp), %eax call ___main
addl %edx, %eax movl $1, 12(%esp)
popl %ebp movl $2, 8(%esp)
.cfi_restore 5 movl 12(%esp), %edx
.cfi_def_cfa 4, 4 movl 8(%esp), %eax
ret addl %edx, %eax
.cfi_endproc movl %eax, 4(%esp)
LFE0: movl $0, %eax
.def ___main; .scl 2; leave
.type 32; .endef .cfi_restore 5
.globl _main .cfi_def_cfa 4, 4
ret

126
Uvod u programiranje Funkcije

.def _main; .scl 2; .type .cfi_endproc


32; .endef
_main:
LFB1:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
andl $-16, %esp
subl $32, %esp
call ___main
movl $1, 28(%esp)
movl $2, 24(%esp)
movl $2, 4(%esp)
movl $1, (%esp)
call _f
movl %eax, 20(%esp)
movl $0, %eax
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
Iz ovoga je primjera jasno da će izvođenje lijevog programa trajati barem dvostuko dulje
nego desnog. U ovom primjeru to nije toliko bitno jer će se i jedan i drugi program obaviti
u vremenu reda nanosekundi. Međutim, ako se poziv funkcije obavi mnogo puta,
povećavajući red veličine trajanja programa na razinu sekundi ili čak minuta, načiniti
program dvostruko bržim itekako je zanimljivo.
Ključna riječ inline u definiciji funkcije, u gornjem primjeru
inline int f(int a, int b) {

naputak je prevodiocu da kod fukcije ugradi izravno u program koji funkciju poziva.
Prevođenjem programa
inline int f(int a, int b) {
return a+b;
}
int main(void) {
int a = 1, b = 2, c;
c = f(1,2);
}
nastat će asemblerskog kôda gotovo identičan programu
int main(void) {
int a = 1, b = 2, c;
c = a + b;
}
Ovdje treba napomenuti da će u prevodilac ugrađena optimizacija programskog kôda
manje funkcije ionako prevesti kao da su inline, zbog kraćeg izvođenja, ali i zbog, kako je
u gornjem primjeru jasno pokazano, uštede na memoriji potrebnoj za pohranu izvršnog
kôda. Kod većih funkcija uštede nema, naprotiv, a trajanje dodatnog posla koji je potrebno
obaviti tijekom poziva i povratka iz funkcije postaje zanemarivo u odnosu na vrijeme
izvođenja ostatka funkcije.

Makro s parametrima
Već prije spomenuta naredba #define zamjenjuje prije prevođenja neki niz znakova
drugim. Do sada se koristila samo u jednostavnom obliku poput
127
Uvod u programiranje Funkcije

#define PI 3.141592653
Naredba #define može se napisati i u nešto složenijem obliku:
#define naziv(parametar1, parametar2,…) izraz_koji_koristi_parametre
U tom će se slučaju svaka pojava niza naziv(parametar1, parametar2,…) razviti u
izraz_koji_koristi_parametre.
Tipičan primjer je izračunavanje veće od dvije vrijednosti
#define max(a, b) ((a) > (b) ? (a) : (b))
Zagrade u izrazu koji koristi parametre nisu ovdje radi sintakse, nego zato da se spriječi
kriva interpretacija koja može nastati zbog utjecaja prioriteta operatora. Naime,
pretprocesor parametre, kakvi god oni bili, jednostavno supstituira u izraz. U sljedećem
primjeru, očekivalo bi se da naredba
r = 3 + max(4,5)
varijabli r pridruži vrijednost 8.
Međutim, uz makro max definiran kao
#define max(a, b) a > b ? a : b
a će se supstituirati s 4, a b s 5, pa će se naredba
r = 3 + max(4,5)
pretvoriti u
r = 3 + 4 > 5 ? 4 : 5;
Rezultat takvoga izraza, zbog ugrađenog prioriteta operatora, je 4 (jer se prvo obavi
zbrajanje 3 + 4 = 7 što je veće od 5.
Uz ispravno napisanu definiciju
#define max(a, b) ((a) > (b) ? (a) : (b))
naredba
r = 3 + max(4,5)
se pretvara u
r = 3 + ((4) > (5) ? (4) : (5));
pa je rezultat očekivanih 8.
Ovdje su, naizgled, zagrade oko parametara nepotrebne.
Međutim, u primjeru
#define produkt(a,b) (a*b)
supstitucija u naredbi
r = produkt(x+1, y);
rezultira naredbom
r = x+1*y;
što je efektivno zbrajanje x i y.
Uz
#define produkt(a,b) ((a)*(b))
rezultat će biti ispravan:

128
Uvod u programiranje Funkcije

r = ((x+1)*(y));
Kako nije moguće znati u kojem će se kontekstu makro koristiti, najjednostavnije je
rutinski sve parametre i cijeli izrazi staviti u zagrade.
U definiciji makro narebe između naziva i otvorene zagrade kojom započinje popis
parametara ne smije stajati praznina, jer je praznina znak koji odvaja tekst koji se
supstituira od teksta kojim se supstituira.
#define produkt (a,b) ((a)*(b))
u naredbi
r = produkt(1,2);
rezultirat će supstitucijom teksta produkt s ostatkom naredbe #define tj. s
r = (a,b) ((a)*(b))(1,2);
što je sintaktička greška.
Nakon zagrade kojom se započinje lista parametara može se pojaviti praznina jer
prevodilac očekuje da listu parametara završi zatvorena zagrada. Zbog toga je prazninu
nakon zatvorene zagrade moguće, ali ne i uobičajeno, izostaviti.
Makro
#define produkt( a, b)a*b
supstituirat će se korektno, ali takvo pisanje svakako nije preporučljivo.
Kod makro naredbi koje koriste ternarni operator nad argumentom se ne bi smjeli
primjenjivati operatori uvećanja ili umanjenja za 1.
U sljedećem primjeru
#define abs(x) ((x) < 0 ? –(x) : (x))

a = 4;
r = abs(a++);
printf("%d %d\n“, r, a);
ispisat će se
5 6
jer se naredba s makrom razvija u
r = ((a++) < 0 ? –(a++) : (a++);
dok će se za
a = 4;
r = abs(++a);
printf("%d %d\n“, r, a);
ispisati
6 6
Sličan problem pojavio bi se i s max.
U ovim primjerima puno je učinkovitije i robusnije koristiti inline funkcije.

129
Uvod u programiranje Organizacija složenih programa

Organizacija slozenih programa


Složeniji programi današnjice sastoje se i od više milijuna linija programskog kôda. Jezgra
operacijskog sustava Linux, primjerice, u trenutku pisanja ovoga teksta imala je oko 20
milijuna linija, većinom pisanih u C-u. Bilo bi krajnje nepraktično sav taj programski kôd
čuvati u istoj datoteci, jer bi snalaženje u njoj bilo teško čak i uz mehanizme pretraživanja,
a dugo bi trajalo prevođenje, pa i učitavanje u uređivač teksta te spremanje promjena.
Svaki puta prevoditi dobro provjerene funkcije nepotrebno je trošenje računalnih
resursa. Zato se svi imalo složeniji programi rastavljaju na logičke cjeline, zvane
modulima, od kojih svaki sadrži jednu ili više funkcija. Moduli se prevode nezavisno i
povezuju u izvršni program povezivačem (linker).

Prototip funkcije
Ako se funkcije u nekom izvornom programu poredaju tako da je svaka definirana prije
korištenja, prevodilac je u svakome trenutku u mogućnosti provjeriti je li funkcija pozvana
na odgovarajući način, pa će za funkciju
int f(int a, int b) {

}
koja je pozvana na sljedeći način

r = f (1, 2, 3);
prevodilac dojaviti grešku jer broj argumenata u pozivu ne odgovara broju parametara.
Međutim, ako je funkcija definirana u nekom drugom modulu, prevodilac nije u
mogućnosti usporediti definiciju funkcije s pozivom. Zato je u modul u kojemu se funkcija
koristi potrebno ugraditi prototip funkcije.
Prototip je linija programskog kôda koja se sastoji samo od tipa rezultata funkcije, imena
funkcije te popisa parametara i njihovih tipova:
tip_rezultata naziv(tip_parametra1 parametar1, tip_parametra2 parametar2…);
Prototip funkcije naziva se još i deklaracijom funkcije.
Prototip neke funkcije mora se navesti prije funkcije koja tu funkciju poziva. Mogao bi se
upisati i izravno u modul koji poziva funkciju, primjerice
funkcije.c
int f1(int a, int b) {

}
float f2(float c) {

}
main.c
int f1(int a, int b);
int main(void) {

r = f1(x, y);
}

130
Uvod u programiranje Organizacija složenih programa

ali to nije dobra praksa. Kad bi se funkcija pozivala u više modula, tada bi svaka promjena
parametara funkcije zahtijevala promjenu prototipa u svakom od modula u kojemu je
upisan. Nadalje, u gornjem primjeru prevodilac ne može usporediti prototip sa stvarnom
definicijom funkcije.
Zato se prototipovi funkcija izdvajaju u datoteke zaglavlja (header files, .h) u pravilu tako
da se za svaki modul načini po jedna datoteka zaglavlja s prototipovima svih funkcija toga
modula, pa se takva datoteka naredbom #include uključuje u modul koji funkcije definira,
kao i u sve module koji te funkcije pozivaju:
funkcije.h
int f1(int a, int b);
float f2(float c);
funkcije.c
#include "funkcije.h"
int f1(int a, int b) {

}
float f2(float c) {

}
main.c
#include "funkcije.h"
int main(void) {

r = f1(x, y);
}
Sada je eventualnu promjenu parametara (osim, naravno, u konkretnim pozivima
funkcija) potrebno napraviti samo na dva mjesta: u definiciji funkcije i u zajedničkoj
deklaraciji).
Pojedinačno prevođenje prevodiocem gcc izvelo bi se naredbama
gcc -c funkcije.c
gcc -c main.c
a povezivanje naredbom
gcc -o main.exe funkcije.o main.o

131
Uvod u programiranje Doseg i vrijeme života varijabli

Doseg i vrijeme zivota varijabli


Varijable definirane u tijelu funkcije mogu se koristiti od mjesta definicije do završetka
bloka u kojem je varijabla definirana. Najčešće je to vitičasta zagrada koja završava
definiciju funkcije. Ovime je definiran njihov doseg ili vidljivost. Za takve se varijable
memorija rezervira na stogu, pa se zato prilikom sljedećeg poziva funkcije, osim ako u
međuvremenu nije pozvana niti jedna druga funkcija, ne može znati kakva će se vrijednost
zateći u takvim varijablama. Vrijeme života takve varijable, dakle, završava u trenutku
izlaska iz bloka u kojem je definirana.
Takve varijable nazivaju se još i lokalnim varijablama, jer su vidljive samo unutar bloka, a
također i dinamičkim varijablama, jer nisu čvrsto povezane s određenom adresom u
memoriji, a i vrijednost im se može promijeniti između dva poziva iste fukcije. S druge
strane varijable mogu biti globalne, što znači da su vidljive i iz više funkcija, te statičke, što
znači da će vrijednost u njima ostati sačuvana između dva poziva iste funkcije.
Varijabla može biti lokalna i u nekom bloku ograničenom vitičastim zagradama unutar
funkcije. To može biti tijelo neke naredbe za kontrolu programskog slijeda (if, while, for,
do) ali i nezavisno započeti blok. Sljedeći program
#include <stdio.h>
int main(void) {
int a; a
a = 1;
{
int a; a
a = 2;
printf("a=%d\n", a);
}
printf("a=%d\n", a);
}

Program 27. LokalneVarijableUnutarBloka.c

ispisuje
a=2
a=1
Varijabla a definirana na početku glavnog programa vidljiva je i traje do izlaska iz glavnog
programa. Međutim, definicija varijable istog imena u unutrašnjem bloku prekriva
definiciju iz vanjskog bloka, kako je simbolički prikazano prethodnom slikom.
Očigledno je da postojanje više varijabli istog imena unutar iste funkcije može izazvati
pomutnju, pa bi ga trebalo izbjegavati.
Izuzetak je, možda, definicija kontrolne varijable petlje u naredbi for, koja je samo
poseban slučaj definiranja varijable unutar bloka. Prihvatljivo je definirati više nezavisnih
kontrolnih varijabli istoga kratkog imena, kao u sljedećem primjeru
for (int i=1; i<=10;i++) {

}
for (int i=0; i<=100;i++) {

}

132
Uvod u programiranje Doseg i vrijeme života varijabli

ali, premda je potpuno jasan i dobro definiran doseg varijabli naziva i u sljedećem
primjerima
int i;
i = 10;
for (int i=10; i >= 0; i--) {

}
ili
for (int i = 0; i <= 10; i++) {

for (int i = 0; i <= 10; i++) {

}
}
takvo programiranje može manje vičnome programeru stvoriti zabunu pa ga zato treba
izbjegavati.
Doseg i vrijeme života varijable može se promijeniti oznakom smještajnog razreda
(storage class) u definiciji:
smještajni_razred tip ime;
Smještajni razredi su auto, register, static i extern. Ovi smještajni razredi i pozicija
definicije unutar programa određuju hoće li varijabla biti lokalna, globalna ili statička te
time određuju i dio memorije u kojem će varijabla biti smještena.
Memorija dodijeljena programu tipično se dijeli na 5 segmenata:

text data bss heap stack

Segment text sadrži izvršni kôd programa - instrukcije.


Segment data sadrži globalne i statičke varijable kojima je inicijalizacijom pridružena
vrijednost.
Segment bss (block started by symbol) sadrži globalne i statičke varijable kojima
inicijalizacijom nije pridružena vrijednost.
U segmentu heap (gomila) rezervira se memorija tijekom dinamičke rezervacije, kako je
to opisano u poglavlju Dinamička rezervacija memorije. Memorija se rezervira od nižih
prema višim adresama.
U segmentu stack (stog) rezervira se memorija za lokalne varijable. Stog raste prema
nižim adresama.
Pretpostavljeni smještajni razred je auto, pa je
auto int x;
isto što i
int x;
Varijable definirane unutar nekog bloka bez navođenja imena smještajnog razreda ili
navođenjem smještajnog razreda auto zato se kolokvijalno nazivaju još i automatskim
varijablama. Memorija za takve varijable rezervira se na stogu, kao je opisano u poglavlju
Mehanizam poziva funkcije.

133
Uvod u programiranje Doseg i vrijeme života varijabli

Globalne varijable
Varijabla definirana izvan tijela funkcije bez oznake smještajnog razreda globalna je na
razini cijeloga programa. Ona, međutim, smije tako biti definirana samo unutar jednoga
modula pa ako je u jednome modulu definirana varijabla x ovako:
modul1.c

int x;
void f1(int a) {

}
int f2(int b) {

}
tada je nije moguće definirati i u drugim modulima istoga programa, primjerice u
modul2.c

int x;
float f3(float c) {

}
double f4(int d) {

}
jer će se dogoditi greška prilikom povezivanja.
Varijabla mora biti definirana samo unutar jednoga modula, dok u svim ostalim modulima
smije biti samo deklarirana korištenjem smještajnog razreda extern, koji govori
prevodiocu da postoji varijabla određenog tipa i naziva, ali da je rezervacija memorije za
varijablu načinjena negdje drugdje.
modul1.c

int x;

modul2.c

extern int x;

modul3.c

extern int x;

Ako se globalnim varijablama u definiciji eksplicitno ne inicijalizira vrijednost, kao u
gornjem primjeru, prevodilac će ih inicijalizirati na nulu i smjestiti u segment bss. Ako se
globalnim varijablama u definiciji inicijalizira vrijednost, prevodilac će ih smjestiti u
segment data.
Ako se uz deklaraciji extern varijabla inicijalizira, smatra se da je riječ o definiciji.

134
Uvod u programiranje Doseg i vrijeme života varijabli

extern int x = 1;
isto je što i
int x = 1;
Globalna varijabla vidljiva je u svim funkcijama nekog modula definiranim ispod definicije
odnosno deklaracije globalne varijable, osim ako unutar funkcije nije definirana varijabla
istog imena, kao u sljedećem primjeru:

int a;
int f(int x) { a
a = 1;

}
float g(float y) {
int a; a
a = 2;

}
double h(double c) {
a = 4;

}

Slika 34. Doseg globalnih varijabli

Smještajni razred register


Smještajnim razredom register daje se naputak prevodiocu da po mogućnosti varijablu
zadrži koliko god je moguće u registru procesora, kako bi se uštedjelo vrijeme potrebno
za kopiranje podataka iz središnje memorije u procesor i natrag kod svake operacije koju
je nad varijablom potrebno obaviti. U većini će slučajeva ovu optimizaciju moderni
prevodioci obaviti i bez navođenja ovog smještajnog razreda.
Smještajni razred register smije se napisati samo unutar bloka, uključujući i u definiciji
parametara funkcije. Doseg i vrijeme života jednaki su kao i za varijable razreda auto.
Obično se koristi za varijable koje se intenzivno mijenjanju poput kontrolnih varijabli
petlji:

register int i;
for (i = 0; i < 10000; i++) {

Smještajni razred static


Smještajni razred static može se navesti uz varijablu definiranu unutar bloka, ali i uz
varijablu definiranu izvan bloka.
Ako je varijabla definirana unutar bloka, tada joj je doseg do vitičaste zagrade kojom
završava blok u kojem je definirana, ali izlaskom iz bloka ne završava vrijeme života takve
varijable. Ponovnim ulaskom u blok u varijabli će se zateći vrijednost koju je imala u
trenutku posljednjeg izlaska iz bloka. Takve varijable zovemo statičkim varijablama.
Prostor za statičke varijable rezervira se u segmentu data ako su u definiciji inicijalizirane,
odnosno u segmentu bss ako u definiciji nisu inicijalizirane. Zato je adresa statičke

135
Uvod u programiranje Doseg i vrijeme života varijabli

varijable ista za sve pozive funkcije u kojoj je ta statička varijabla definirana, bez obzira
na to odakle je i s koje razine funkcija pozvana. Također, prevodilac sve statičke varijable
koje nisu eksplicitno inicijaliziranje u definiciji tijekom prevođenja inicijalizira na nulu.
Tako se, primjerice, može realizirati brojač poziva neke funkcije:
int f(int x) {
static int brojac;
brojac++;

}
Prilikom svakog poziva ove funkcije stara vrijednost varijable brojac bit će uvećana za
jedan, pri čemu se i bez eksplicitne inicijalizacije podrazumijeva da je početna vrijednost
nula.
Ako je varijabla smještajnog razreda static definirana izvan tijela funkcije, bit će vidljiva
do kraja modula u kojem je definirana, osim ako u funkciji nije definirana neka druga
varijabla istoga imena. Prostor za takvu varijablu također se rezervira u segmentu data ili
bss, ovisno o tome je li inicijalizirana u definiciji.
Takva varijabla ne može se koristiti u drugim modulima, štoviše dozvoljeno je
modul1.c

static int x;
int f(int a) {

}

modul2.c

static int x;
int g(int b) {

}

U ovom slučaju postoje dvije nezavisne definicije i riječ je o dvije različite varijable.

136
Uvod u programiranje Funkcije iz standardne biblioteke

Funkcije iz standardne biblioteke


U programskom jeziku C može se riješiti gotovo svaki problem. Pretpostavimo da nam je
za neku svrhu potrebna funkcija sin x. sin x može se izračunati Maclaurinovim72 redom

(−1)𝑛 2𝑛+1
sin 𝑥 = ∑ 𝑥
(2𝑛 + 1)!
𝑛=0
Sumu ovog reda nije teško ostvariti u C-u. Funkcija koja računa faktorijele već je napisana,
a potenciranje nenegativnom cjelobrojnom potencijom može se jednostavno napisati kao
1, 𝑎𝑘𝑜 𝑗𝑒 𝑦 = 0
𝑦
𝑥𝑦 = {
∏ 𝑥 , 𝑎𝑘𝑜 𝑗𝑒 𝑦 > 0
𝑛=1
i napisati u C-u kao
double pot(double x, int y) {
double rez;
int n;
for (rez = 1, n = 1; n <= y; n++) {
rez *= x;
}
return rez;
}
Uz pomoć funkcije koja računa faktorijele i funkcije koja računa potenciju funkciju sinus
nije teško napisati. Jedini problem je odrediti koliko elemenata reda treba zbrojiti da bi se
dobila zadovoljavajuća preciznost. Članovi reda, zbog dijeljenja s faktorijelima, vrlo brzo
padaju, pa će zbog ograničene preciznosti realne varijable brzo postati toliko mali da
njihovo dodavanje neće imati utjecaj na vrijednost sume. Dakle, pamtit će se prethodna
vrijednost sume, pa ako dodavanjem sljedećeg člana reda vrijednost sume ostane
nepromijenjena u odnosu na prethodnu, može se prestati s dodavanjem.
double sinus(double x) {
int n = 0;
double suma = 0, prethodnaSuma = 0;
while (1) {
suma += pot(-1, n) / fakt(2*n + 1) * pot(x, 2*n + 1);
if (suma == prethodnaSuma) break;
prethodnaSuma = suma;
n++;
}
return suma;
}
Program 28. SinusRedomLose.c

Kod izračunavanja sinus(1), iteriranje prestaje u 10. koraku petlje.

72ColinMaclaurin (1.2.1698., Kilmodan - 14.6.1746., Edinburgh). škotski matematičar, po kojem je nazvan


posebni slučaj Taylorovog reda
Brook Taylor (18.8.1685., Edmonton – 29.12.1731., London), engleski matematičar koji je formalno uveo
princip izračunavanja funkcije kao sume beskonačnog reda

137
Uvod u programiranje Funkcije iz standardne biblioteke

Ovako napisana funkcija radi ispravno, ali jako neučinkovito. U svakom novom prolazu
petlje ispočetka se izračunavaju -1n, x2n+1 i (2n+1)!, a sve su to veličine koje se mogu
izračunati množenjem prethodne vrijednosti s nekim faktorom.
Ako se izraz

−1𝑛
sin 𝑥 = ∑ 𝑥 2𝑛+1
(2𝑛 + 1)!
𝑛=0
raspiše član po član dobije se
1 1 −1 3 1 −1 7
sin 𝑥 = 𝑥 + 𝑥 + 𝑥5 + 𝑥 +⋯
1! 3! 5! 7!
ili kraće
𝑥3 𝑥5 𝑥7
sin 𝑥 = 𝑥 − + − +⋯
3! 5! 7!
Ovdje se lako može uočiti da se sljedeći član reda brzo može izračunati množenjem
prethodnog s -x2 i dijeljenjem u prvom koraku s 2·3, u sljedećem s 4·5, zatim s 6·7 i tako
dalje. Sljedeća je izvedba neusporedivo učinkovitija, uz isti broj prolaza petlje:
double sinus(double x) {
int n;
double suma, prethodnaSuma, clan;
prethodnaSuma = suma = clan = x;
n = 2;
while (1) {
clan = -clan * x * x / (n * (n+1));
suma += clan;
if (suma == prethodnaSuma) break;
prethodnaSuma = suma;
n += 2;
}
return suma;
}
Program 29. SinusRedomDobro.c

Za neku trigonometrijsku primjenu, slično bi se moglo realizirati cos x, arcsin x, arccos x


itd.
Naravno, netko je već davno prije nas zatrebao i realizirao ove i mnoge druge funkcije, pa
ih se može pronaći u standardnoj biblioteci funkcija koja se isporučuje zajedno s
prevodiocem.
Detaljni opis svih funkcija iz standardne biblioteke prelazi okvire ove knjige i može ga se
lako naći na Internetu. Zato će ovdje biti obrađene samo značajnije od njih.

Matematičke funkcije
Slijedi popis češće korištenih funkcija s prototipovima i datotekama zaglavlja u kojima su
deklarirane.
Funkcije su implementirane u više inačica, s raznim tipovima parametara i odgovarajućim
tipovima rezultata, radi optimizacije. Funkcija s parametrima tipa long double koja vraća
long double funkcionirala bi za sve vrste argumenata, ali bi se pri pozivu argumenti koji
nisu long double implicitno pretvarali u long double. Rezultat funkcije tipa long double

138
Uvod u programiranje Funkcije iz standardne biblioteke

zahtijevao bi da se i ostali operandi u istome izrazu pretvore u long double, da bi se kasnije


rezultat cijeloga izraza vraćao u izvorni tip.
Inačice cjelobrojnih funkcija s prefiksima l i ll imaju parametre tipa long odnosno long
long, i vraćaju rezultat istoga tipa. Inačice realnih funkcija sa sufiksima f i l imaju
parametre tipa float odnosno long double, i vraćaju rezultat istoga tipa.
Prototip Inačice Značenje Deklaracija
int abs(int n); labs, Apsolutna <stdlib.h>
llabs vrijednost
double fabs(double arg); fabsf, -"- <math.h>
fabsl
double fmod(double x, double y); fmodf, Ostatak <math.h>
fmodl dijeljenja
realnih
brojeva73
double fmin(double x, double y); fminf, Manja od dvije <math.h>
fminl vrijednosti
double fmax(double x, double y); fmax, Veća od dvije <math.h>
fmaxl vrijednosti
double exp(double x); expf, ex <math.h>
expl
double exp2(double n); exp2f, 2n <math.h>
exp2l
double log(double x); logf, ln x <math.h>
logl
double log10(double x); log10f, log10 x <math.h>
log10l
double log2(double x); log2f, log2 x <math.h>
log2l
double sqrt(double x); sqrtf, √x <math.h>
sqrtl
double cbrt(double x); cbrtf, 3
√x <math.h>
cbrtl
double hypot(double x, double y); hypotf, √x 2 + y 2 <math.h>
hypotl
double pow(double x, double y); powf, xy <math.h>
powl
double sin(double x); sinf, sin x <math.h>
sinl
double cos(double x); cosf, cos x <math.h>
cosl
double tan(double x); tanf, tan x <math.h>
tanl

73 Ostatak dijeljenja realnih brojeva najbolje se može ilustrirati primjerom: fmod(1., 0.3) → 0.1

139
Uvod u programiranje Funkcije iz standardne biblioteke

double asin(double x); asinf, arcsin x <math.h>


asinl
double acos(double x); acosf, arccos x <math.h>
acosl
double atan(double x); atanf, arctan x <math.h>
atanl
double sinhh(double x); sinhf, sinh x <math.h>
sinhl
double cosh(double x); coshf, cosh x <math.h>
coshl
double tanh(double x); tanhf, tanh x <math.h>
tanhl
double asinh(double x); asinhf, arcsinh x <math.h>
asinhl
double acosh(double x); acoshf, arccosh x <math.h>
acoshl
double atanh(double x); atanf, arctan x <math.h>
atanl
double ceil(double x); ceilf, ⌈x⌉, najbliži <math.h>
ceill cijeli broj ≥ x
double floor(double x); floorf, ⌊x⌋, najbliži <math.h>
floorl cijeli broj ≤ x
double trunc(double x); truncf, Najbliži cijeli <math.h>
truncl broj po
apsolutnoj
vrijednosti ≤
|x|74
double round(double x); roundf, Zaokruživanje <math.h>
roundl na bliži cijeli
broj75

Funkcije nad znakovima


U datoteci <ctype.h> definirano je nekoliko korisnih funkcija koje rade sa znakovima76:
Prototip Značenje Primjeri
int toupper(int ch); Vraća odgovarajuće veliko slovo toupper('a') →
za argument koji je slovo. Ako 'A'

74 floor(-3.5) → -4, trunc(-3.5) → -3


75 Zaokruživanje na n decimalnih mjesta lako se dobije izrazom
round(x * pow(10, n)) / pow(10, n)
76Ponašanje ovih funkcija ovisi o lokalizacijskim postavkama zadanim pozivom funkcije setlocale.
Lokalizacija neće biti obrađena u ovoj knjizi. Ovdje će se opisati samo ponašanje uz pretpostavljene
postavke, setlocale(LC_ALL, "C");

140
Uvod u programiranje Funkcije iz standardne biblioteke

argument nije slovo, vraća toupper('B') →


vrijednost argumenta. 'B'
toupper('3') →
'3'
int tolower(int ch); Vraća odgovarajuće malo slovo za tolower('a') →
argument koji je slovo. Ako 'a'
argument nije slovo, vraća tolower('B') →
vrijednost argumenta. 'b'
tolower('3') →
'3'
int isdigit(int ch); Vraća 1 ako je argument isdigit('2') → 1
znamenka, inače 0 isdigit('A') → 0
int isalnum(int ch); Vraća 1 ako je argument isalnum('2') → 1
znamenka ili slovo, inače 0. isalnum('A') → 1
isalnum('.') → 0
int isprint(int ch); Vraća 1 ako se argument može isprint('A') → 1
ispisati, tj. nije neki od kontrolnih isprint('+') → 1
znakova, inače 0. Znakovi koji se
mogu ispisati su ASCII znakovi s isprint('\n') → 0
kôdom u intervalu [3210,12610]
odnosno [2016,7E16].
int iscntrl(int ch); Vraća 1 ako je argument neki od iscntrl('7') → 0
kontrolnih znakova, inače 0. iscntrl('\n') → 1
Kontrolni znakovi su ASCII
znakovi s kôdom u intervalu [0,
3110] i 127 odnosno [0, 1F16] i
7F16.
int isspace(int ch); Vraća 1 ako je argument isspace(' ') → 1
praznina, inače 0. isspace('?') → 0
int islower(int ch); Vraća 1 ako je argument malo islower('a') → 1
slovo, inače 0. islower('A') → 0
islower('7') → 0
int isupper(int ch); Vraća 1 ako je argument veliko isupper('a') → 0
slovo, inače 0. isupper('A') → 1
isupper('7') → 0
Pobrojane funkcije, kao i neke matematičke funkcije poput abs, naizgled bi se jednostavno
moglo ostvariti i makro naredbom npr.
#define toupper(ch) \
((ch) >= 'a' && (ch) <= 'z' ? (ch) – ('a' – 'A') : (ch))

141
Uvod u programiranje Funkcije iz standardne biblioteke

Da se podsjetimo, velika slova engleske abecede u ASCII kôdu započinju na 65, a mala na
97. Razlika je 32, ali prenosivije je to napisati kao ('a' – 'A'). Velika slova u mala pretvaraju
se dodavanjem, a mala u velika oduzimanjem ove razlike.
Ovako napisana makro naredbe radi dobro, osim ako je argument varijabla na kojoj se
primjenjuje operator uvećanja ili umanjenja. Tako bi se, uz makro toupper definiran kako
je gore opisano, u sljedećem primjeru
char x = 'a';
printf("%c\n", toupper(++x));
ispisalo D umjesto očekivanog B, jer se argument uveća tri puta tijekom evaluacije
ternarnog operatora.
Izvedba funkcijom77 radi dobro jer se u funkciju kopira uvećana vrijednost varijable x.

Izlazak iz programa
Ponekad je, u slučaju neke fatalne greške u programu, nastavak nemoguć. Ako se takva
greška dogodi na razini glavnog programa, naredbom return moguće je prekinuti rad
glavnog programa i vratiti se u operacijski sustav s nekim statusom različitim od 0.
Međutim, kako naredba return prekida izvođenje samo one funkcije u kojoj je izdana i
nastavlja s radom na prethodnoj razini, prekid cijelog programa s neke dublje razine
korištenjem samo naredbe return zahtijevao bi, nakon svakog povratka iz funkcije,
ispitivanje je li se tijekom izvođenja funkcije dogodila greška. Ako jest, trebalo bi se odmah
vratit na prethodnu razinu, npr.
int g(void) {

if (greška) return 1;

return 0;
}
int f(void) {

status = g();
if (status != 0) return status;

return 0;
}
int main(void) {

status = f();
if (status != 0) return status;

return 0;
}
Ovo je, naravno, nepraktično, pa je zato na raspolaganju funkcija

77Stvarna izvedba ovih funkcija ne koristi ispitivanje je li argument u nekom intervalu,


već to radi preko tablice, kako je to ilustrirano u poglavlju Jednodimenzijska polja fiksne
veličine

142
Uvod u programiranje Funkcije iz standardne biblioteke

void exit(int status);


deklarirana u <stdlib.h> koja trenutačno prekida rad programa i s proizvoljno duboke
razine pozivanja funkcija odmah se vraća u operacijski sustav. Argument status bit će
proslijeđen operacijskom sustavu na jednak način kao i status proslijeđen naredbom
return iz glavnog programa.
Povratak u operacijski sustav iz funkcije g() iz prethodnog programa tako se može riješiti
znatno jednostavnije:
void g(void) {

if (greška) exit(1);

return;
}
void f(void) {

g();

return;
}
int main(void) {

f();

return 0;
}

Pokretanje drugog programa


Ako se iz programa želi pokrenuti neki drugi program ili neka naredba operacijskog
sustava, na raspolaganju nam je funkcija system. Argument funkcije je tekst naredbe
operacijskog sustava, napisan onako kako bi ga se utipkalo u naredbeni prozor. Ako
naredbu nije moguće izvesti, primjerice zato što je navedeno ime nepostojećeg programa,
funkcija system vraća rezultat 1. Inače, funkcija čeka da pokrenuti program završi svoje
izvođenje i vraća status koji je pokrenuti program vratio naredbom return ili funkcijom
exit.
U primjeru u kojem je sljedeći program preveden u izvršni kôd koji je spremljen u
datoteku main1.exe:
int main(void) {
return 10;
}
program koji nalazi u istoj mapi operacijskog sustava na sljedeći će način izvesti taj
main1.exe
#include <stdio.h>
#include <stdlib.h>
int main(void) {
printf("%d\n", system("main1"));
return 0;
}
Ispisat će se
10

143
Uvod u programiranje Funkcije iz standardne biblioteke

Poziv funkcije system, dakle, može se usporediti s izravnim pozivom funkcije main iz
izvršnog programa koji je naveden kao argument funkcije system.
Ako pozvani program očekuje argumente, argumente toga programa treba jednostavno
ugraditi u tekst argumenta funkcije:

status = system("copy main.c main.bak");

Generiranje pseudoslučajnih brojeva


Računalne igre i simulacije redovito generiraju događaje koje se ne bi smjelo unaprijed
moći predvidjeti. Program koji će simulirati bacanje kocke morat će generirati neku od
brojaka 1 do 6 na nepredvidiv način. Slično je s programom koji simulira rulet ili neku
kartašku igru. Program koji simulira kretanje napona u nekoj električnoj mreži također
će morati generirati na slučajan način vrijednosti napona iz nekog očekivanog intervala.
U programskom jeziku C postoji elementarna funkcija
int rand(void);
koja vraća pseudoslučajni 78 broj iz intervala [0, RAND_MAX]. RAND_MAX je simbolička
konstanta definirana, kao i rand(), u <stdlib.h> i za većinu C prevodilaca iznosi 32767. To
znači da će za svaku primjenu trebati nekako prilagoditi rezultat funkcije rand() vlastitim
potrebama.
Ako je potrebno generirati male cijele brojeve, kao u slučaju bacanja kocke, dovoljan je i
operator %. Izraz
rand() % n
dat će rezultat u intervalu [0, n-1], pa će za slučaj bacanja kocke tome rezultatu trebati
dodati 1. Kod simulacije ruleta dodavanje jedinice neće biti potrebno, jer na ruletu postoji
i pretinac s oznakom 0:

kocka = rand() % 6 + 1;
rulet = rand() % 36;

Kod generiranja velikih cijelih brojeva ovo nije moguće napraviti. Ako je potrebno
generirati cijele brojeve u intervalu [1, 30000], tada ne bismo smjeli napisati
rezultat = rand() % 30000 + 1;
jer će brojevi od 1 do 2768 imati dvostruko veću vjerojatnost pojavljivanja. Generirat će
se kad je rezultat funkcije rand() između 0 i 2767 i kad je rezultat između 30000 i 32727.
Ostali će se brojevi generirati samo ako je rezultat funkcije rand() između 2768 i 29999.
Primjena operatora % nije moguća i kad se očekuje realni broj, primjerice ako se želi
generirati slučajna vrijednost napona između 4.5 i 5.5 V. U takvu svrhu potrebno je
načiniti transformaciju funkcijom poput
float randOdDo(float odg, float dog) {
return rand() / (float) RAND_MAX * (dog - odg) + odg;

78Broj se naziva pseudoslučajnim jer se ne dobije na slučajan način, već determinističkim algoritmom
opisanim kasnije, pri čemu se u nizu tako generiranih brojeva ne može prepoznati pravilnost. Generatori
stvarno slučajnih brojeva ostvaruju se sklopovljem koje kvantificira elektronički šum.

144
Uvod u programiranje Funkcije iz standardne biblioteke

}
gdje su odg i dog donja odnosno gornja granica željenog intervala. Parametre nije moguće
nazvati od i do jer je do ključna riječ.
Dijeljenjem s RAND_MAX interval [0, 32767] preslikava se jednoliko u interval [0, 1].
Množenjem s rasponom intervala rezultat se preslikava u [0, dog-odg]. Zatim se rezultatu
dodaje donja granica odg, pa je konačni rezultat u intervalu [odg, dog].
Prvi dio gornjeg postupka (dijeljenje i množenje) u računarstvu se naziva skaliranjem, a
drugi dio, dodavanje donje granice, translacijom.
Za cijele brojeve postupak treba malo modificirati jer bi gornja formula rezultirala vrlo
malom vjerojatnošću pojavljivanja gornje granice intervala, samo kad rand() vrati upravo
RAND_MAX. Kod skaliranja, rezultat treba dijeliti s RAND_MAX+1. i zatim množiti s
razlikom između donje i gornje granice uvećanom za 1. Ispravna funkcija koja generira
cijele brojeve u intervalu [odg, dog] je
int randOdDoInt(int odg, int dog) {
return rand() / (RAND_MAX + 1.) * (dog – odg + 1) + odg;
}
U primjeru bacanja kocke, odg=1 a dog=6, dijeljenje s RAND_MAX+1. rezultira intervalom
[0, 0.999969482421875]. Treba primijetiti da bi bez točke uz 1 u djelitelju rezultat uvijek
bio 0. Množenjem sa 6 – 1 + 1 = 6 dobije se interval [0, 5.99981689453125] a dodavanjem
donje granice koja iznosi 1 dobije se interval [1, 6.99981689453125]. Funkcija vraća int
pa se pri povratku obavlja implicitna konverzija u int odsijecanjem decimala. Rezultat je,
dakle, u intervalu [1, 6]. Intuitivno je jasno da će jedna šestina ishoda funkcije rand() biti
transformirana u 1, jedna šestina u 2 itd. Ovo se može i dokazati sljedećim programom u
kojemu će se za sve moguće ishode funkcije rand(), enumerirane petljom, izračunati
vrijednost dobivena gornjom transformacijom. Kad se pojavi nova vrijednost, ispisuje se
broj pojavljivanja prethodne vrijednosti i resetira brojač.
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int staro = 1, brojac = 0, i, novo, odg = 1, dog = 6;
for (i = 0; i <= RAND_MAX; i++) {
novo = i / (RAND_MAX + 1.) * (dog - odg + 1) + odg;
if (novo != staro) {
printf("%d %d\n", staro, brojac);
brojac = 0;
staro = novo;
}
brojac++;
}
printf("%d %d\n", staro, brojac);
return 0;
}
Program 30. RandTransformacija.c

Program će ispisati
1 5462
2 5461
3 5461
4 5462

145
Uvod u programiranje Funkcije iz standardne biblioteke

5 5461
6 5461
što je vrlo ravnomjerna distribucija, najbolje što se može dobiti jer 32768 ukupnih ishoda
funkcije rand() nije djeljivo sa 6.
Funkcija rand() implementirana je kao linearni kongruencijski 79 generator koji svaki
sljedeći pseudoslučajni broj izračunava iz prethodnog jednostavnom formulom
Xn+1 = (aXn + c) mod m
Iz ovoga je jasno da cijeli slijed brojeva ovisi o X0, koji se često naziva sjemenom, i da će se,
jednom kad Xn+1 bude jednako X0, cijeli slijed ponoviti, zauvijek. Konstante a, c i m moraju
biti odabrane tako da period ponavljanja bude što dulji, po mogućnosti jednak m. To je
moguće pod određenim uvjetima, od kojih je jedan uvjet da m i c nemaju zajednički
djelitelj veći od jedan.
Jednostavnim programom može se ispitati duljina perioda funkcije rand():
#include <stdio.h>
#include <stdlib.h>
int main (void) {
int brojac = 0, prvi, sljedeci;
prvi = rand();
while (1) {
sljedeci = rand();
brojac++;
if (sljedeci == prvi) break;
}
printf("%d %d\n", prvi, brojac);
return 0;
}
Program 31. RandPeriod.c

Ispisuje se
41 30545
Svako će, međutim, pokretanje programa ispisati iste vrijednosti, jer generator
pseudoslučajnih brojeva kreće od istog X0.
U praksi bi to značilo da će redoslijed vrijednosti bačene kocke uvijek biti isti, kao i da će
se ponavljati rasporedi karata, slijed pretinaca na ruletu ili kretanje napona u mreži.
Zato je prije korištenja generatora slučajnih brojeva potrebno pozvati funkciju
void srand(unsigned int sjeme);
koja u suštini postavlja vrijednost X0 na vrijednost sjeme. Ta bi vrijednost za svako
pokretanje programa trebala biti drukčija, pa je zato najprikladnije sjeme generatora
izvesti iz trenutačnog vremena. Najjednostavnije je iskoristiti funkciju time koja vraća broj

79Generator se naziva linearnim jer je formula za izračunavanje linearna, a kongruencijskim jer koristi
operator modulo. U matematici, dva su broja a i b kongruentna u odnosu na treći c ako podijeljena s njime
daju isti ostatak, tj. a mod c = b mod c, što se još piše kao a ≡ b (mod c).

146
Uvod u programiranje Funkcije iz standardne biblioteke

sekundi od početka 1. siječnja 1970. godine, mjereno po vremenu UTC 80 na sljedeći


način81:
#include <time.h>
srand(time(NULL));
Funkcija srand() dobar je primjer za korištenje statičkih globalnih varijabli, jer postavlja
vrijednost kasnije korištenu od neke druge funkcije, što se može vidjeti u jednoj od
mogućih stvarnih implementacija generatora slučajnih brojeva, u modulu rand.c
rand.c
#include <stdlib.h>
static unsigned long int rand_next = 1;
void srand(unsigned int seed) {
rand_next = seed;
}
int rand (){
rand_next = rand_next * 1103515245 + 12345;
return ((unsigned int)(rand_next / 65536) % 32768);
}
Pokušajmo sada, kao kompletan primjer, ostvariti igru „veće/manje“ (hi-lo game) u kojoj
računalo „zamisli“ broj iz zadanog intervala, a igrač nastoji pogoditi koji je to bio broj uz
najmanje pokušaja. Računalo pri tome pomaže tako da nakon svakog pokušaja igrača daje
informaciju je li traženi broj veći, manji ili jednak „zamišljenom“. Kad igrač pogodi broj,
igra završava i ispisuje se broj pokušaja.
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int randOdDoInt(int odg, int dog) {
return rand() / (RAND_MAX + 1.) * (dog - odg + 1) + odg;
}
int main (void) {
int brojac = 0, zamisljeni, broj, dg, gg;
srand(time(NULL)); // inicijalizacija generatora
printf("Unijeti donju i gornju granicu:");
do {
scanf("%d %d", &dg, &gg);
} while (dg >= gg); // učitavanje se ponavlja
// ako su nelogične granice
zamisljeni = randOdDoInt(dg, gg);
while (1) {
printf("Unijeti broj:");
scanf("%d", &broj);
brojac++;
if (broj == zamisljeni) {
break;

80UTC: Coordinated Universal Time je vrijeme na geografskoj širini 0°, koje se u engleskom govornom
području naziva još i GMT - Greenwich Mean Time
81 Prototip funkcije time je time_t time(time_t *t);
time_t je cjelobrojni tip definiran naredbom typedef koji nije striktno propisan standardom. Funkcija vraća
rezultat preko imena ali i, pozivom preko adrese, preko argumenta, što će biti objašnjeno u poglavlju
Promjena vrijednosti argumenata funkcije. Ako se kao argument preda pokazivač NULL, tada funkcija vraća
rezultat samo preko imena.

147
Uvod u programiranje Funkcije iz standardne biblioteke

} else if (broj < zamisljeni) {


printf("Broj je manji od zamišljenog\n");
} else {
printf("Broj je veći od zamišljenog\n");
}
}
printf("Zamišljeni broj pogođen je u %d pokušaja\n",
brojac);
return 0;
}
Program 32. HiLoGameIgrac.c

U ovoj igri najbolje je primijeniti strategiju raspolavljanja. Prvi pokušaj treba biti na
sredini zadanog intervala. Ako je zamišljeni broj veći od pokušaja, donja granica intervala
koji sljedeći treba raspoloviti postavlja se na vrijednost pokušaja uvećanu za 1. Ako je
zamišljeni broj manji, gornja granica intervala koji sljedeći treba raspoloviti postavlja se
na vrijednost pokušaja umanjenu za 1.
Zanimljivo je realizirati ovu strategiju u inačici igre u kojoj igrač zamišlja broj, a računalo
pogađa. Igrač na svaki pokušaj računala odgovara s >, < ili =. Ako se uz gore opisanu
strategiju postigne da je donja granica intervala koji sljedeći treba raspoloviti veća od
gornje, igrač je sigurno u nekom trenutku dao neispravan odgovor.
#include <stdio.h>
int main (void) {
int brojac = 0, broj, dg, gg;
char odgovor;
printf("Unijeti donju i gornju granicu:");
do {
scanf("%d %d", &dg, &gg);
} while (dg >= gg);
while (dg <= gg) {
broj = (dg + gg) / 2;
brojac++;
printf("%d\n", broj);
scanf("%c ", &odgovor);
if (odgovor == '=') {
break;
} else if (odgovor == '<') {
dg = broj + 1;
} else {
gg = broj - 1;
}
}
if (dg <= gg) {
printf("Zamišljeni broj pogođen je u %d pokušaja\n",
brojac);
} else {
printf("Odgovori nisu bili ispravni\n");
}
return 0;
}
Program 33. HiLoGameRacunalo.c

148
Uvod u programiranje Funkcije iz standardne biblioteke

Praznina u formatu kojim se učitava odgovor bit će objašnjena u poglavlju Formatirani


ulaz i izlaz.

149
Uvod u programiranje Pokazivači

Pokazivaci
Varijable kakve smo do sada koristili uvijek su sadržavale neke konkretne podatke
potrebne za ispravan rad programa – brojače, sume, znakove itd. Za svaku takvu varijablu
rezervira se dio memorije na nekoj poziciji – adresi. Pokazivači (pointers) su također
varijable, ali varijable koje u sebi sadrže adresu neke druge varijable. Korištenjem
pokazivača bit će moguće indirektno pročitati ili postaviti vrijednost drugih varijabli.
Pokazivač se definira tako da se između imena i tipa varijable upiše znak *, koji u tome
kontekstu znači „pokazivač“.
Tako se u primjeru
int v;
int *p;
definiraju cjelobrojna varijabla imena v i pokazivač na cjelobrojnu varijablu koji se zove
p.
Time ni v ni p nisu inicijalizirani: za njih se rezervira memorija negdje na stogu, na
memorijskim adresama koje su nekad zauzimale neke druge varijable. U konkretnom
slučaju i jedna i druga varijabla zauzet će po 4 bajta: v zato što je tipa int, a p zato što mora
pohraniti adresu. Adresa varijable je redni broj prvoga bajta te varijable i može biti koja
pozicija unutar memorije. Zato su adresa podatka tipa char i adresa podatka tipa double
iste veličine. Rezultat gornje definicije je sljedeći:
p v
… ? ? …
6422312 6422316
Na adresi 6422312 rezerviran je prostor za p, a na adresi 6422316 prostor za v. Ovo se
simbolički može prikazati i sljedećom slikom:

?
p v
?

v ima nepoznatu vrijednost, a p pokazuje u nepoznato. Nakon što se obavi naredba


v = 7;
?
situacija je sljedeća:
p v
p v
… ? 7 … ili 7
6422312 6422316

Adresa neke varijable može se saznati primjenom unarnog adresnog operatora &.
Sljedećom naredbom ispisuje se adresa varijable v, prvo konverzijskom specifikacijom za
ispis cijelih brojeva bez predznaka %u, jer je adresa po tipu podatka nenegativan cijeli
broj, a potom konverzijskom specifikacijom za ispis adrese %p, koja adresu ispisuje u
heksadekadskom obliku:
printf("%u %p\n", &v, &v);

150
Uvod u programiranje Pokazivači

ispisat će
6422316 0061ff2c
Pokazivaču p može se primjenom unarnog adresnog operatora & pridijeliti vrijednost
adrese varijable v:
p = &v;
što će rezultirati pokazivanjem varijable p na varijablu v:
p V p v
ili 7
… 6422316 7 …
6422312 6422316
Adresni operator može se primijeniti nad bilo kojom varijablom, osim varijabli definiranih
smještajnim razredom register.
Nad pokazivačem se može primijeniti unarni operator dereferenciranja82 *, koji se može
naći s lijeve strane operatora pridruživanja, tj. biti l-value:
*p = 8;
Ovu naredbu možemo čitati kao „na adresu pohranjenu u pokazivaču p upiši vrijednost 8“
ili, više kolokvijalno, „tamo kamo pokazuje pokazivač p upiši vrijednost 8“. U pokazivaču
je pohranjena adresa 6422316 pa na adresa 6422316 treba upisati 8 (lijeva slika).
Alternativno, p pokazuje na v (desna slika), pa izmjenu treba načiniti tamo kamo pokazuje
p, tj. u varijabli v. Rezultat je
p V p v
… 6422316 8 … ili 8
6422312 6422316

Operator dereferenciranja može se pojaviti i s desne strane operatora pridruživanja, kao


dio izraza (r-value).
U sljedećem primjeru uzima se vrijednost s adrese sadržane u pokazivaču p, vrijednost se
uvećava za 1 i pohranjuje na adresu sadržanu u pokazivaču p.
*p = *p + 1;
Posljedica je
p v p v
… 6422316 9 … ili 9
6422312 6422316

Sada će naredba u kojoj se ispisuje vrijednost pokazivača (adresu pohranjenu u


pokazivaču) i vrijednost s adrese pohranjene u pokazivaču (varijable na koju pokazuje
pokazivač)
printf("%u %d\n", p, *p);
ispisati
6422316 9

82Adresa se još zove i referencom, pa se glagol dereferencirati koristi u kontekstu „doći do sadržaja preko
reference (adrese)“

151
Uvod u programiranje Pokazivači

p je, dakle, pokazivač, a *p varijabla na koju pokazivač pokazuje. To se jasno može pročitati
i iz same definicije
int *p;
Ako se definicija vizualno rastavi na
int * p;
jasno se vidi da je p tipa int *, dakle pokazivač na int.
Ako se ta ista definicija vizualno rastavi na
int *p;
iz nje se može pročitati da je *p tipa int, dakle varijabla tipa int.
Dereferenciranje pokazivača koji nisu inicijalizirano nije dozvoljeno, iako prevodilac neće
javiti grešku. Sljedeći program
int *p;
*p = 7;
imat će nedefinirani ishod. Ako kombinacija bitova zatečena na dijelu stoga koji je pripao
varijabli p tvori adresu izvan adresnog prostora programa, dogodit će se greška kod
izvođenja, obično greška segmetacije (Segmentation fault) ili povreda prava pristupa
(Access violation). Međutim, ako ta kombinacija bitova tvori adresu unutar adresnog
prostora programa, posljedica je mnogo neugodnija: promijenit će se sadržaj neke (ili
nekih!) varijabli u programu, što će biti dosta teško detektirati.

Tipovi pokazivača
Iako pokazivači na varijable svih tipova imaju veličinu od četiri bajta (adresa podatka tipa
char ni po čemu se ne razlikuje od adrese podatka tipa double, premda su podaci na toj
adresi drastično različiti, i po veličini i po načinu prikaza), nije ih moguće definirati na
zajednički način. Pokazivač mora u definiciji imati oznaku tipa na koji pokazuje, jer se
inače vrijednost na adresi pohranjenoj u pokazivaču ne bi mogla dereferencirati. Adresa
6422316, jedina informacija pohranjena u pokazivaču p, specificira samo gdje podatak
počinje, ali ne i koliko bajtova pri dereferenciranju s te adrese treba uzeti niti kako ih
interpretirati. Upravo tip specificiran u definiciji (int *) govori prevodiocu da s adrese
6422316 uzme četiri bajta i interpretira njihov sadržaj kao potencije broja 2.
Moguće je, međutim, definirati generički pokazivač, pokazivač na bilo što, navođenjem
ključne riječi void u definiciji, pa mu pridružiti bilo koju adresu:
void *r;
int v = 7;
r = &v;
Takav se pokazivač neće moći dereferencirati. Pokušaj dereferenciranja npr.
printf("%d\n", *r);
rezultirat će greškom kod prevođenja jer sama adresa nije dostatna za dereferenciranje.
Pokazivač tipa void obvezno se prije dereferenciranja mora operatorom pretvorbe tipa
(cast operator) pretvoriti u neki konkretni tip, bilo da se koristi kao l-value
*(int *)r = 8;
ili r-value
printf("%d\n“, *(int *)r);

152
Uvod u programiranje Pokazivači

Prilikom pretvorbe pokazivača potreban je oprez. Sljedeći odsječak


int v;
void *r;
v = 7;
r = &v;
v = *(float *)r + 1;
printf("%d\n", v);
umjesto možda očekivane vrijednosti 8, ispisat će 1 jer je u varijabli v pohranjena
kombinacija bitova
00000000 00000000 00000000 00000111
koja se dereferencira kao realni broj. Taj broj ima karakteristiku 0 i mantisu različitu od
nule, što znači da je po IEEE 754 standardu riječ o denormaliziranom broju s potencijom
2-126, u ovom slučaju 0. 00000000000000000000111 · 2-126 ≈ 1.4013 · 10-45. To je vrlo
blizu nuli, pa se dodavanjem jedinice i zaokruživanjem na cijeli broj kao rezultat dobije
točno 1.

Aritmetika s pokazivačima
Ako pretpostavimo da sljedeći programski odsječak
int a, b, *c;
c = &b;
rezultira sljedećom rezervacijom memorije i, posljedično, postavljanjem odgovarajuće
vrijednosti varijable c:
c b a
… 6422316 …
6422312 6422316 6422320
dereferenciranjem pokazivača c dobit ćemo vrijednost varijable b.
Pretpostavimo da se adresa zapisana u c nekako poveća za 1, na 6422317.
Dereferenciranje takvog pokazivača vratilo bi sadržaj četiriju bajtova koji započinju na
adresi 6422317, tj. posljednja tri bajta varijable b i prvoga bajta varijable a:
c b 6422317 a
… 6422317 …
6422312 6422316 6422320
Tako nešto nikada nema smisla.
Zato se uvećanjem pokazivača za jedan adresa u njemu pohranjena uvijek povećava za
veličinu podatka na koji pokazivač pokazuje.
U primjeru,
int a, b, *c;
c = &b;
c = c + 1;
adresa u 6422316 uvećat će se za 4 (1 * sizeof (int)) pa će iznositi 6422320
c b a
… 6422320 …
6422312 6422316 6422320
čime će se zapravo ostvariti pokazivanje na varijablu a, pa će sada

153
Uvod u programiranje Pokazivači

*c = 1;
upisati vrijednost 1 u varijablu a.
Pokazivače na pojedine varijable nikad se neće uvećavati. U ovom konkretnom slučaju u
memoriji se slučajno iza podatka tipa int doista nalazi još jedan podatak tipa int, pa će
dereferenciranje raditi korektno. Unaprijed se ne može znati što će se doista iza neke
varijable smjestiti, posebno ako se program želi načiniti prenosivim na druge prevodioce
koji možda rezerviraju memoriju na drukčiji način. U općenitom slučaju iza varijable b
mogla je, primjerice, započeti neka varijabla tipa double, u kojem bi slučaju naredba
*c = 1;
postavila prva četiri bajta te varijable na 00000000 00000000 00000000 000000012, s
odgovarajućim (besmislenim) rezultatom.
Postoji, međutim, slučaj kada možemo biti sigurni da iza podatka nekoga tipa postoji
sljedeći podatak takvoga istog tipa. Takva struktura podataka naziva se poljem i obrađena
je u posebnom poglavlju Polja. Aritmetika s pokazivačima jedino u takvom slučaju doista
ima smisla.
Općenito, u slučaju
tip *p;

p = p + n;
adresa pohranjena u p uvećava se za n*sizeof(tip).
Adresu je moguće i umanjiti za određeni iznos n, pri čemu se adresa u
pokazivaču stvarno umanjuje za n*sizeof(tip).
Zbrajanje, množenje i dijeljenje pokazivača besmislene su i nedozvoljene operacije, ali
oduzimanje pokazivača istoga tipa rezultira udaljenošću između dvije memorijske
lokacije izraženom brojem podataka onoga tipa na koji pokazivači pokazuju. Tako se u
primjeru
int a, b;
int *pa = &a, *pb = &b;
printf("%u %u %d", pa, pb, pa - pb);
ispisuje
6422308 6422304 1
jer su a i b, kao što se iz adresa može vidjeti, u memoriji susjedne, pa su njihovi početci
međusobno udaljeni za veličinu jednog podatka tipa int.
Oduzimanje pokazivača različitog tipa nije dozvoljeno, pa ako je potrebno izračunati
udaljenosti između dva podatka različitih (ili istih) tipova izražene u bajtovima,
pokazivače je operatorom pretvorbe tipa prvo potrebno pretvoriti u char *:
Tako će
char c;
int a;
int *pa = &a;
char *pc = &c;
printf("%u %u %d", pa, pc, (char *) pa - (char *) pc);
ispisati
6422304 6422311 -7

154
Uvod u programiranje Pokazivači

Uz određeni oprez, operator pretvorbe pokazivača može se iskoristi za uvid u


unutrašnjost varijable. Sljedeći program prikazat će jesu li varijabli u memoriji smještene
na način Big Endian (najznačajniji bajt prvo, MSB first) ili Little Endian (najmanje značajan
bajt prvo, LSB first):
#include <stdio.h>
int main(void) {
int a = 263;
int *pa = &a;
printf("%d %d %d %d\n", *(char *)pa,
*((char *)pa + 1),
*((char *)pa + 2),
*((char *)pa + 3));
return 0;
}
Program 34. PokazivacEndian.c

263 = 256 + 7 pa varijabla a ima binarnu sliku


00000000 000000000 00000001 00000111
Ispisuje se
7 1 0 0
iz čega je očigledno da je najmanje značajan bajt na najnižoj adresi i da je u pitanju Little
Endian.
U ovom primjeru nije bilo nužno uvesti pokazivač pa. Sljedeći program radi potpuno isto:
int a = 263;
printf("%d %d %d %d\n", *(char *)&a,
*((char *)&a + 1),
*((char *)&a + 2),
*((char *)&a + 3));
Naravno, pretvorbom tipa pokazivača moguće je (ali ne i preporučljivo), intervenirati nad
dijelom varijable, pa će tako, nadovezujući se na prethodni kod,
*((char *)&a + 2) = 3;
stvoriti sljedeću binarnu sliku u a
00000000 000000011 00000001 00000111
odnosno postaviti vrijednost varijable a na 196871. Ovim se samo htjelo pokazati da je
neopreznim radom s pokazivačima moguće postići neočekivane ili neželjene rezultate.
Sve dok adresa upisana u pokazivaču pripada dijelu memorije rezerviranom za program,
indirektna promjena preko pokazivača bespogovorno će se obaviti rezultirajući tako
semantičkom greškom koju je vrlo teško otkriti.
U poglavlju Programska petlja s poznatim brojem ponavljanja bilo je prikazano kako uz
pomoć operatora nad bitovima ispisati binarni sadržaj neke cjelobrojne varijable. Sadržaj
realnih varijabli tako nije moguće ispisati jer se nad njima ne mogu primijeniti operatori
posmaka. Može se, međutim, realni broj rastaviti na bajtove pa ispisivati sadržaj svakog
pojedinog bajta, vodeći računa o činjenici da je i ovdje korišten Little Endian, pa je zadnjih
osam bitova mantise pohranjeno na najnižoj adresi, predzadnjih osam na prethodnoj i
tako dalje. Zato je prvo potrebno ispisati bajt koji se nalazi na najvišoj adresi.
#include <stdio.h>
#include <limits.h>

155
Uvod u programiranje Pokazivači

int main(void) {
double a;
int i, j;
char *p;
while(1) {
scanf("%lf", &a);
if (a == 0) break;
p = (char *)&a + sizeof(a) - 1; // adresa posljednjeg bajta
for (i = sizeof(a); i > 0; i--, p--) {// po svim bajtovima
for (j = CHAR_BIT - 1; j >= 0; j--) { // po bitovima u bajtu
printf("%d", (*p >> j) & 1);
}
printf(" ");
}
printf("\n");
}
return 0;
}
Program 35. BitoviPoBajtovima.c

Za ulaz 12.5, na primjer, rezultat će biti


01000000 00101001 00000000 00000000 00000000 00000000 00000000 00000000

Pokazivači na pokazivače
Pokazivač je varijabla, pa je adresnim operatorom moguće dobiti i adresu pokazivača kao
i adresu bilo koje druge varijable. Kako je adresa neke varijable zapravo pokazivač na tu
varijablu, tako je i adresa pokazivača onda pokazivač na pokazivač i definira se kao
tip **ime;
Ako sljedeće naredbe:
int a;
int *p;
int **r;
rezerviraju memoriju na sljedeći način
r p a
… …
6422312 6422316 6422320
tada će
p = &a;
r = &p;
rezultirati sljedećim sadržajem varijabli p i r:
r p a
… 6422320 6422320 …
6422312 6422316 6422320
što se može simbolički prikazati kao
r p a
?

156
Uvod u programiranje Pokazivači

Kao i prije, definicija


int **r;
može se vizualno rastaviti na
int ** r;
iz čega se može pročitati da je r tipa int ** tj. pokazivač na pokazivač na int,
na
int * *r;
iz čega je *r tipa int * ili pokazivač na int
odnosno na
int **r;
iz čega se vidi da je **r tipa int.
Varijabla a se, dakle, slučaju može dereferencirati preko pokazivača r kao **r, npr
**r = 7;
ili
v = **r;
Slična logika vrijedi i za više razina indirekcije, npr.
char ****s;
Broj razina nije ograničen, ali primjene pokazivača s više od dvije razine indirekcije nisu
česte.

Pokazivač NULL
U <stdio.h> definirana je simbolička konstanta
#define NULL 0
koja se konvencionalno koristi za inicijalizaciju pokazivača koji ne pokazuju na neku
konkretnu varijablu.
Kao što je već rečeno, definicija lokalnog pokazivača, kao ni bilo koje druge lokalne
varijable, neće inicijalizirati njegov sadržaj, nego će se u njemu zateći vrijednost koja
odgovara kombinaciji bitova zatečenih na stogu. Tu je kombinaciju nemoguće razlikovati
od stvarne adrese. Ako se, međutim, nakon inicijalizacije ili u samoj inicijalizaciji
pokazivač inicijalizira na NULL, lako će se prepoznati da taj pokazivač ne pokazuje na ništa
konkretno, jer adresa 0 sigurno nije dio programa. Vrlo niske adrese kod svakog su
računala rezervirane za posebne namjene.

Promjena vrijednosti argumenata funkcije


Pokušajmo sada napisati funkciju koja bi trebala zamijeniti vrijednosti dviju varijabli, tj.
prvi argument funkcije postaviti na vrijednost drugog argumenta funkcije i obratno.
Sljedeći program
#include <stdio.h>
void zamijeni(int a, int b) {
int pom;
pom = a; // 3
a = b; // 4

157
Uvod u programiranje Pokazivači

b = pom; // 5
return; // 6
}
int main (void) {
int a=3, b=5; // 1
zamijeni(a, b); // 2
printf("%d %d\n", a, b);
return 0;
}
Program 36. ZamjenaFunkcijomNeispravno.c

ispisat će
3 5
iako se, naizgled, zamjena obavila ispravno.
Promotrimo kako izgleda sistemski stog nakon izvođenja svake pojedine naredbe iz
prethodnog programa, numerirane u komentarima, uzevši u obzir činjenicu da je i main
funkcija pozvana iz operacijskog sustava:
1 2 3 4 5 6
pom ? pom 3 pom 3 pom 3
p.a. p.a. p.a. p.a.
a 3 a 3 a 5 a 5
b 5 b 5 b 5 b 3
a 3 a 3 a 3 a 3 a 3 a 3
b b 5 b 5 b 5 b 5 b 5
5
p.a. p.a. p.a. p.a. p.a. p.a.

Slika 35. Stanje stoga kod neispravnog pokušaja zamjene funkcijom

Sve izmjene napravljene su unutar okvira stoga koji pripada funkciji, tako da su varijable
a i b glavnog programa ostale nepromijenjene.
Zbog činjenice da se pri pozivu funkcije vrijednost argumenta uvijek kopira na stog, sam
argument nikako nije moguće promijeniti.
Moguće je, međutim, umjesto vrijednosti varijable, funkciji predati adresu te varijable, pa
u funkciji varijablu promijeniti indirektno, preko pokazivača:
#include <stdio.h>
void zamijeni(int *a, int *b) {
int pom;
pom = *a; // 3
*a = *b; // 4
*b = pom; // 5
return; // 6
}
int main (void) {
int a=3, b=5; // 1
zamijeni(&a, &b); // 2
printf("%d %d\n", a, b);
return 0;

158
Uvod u programiranje Pokazivači

}
Program 37. ZamjenaFunkcijomIspravno.c

Ispisat će se
5 3
jer je, uz pretpostavku da se a nalazi na adresi 12344 a b na adresi 12340, situacija na
stogu tijekom izvođenja sada sljedeća:
1 2 3 4 5 6
pom ? pom 3 pom 3 pom 3
p.a. p.a. p.a. p.a.
a 12344 a 12344 a 12344 a 12344
b 12340 b 12340 b 12340 b 12340
5 5 5 5
a 3 a 3 a 3 a a a
b b 5 b 5 b 5 b 3 b 5
5
p.a. p.a. p.a. p.a. p.a. p.a.

Slika 36. Stanje stoga kod ispravne zamjene funkcijom

Drukčije prikazano, umjesto slike koja logički odgovara prvom programu:

main a b zamijeni a b pom


3 5 3 5

varijable i njihov doseg u drugom programu mogu se prikazati ovako:

main a b zamijeni a b pom


3 5

Promjena varijabli u glavnom programu obavlja se dereferenciranjem pokazivača u


funkciji. Ovakav poziv često se naziva pozivom preko adrese (call by reference) iako treba
primijetiti da je tehnički i ovdje u pitanju poziv preko vrijednosti, samo što je vrijednost
predana funkciji, umjesto vrijednosti same varijable, sada vrijednost adrese te varijable.
Sada je jasno zašto je u pozivu do sada mnogo puta korištene funkcije scanf uz svaku
varijablu bilo potrebno napisati adresni operator &: scanf je funkcija koja učitava podatke
s tipkovnice, prema konverzijskim specifikacijama pretvara ih u odgovarajući binarni
zapis i taj binarni zapis upisuje na adrese varijabli koje su funkciji predane kao argumenti.

159
Uvod u programiranje Polja

Polja
Pokušajmo riješiti sljedeći problem. S tipkovnice se unose redom ocjene dobivene na
predmetu Uvod u programiranje. Kad se unese ocjena 0, prestaje se s unosom i ispisuje
redom broj pojavljivanja svake pojedine ocjene.
Na primjer, za ulazne podatke
1 2 3 2 3 4 3 2 5 4 3 2 1 0
ispis treba izgledati
Broj ocjena 1: 2
Broj ocjena 2: 4
Broj ocjena 3: 4
Broj ocjena 4: 2
Broj ocjena 5: 1
Drugim riječima, potrebno je ispisati frekvencije pojavljivanja pojedinih ocjena. S
dosadašnjim znanjem programiranja, taj bi se problem mogao riješiti uvođenjem 5
brojača:
#include <stdio.h>
int main(void) {
int br1 = 0, br2 = 0, br3 = 0, br4 = 0, br5 = 0;
int oc;
while (1) {
scanf("%d", &oc);
if (oc == 0) break;
switch (oc) {
case 1:
br1++;
break;
case 2:
br2++;
break;
case 3:
br3++;
break;
case 4:
br4++;
break;
case 5:
br5++;
break;
default:
printf("Neispravna ocjena: %d", oc);
}
}
printf("Broj ocjena 1: %d\n", br1);
printf("Broj ocjena 2: %d\n", br2);
printf("Broj ocjena 3: %d\n", br3);
printf("Broj ocjena 4: %d\n", br4);
printf("Broj ocjena 5: %d\n", br5);
return 0;
}
Program 38. FrekvencijeBezPolja.c

160
Uvod u programiranje Polja

Program je sasvim funkcionalan, ali dug i s mnogo ponavljajućeg kôda. Pokušajmo sada
zamisliti kako bi ovaj program izgledao da se na predmetu Uvod u programiranje uvede
vrlo precizno ocjenjivanje ocjenama 1 do 1000, a da zadatak ostane isti, odrediti
frekvenciju pojavljivanja svake pojedine ocjene: 1000 varijabli, 1000 grana skretnice,
1000 linija za ispis.
U matematiku je davno uveden pojam vektora kao uređene n-torke skalara
a = (a1, a2, a3, …, an)
gdje se i-ti član n-torke može referencirati izrazom ai.
Kad bi u programskom jeziku C postojao tip podatka vektor i takva notacija, brojanje
frekvencija ocjena moglo bi izgledati ovako:
vektor br;
int oc;
for (oc = 1; oc <= 5; oc++) broc = 0;
while (1) {
scanf("%d", &oc);
if (oc == 0) break;
else if (oc > 5) printf("Neispravna ocjena %d\n", oc);
else broc++;
}
for (oc = 1; oc <= 5; oc++) {
printf("Broj ocjena %d: %d\n", broc);
}
Takav tip podatka i takva notacija ne postoje, ali postoji koncept polja83 koji u suštini ima
jednaku funkcionalnost.
Polje je struktura podataka koja se sastoji od određenog broja podataka jednakog tipa,
pohranjenih u memoriji računala jedan za drugim.
Polja mogu biti jednodimenzijska (što odgovara matematičkom pojmu vektora),
dvodimenzijska (što odgovara matematičkom pojmu matrice), trodimenzijska i tako dalje.
Broj dimenzija polja nije ograničen.
Noviji standardi C-a poznaju polja fiksne veličine i polja varijabilne veličine.

Jednodimenzijska polja fiksne veličine


Jednodimenzijsko polje fiksne veličine definira se navođenjem tipa, imena i konstantnim
izrazom u uglatim zagradama koji određuje veličinu polja, kao u sljedećem primjeru:
int br[5];
Memorijski prostor za takvo polje rezervira se na stogu tijekom prevođenja, pa treba
paziti na veličinu. Pretpostavljena veličina stoga za program kod većine prevodilaca je
reda veličine megabajta, što je manje od memorije potrebne za npr.
double p[200000];

83 Polje je relativno neprikladan, ali u hrvatskom računarstvu već dulje vrijeme prihvaćen prijevod engleske
riječi array (red, poredak, stroj). Kako je red (ispravno) prihvaćen prijevod za termin queue, bolje bi bilo
umjesto polje koristiti termin niz ili poredak, čime bi se oslobodio termin za prijevod u računarstvu često
korištenog termina field.

161
Uvod u programiranje Polja

Prevođenje programa koji definira lokalne varijable koje bi prepunile stog uspijeva, ali se
zato javlja greška kod izvođenja. Stog se za program može povećati navođenjem
odgovarajuće opcije prevodioca. Za gcc je to –wl,--stack,veličina_stoga_u_bajtovima
Naredbom
int br[5];
definirano je polje pod nazivom br od ukupno 5 podataka tipa int koji će u memoriju biti
pohranjeni jedan iza drugoga. Pojedinačni elementi polja referenciraju se cjelobrojnim
indeksnim izrazom – indeksom u uglatim zagradama, pri čemu se prvi od n članova polja
referencira indeksnim izrazom koji ima vrijednost 0, a posljednji indeksnim izrazom koji
ima vrijednost n-1. Tako definirano polje u memoriju se pohranjuje na sljedeći način, pri
čemu jedan manji pravokutnik odgovara jednom bajtu memorije:
br[0] br[1] br[2] br[3] br[4]
… …
6422300 6422304 6422308 6422312 6422316
Sada je, uz korištenje polja, moguće napisati mnogo kraće rješenje, u kojem će se iskoristiti
i mogućnost inicijalizacije vrijednosti polja prilikom definicije. Općenito, elementi bilo
kojeg polja mogu se postaviti na početne vrijednosti kao u sljedećem primjeru
int p[3] = {1, 2, 3};
Ovdje je p[0] postavljen na vrijednost 0, p[1] na vrijednost 2, a p[2] na vrijednost 3.
Polju koje se ovako inicijalizira nije potrebno navoditi veličinu. Jednaku funkcionalnost
dobili bismo definicijom
int p[] = {1, 2, 3};
jer prevodilac iz broja inicijalizatora određuje veličinu polja.
Veličina polja u bajtovima može se saznati primjenom operatora sizeof pa će tako za polje
iz prethodnog primjera
printf("%d\n", sizeof(p));
ispisati 12, a dijeljenje ove informacije s veličinom pojedinačnog elementa dati broj
elemenata polja. Naredba
printf("%d\n", sizeof(p)/sizeof(p[0]));
ispisuje 3.
Broj inicijalizatora može biti manji od definirane veličine polja. U sljedećem primjeru
float f[300] = {1, 2};
samo se prvi i drugi element postavljaju na 1 odnosno 2, ali se preostalih 298 elemenata
inicijalizira na 0.
Treba primijetiti da elementi polja koje nije inicijalizirano u definiciji imaju nedefinirane
vrijednosti (kombinacije bitova zatečene od prije na adresama elemenata polja). Ako
postoji inicijalizacija, mora biti zadana bar jedna vrijednost. Broj inicijalizatora ne smije
premašiti veličinu polja, pa naredbe
int p[3] = {};
ili
int p[3] = {1, 2, 3, 4};
rezultiraju greškom kod prevođenja

162
Uvod u programiranje Polja

Ovo svojstvo jezika elegantno se može iskoristiti za postavljanje početnih vrijednosti svih
elemenata polja na nulu.
int br[5] = {0};
inicijalizira eksplicitno prvi element nulom, ali implicitno inicijalizira i sve preostale
članove polja, također nulom.
Tako se, na žalost, može inicijalizirati samo nulama. Inicijalizacija na bilo koju drugu
početnu vrijednost mora se obaviti petljom.
double d[1000] = {1};
inicijalizira na jedinicu samo element s indeksom 0, dok sve preostale elemente
inicijalizira na nulu, pa je zato takvu inicijalizaciju potrebno napraviti na sljedeći način
double d[1000];
int i;
for (i = 0; i < 1000; i++) d[i] = 1;
Ovdje treba primijetiti da eventualna potreba za promjenom veličine polja iziskuje
promjenu na dva mjesta u programu: u definiciji i u petlji koja polje inicijalizira.
Zato je uobičajeno za određivanje veličine polja koristiti simboličku konstantu definiranu
naredbom #define:
#define MAX 1000
double d[MAX];
int i;
for (i = 0; i < MAX; i++) d[i] = 1;
Sada je promjenu veličine polja dovoljno napraviti samo na jednom mjestu.
Svojstvo jezika da se prvi element polja referencira indeksnim izrazom s vrijednošću 0
ponekad će zahtijevati transformaciju indeksnog izraza ili rezervaciju polja s više
elemenata nego što je stvarno potrebno. U našem primjeru ocjene su od 1 do 5, pa ako
želimo prvi element polja (s indeksom 0) iskoristiti za brojanje jedinica, a posljednji
element (s indeksom 4) za brojanje petica, tada u svakom indeksnom izrazu moramo
napraviti transformaciju
indeks = ocjena - 1
#include <stdio.h>
#define MAXOCJENA 5
int main(void) {
int br[MAXOCJENA] = {0};
int oc;
while (1) {
scanf("%d", &oc);
if (oc == 0)
break;
else if (oc > MAXOCJENA)
printf("Neispravna ocjena: %d\n", oc);
else
br[oc-1]++;
}
for (oc = 1; oc <= MAXOCJENA; oc++) {
printf("Broj ocjena %d: %d\n", oc, br[oc-1]);
}
return 0;
}

163
Uvod u programiranje Polja

Program 39. FrekvencijePoljem.c

Alternativno, ako je najmanja ocjena doista i uvijek 1, moguće je definirati polje koje je za
jedan element veće, dakle polje s indeksima od 0 do 5, pa transformacija nije potrebna:
#include <stdio.h>
#define MAXOCJENA 5
int main(void) {
int br[MAXOCJENA+1] = {0};
int oc;
while (1) {
scanf("%d", &oc);
if (oc == 0)
break;
else if (oc > MAXOCJENA)
printf("Neispravna ocjena: %d\n", oc);
else
br[oc]++;
}
for (oc = 1; oc <= MAXOCJENA; oc++) {
printf("Broj ocjena %d: %d\n", oc, br[oc]);
}
return 0;
}
Uobičajeno je u takvim slučajevima izrazom MAXOCJENA+1 (koji će prevodilac ionako u
konkretnom slučaju prevesti u 6) dati semantički jasno do znanja da se rezervira prostor
za pet elemenata, ali dodatno, i za jedan element koji se neće koristiti.
Na ovaj je način nepotrebno potrošeno 4 bajta memorije u podatkovnom dijelu, ali je zato
ušteđen prostor u dijelu za instrukcije (ovdje ne postoje dvije instrukcije oduzimanja koje
su postojale u programu koji transformira ocjenu u stvarni indeks), pa je ukupna veličina
programa manja. Dodatno, program se izvodi brže jer nije potrebno kod svakog
referenciranja elementa polja obaviti transformaciju indeksa.
U nekom hipotetskom primjeru gdje bi donja granica indeksa polja iznosila npr. 1000000,
transformacija svakako ima više smisla nego rezervacija polja koje će imati milijun
elemenata previše.
Prethodni program može se učiniti još robusnijim i neovisnim o sustavu ocjenjivanja ako
se uvede i simbolička konstanta MINOCJENA. Uz modifikaciju ulaznog i izlaznog formata
za učitavanje odnosno ispisivanje znakovnih umjesto numeričkih vrijednosti (%c umjesto
%d), ovako bi izgledao program koji će uz minimalan utrošak memorije brojati frekvencije
američkih ocjena od A do F. Koristit će se činjenica da su znakovne vrijednosti
predstavljene numeričkim ASCII kôdom ('A' = 65, 'B' = 66 itd.).
#include <stdio.h>
#define MAXOCJENA 'F'
#define MINOCJENA 'A'
int main(void) {
int br[MAXOCJENA–MINOCJENA+1] = {0};
char oc;
while (1) {
scanf("%c", &oc);
if (oc == 0)
break;
else if (oc < MINOCJENA || oc > MAXOCJENA)

164
Uvod u programiranje Polja

printf("Neispravna ocjena: %c\n", oc);


else
br[oc-MINOCJENA]++;
}
for (oc = MINOCJENA; oc <= MAXOCJENA; oc++) {
printf("Broj ocjena %c: %d\n", oc, br[oc-MINOCJENA]);
}
return 0;
}
Treba napomenuti da je svaki element polja skalarna varijabla onoga tipa koji je naveden
u definiciji polja, pa je kod učitavanja pojedinačnih elemenata polja funkcijom scanf, kao i
u slučaju bilo koje druge skalarne varijable, potrebno uz argument napisati operator &:
float p[10];
int i;
for (i = 0; i < 10; i++) {
scanf("%f", &p[i]);
}
U sljedećem primjeru pokazat će se primjena polja za tablice pretraživanja (lookup tables).
Pretpostavimo da se želi učinkovito ostvariti funkcija isalnum koja vraća 1 ako je
argument slovo ili brojka. Trivijalna izvedba bila bi
int isalnum(int ch) {
return ch >= '0' && ch <= '9'
|| ch >= 'A' && ch <= 'Z'
|| ch >= 'a' && ch <= 'z';
}
U najgorem slučaju (za ch >= 'a') ova će funkcija obaviti čak šest uspoređivanja. Skraćena
evaluacija (short circuit evaluation) u ovom slučaju ne pomaže jer se o istinitosti cijelog
izraza ne može zaključiti sve dok se ne obavi i posljednja usporedba. Za usporedbu, ako je
ch < '0' za zaključivanje o istinitosti potrebne samo 3 usporedbe, a za ch iz intervala ['0',
'9'] samo dvije.
Učinkovitije je pripremiti polje u kojem funkcija može izravno provjeriti odgovara li ch
traženim uvjetima:
unsigned char t[] = {
0, // 0

0, // 47
1, // 48 ('0')

1, // 57 ('9')
0, // 58
0, // 64
1, // 65 ('A')

}
int isalnum(int ch) {
return t[ch];
}
U ovom rješenju utrošeno je 128 bajta za pomoćno polje, ali je kôd funkcije znatno kraći
što se jasno može vidjeti ako se pogleda generirani asemblerski kôd.

165
Uvod u programiranje Polja

Isto polje, štoviše, može poslužiti kao tablica pretraživanja, osim za isalnum, i za isdigit,
isalpha, isprint. iscntrl, isspace, islower te isupper, tako da se svaki element polja opiše
ogovarajućom kombinacijom bita. Ako bitovi s lijeva nadesno predstavljaju činjenicu da
je neki znak redom brojka, slovo, slovo ili brojka, znak koji se može ispisati, kontrolni
znak, praznina, malo slovo, veliko slovo, tada bi za znak 'a' element polja bio definiran kao
unsigned char t[] = {

0x74, // 0b01110010, za znak 97 ('a')

}
jer 'a' nije brojka, jest slovo, jest slovo ili brojka itd.
Ako se uvedu konstante
#define MASK_DIGIT 0x80 // 0b10000000
#define MASK_ALPHA 0x40 // 0b01000000
#define MASK_ALNUM 0x20 // 0b00100000
#define MASK_PRINT 0x10 // 0b00010000
#define MASK_CNTRL 0x08 // 0b00001000
#define MASK_SPACE 0x04 // 0b00000100
#define MASK_LOWER 0x02 // 0b00000010
#define MASK_UPPER 0x01 // 0b00000001
funkcije se svode na
int isdigit(int ch) {
return t[ch] & MASK_DIGIT;
}
int isalpha(int ch) {
return t[ch] & MASK_ALPHA;
}
… // itd.
Ukupno utrošena memorija za tablicu pretraživanja i kôd svih funkcija ovako je bitno
manja nego da su se realizirale trivijalno, a izvođenje je znatno brže. Funkcije isdigit,
isalpha, isalnum, isprint. iscntrl, isspace, islower te isupper u standardnoj biblioteci
izvedene su na sličan način.

Indeksni izrazi
Indeksni izraz, skraćeno indeks, može biti bilo kako složen, ali mora rezultirati
cjelobrojnom vrijednošću.
Kao primjer, indeksni izraz u primjeru
p[(v*20+7)/16] = 1;
sasvim je prihvatljiv ako je v cjelobrojna varijabla.
Međutim, naredba poput
p[0.5] = 1;
rezultira greškom već pri prevođenju.
Element polja s indeksom i udaljen je od početka polja za i veličina pojedinačnog elementa
polja. Tako je u polju
int p[10];

166
Uvod u programiranje Polja

element s indeksom 0 na samom početku polja, element s indeksom 1 udaljen je za


veličinu jednog podatka tipa int od početka polja i tako dalje.
Ime polja prevodilac shvaća kao pokazivač na početak polja, pa sljedeći odsječak
int p[10];
printf("%u %u %u\n", p, p+1, p+2);
ispisuje adrese prvog, drugog i trećeg elementa polja
6422280 6422284 6422288
jednako kao što bi ispisala naredba
printf("%u %u %u\n ", &p[0], &p[1], &p[2]);
Svaki izraz oblika
p[i]
interno se pretvara u izraz koji koristi pokazivače
*(p + i);
Kod indeksnih izraza uvijek je potreban oprez, jer pogrešno indeksiranje može rezultirati
neočekivanim rezultatom. U sljedećem primjeru
int p[3] = {1,2,3};
int i, a = 1;
p[3] = 4;
for (i = 0; i < 3; i++) printf("p[%d] = %d\n", i, p[i]);
printf("a = %d\n", a);
program, neočekivano, ispisuje
p[0] = 1
p[1] = 2
p[2] = 3
a = 4
iako je varijabli a inicijalizacijom pridijeljena vrijednost 1, a u odsječku nema naredbe koja
tu varijablu (izravno) mijenja.
Problem je, naravno, u naredbi p[3] = 4;
Za polje je u definiciji rezerviran prostor u veličini 3 cijela broja, koja se referenciraju
indeksima 0, 1 i 2. Prevodilac je prostor za varijable u memoriji rezervirao na sljedeći
način:
p[0] p[1] p[2] a i
… …
6422300 6422304 6422308 6422312 6422316
Prilikom dohvata elementa polja određenog indeksnim izrazom, program ne obavlja
nikakvu kontrolu. Izraz p[3] jednostavno se prevodi u *(p + 3), odnosno „sadržaj memorije
na poziciji koja je za veličinu 3 cijela broja udaljena od početka polja p“, a prema gornjoj
slici na toj se poziciji nalazi se upravo varijabla a, pa
p[3] = 4;
zapravo mijenja sadržaj varijable a.
Da je ta naredba glasila
p[4] = 4;

167
Uvod u programiranje Polja

promijenila bi sadržaj varijable i, ali to ne bismo primijetili jer se neposredno nakon toga
vrijednost varijable i, inicijalizacijom petlje, postavlja na 0.
Slično bi se dogodilo da je indeksni izraz negativan: promijenio bi se sadržaj memorije
negdje ispred početka polja, bez ikakve poruke o grešci, jer bi prevodilac p[-2]
konzistentno interpretirao kao *(p – 2), dakle kao cijeli broj koji se nalazi za dva cijela
broja ispred početka polja p.
Poruka o grešci (segmentation fault, access violation) javit će se tek kad se na ovaj način
pokuša pristupiti memoriji koja ne pripada programu.

Znakovni nizovi
Tekstovi se u programskom jeziku C pohranjuju polja sastavljena od podataka tipa char.
Takva polja zovemo još i znakovnim nizovima.
Znakovni niz, kao i svaki drugi podatak, može biti varijabla ili konstanta.
Konstantni znakovni niz prepoznaje se po tome što se nalazi unutar dvostrukih
navodnika, npr. "ABC". Podsjetimo se, znakovna konstanta piše se unutar jednostrukih
navodnika npr. 'A'.
Na kraj svakog konstantnog znakovnog niza prevodilac dodaje znak s ASCII kôdom 0, tzv.
znak null, tako da je "ABC" u memoriji pohranjeno u 4 sukcesivna bajta s numeričkom
vrijednošću
… 65 66 67 0 …
ili, u znakovnoj notaciji
… A B C \0 …
Znak null označava logički kraj znakovnog niza koji će biti korišten u funkcijama koje po
tome nizu trebaju iterirati.
Razlika između "A" i 'A' je, dakle, u tome što je prvo znakovni niz koji zauzima dva bajta
uključujući znak null, dok je drugo konstanta tipa int i zauzima četiri bajta.
Konstantni znakovni niz korišten je do sada već mnogo puta pri pozivima funkcija scanf i
printf.
Format ispisa pri pozivu funkcije printf nije ništa drugo nego znakovni niz koji funkcija
analizira znak po znak.
U primjeru
printf("Dobar dan");
funkcija printf doslovno prepisuje sadržaj formata na izlaz, sve dok ne naiđe na null znak
ugrađen u niz iza slova 'n'.
Ako se u analizi formata naiđe na konverzijsku specifikaciju, koja se prepoznaje po znaku
%, ona će odrediti način transformacije sljedećih argumenata.
Za ispis nizova koristi se konverzijska specifikacija %s. Odgovarajući argument je
znakovni niz po kojemu će se iterirati i u naredbeni prozor ispisivati znakovi sve dok se
ne naiđe na znak null.
U sljedećem primjeru treba ispisati sadržaj znakovnog niza koji se predaje kao sljedeći
argument iza formata, i to tako da se taj znakovni niz ispiše u dvostrukim navodnicima.

168
Uvod u programiranje Polja

Dvostruki navodnici oznaka su početka i završavaju niza koji definira format, pa bi sljedeći
ispis rezultirao greškom:
printf("U nizu je "%s"\n", "ABC");
jer prevodilac iza završenog formata (koji se ovdje shvaća kao "U nizu je ") očekuje ili
zarez koji odvaja format od liste argumenata, ili zatvorenu zagradu. Zato ispred
dvostrukih navodnika koje se želi ispisati na izlaz treba napisati znak \, kao i ispred svih
ostalih znakova koji imaju specijalno značenje, poput \n:
printf("U nizu je \"%s\"\n“, "ABC");
Ispis je
U nizu je "ABC"
Varijabilni znakovni niz definira se kao polje podataka tipa char;
char niz[10];
Niz je moguće inicijalizirati element po element polja, tj. znak po znak, npr.
char niz[10] = {'A', 'B', 'C'};
pri čemu će 7 znakova koji nisu eksplicitno inicijalizirani poprimiti vrijednost 0.
Inicijalizacija niza
char niz1[2] = {'X', 'Y'};
ili
char niz1[] = {'X', 'Y'};
neće na kraj dodati nulu jer je u prvom slučaju inicijalizirano upravo onoliko članova polja
koliko ih ima, a u drugome slučaju veličina polja određuje se prema broju inicijalizatora.
Moguće je varijabilni znakovni niz inicijalizirati i sadržajem konstantnog znakovnog niza,
npr.
char niz[10] = "ABC";
Nakon ovakve inicijalizacije naredba
printf("U nizu je \"%s\"\n“, niz);
ispisuje
U nizu je "ABC"
jer %s iterira do znaka null koji je ugrađen inicijalizacijom, što se može provjeriti ispisom
ASCII kôdova cijelog sadržaja:
char niz[10] = "ABC";
int i;
for (i = 0; i < 10; i++) {
printf("%d ", niz[i]);
}
printf("\n");
čime se ispisuje
65 66 67 0 0 0 0 0 0 0
U programskom jeziku C-u nije moguće znakovnom nizu pridružiti vrijednost operatorom
pridruživanja. Naredba
niz = "DEF";

169
Uvod u programiranje Polja

rezultira greškom pri prevođenju. Za tu svrhu koriste se funkcije o kojima će biti govora
nešto kasnije.
Moguće je, međutim, nizu pridružiti vrijednost znak po znak:
niz[0] = 'D';
niz[1] = 'A';
niz[2] = '\0';
Znak null ne smije se zaboraviti jer bi sljedeći odsječak
char niz[10];
niz[0] = 'D';
niz[1] = 'A';
printf("%s\n", niz);
zahvaljujući činjenici da %s prepisuje sadržaj niza na izlaz sve dok ne naiđe na null, mogao
ispisati npr. ovako:
DA%io7&#$!“9
U ovom primjeru bajt u kojemu je vrijednost nula pronašao se 13 bajta iza početka niza,
pa je ispis završio. U općenitom slučaju ne može se znati gdje će se u memoriji pronaći
prvi null znak. To može biti i unutar neinicijalizanog dijela niza, a može biti mnogo dalje.

Jednodimenzijska polja varijabilne veličine


U većini slučajeva veličinu polja nije moguće odrediti prilikom prevođenja. Kao primjer
može poslužiti sljedeći problem kod kojega je potrebno učitati broj članova polja, zatim
članove polja, odrediti i ispisati njihov prosjek i nakon toga ispisati za svaki član polja
oznaku >, = ili <, ovisno od toga je li član polja manji od prosjeka, jednak prosjeku ili veći
od prosjeka.
U starijim inačicama C-a bilo je potrebno unaprijed dimenzionirati polje za najgori
očekivani slučaj te kod učitavanja broja članova polja provjeravati je li premašen najveći
dozvoljeni broj elemenata.
#include <stdio.h>
#define MAX 1000
int main(void) {
int i, n;
float suma = 0, prosjek, p[MAX];
do {
scanf("%d", &n);
} while (n <= 0 || n > MAX);
for (i = 0; i < n; i++) {
scanf("%f", &p[i]);
suma += p[i];
}
prosjek = suma / n;
printf("Prosjek: %g\n", prosjek);
for (i = 0; i < n; i++) {
printf("%g %c\n", p[i],
p[i] > prosjek ? '>' :
p[i] == prosjek ? '=' : '<');
}
return 0;
}
Program 40. VeciOdProsjekaFiksno.c

170
Uvod u programiranje Polja

Problem se ovdje sastoji u činjenici da je zbog možda 3 elementa polja, koliko je u nekom
konkretnom slučaju potrebno, za takvo polje rezervirano ukupno 4000 bajta memorije.
Također, ako se u nekom trenutku ukaže potreba za više od 1000 elemenata, takvi se
podaci neće moći obraditi bez promjene simboličke konstante MAX i ponovnog
prevođenja cijelog programa.
U ovom slučaju može se koristiti polje varijabilne veličine (variable length array, VLA),
koje se prepoznaje po tome što je u definiciji polja u uglatim zagradama izraz koji nije
konstanta. U prethodnom programu potrebno je načiniti samo sljedeću modifikaciju:
int i, n;
float suma = 0, prosjek;
do {
scanf("%d", &n);
} while (n <= 0);
float p[n];
for (i = 0; i < n; i++) {
scanf("%d", &p[i]);
}
prosjek = suma / n;
Definicija simboličke konstante MAX sada nije potrebna.
Pri korištenju polja varijabilne veličine treba voditi računa o činjenici da ni ono ne smije
biti preveliko. I za polje varijabilne veličine memorija se rezervira na stogu, pa se u slučaju
da polje premaši pretpostavljenu veličinu stoga događa greška pri izvođenju.
Definicija polja može se naći i u petlji, pri čemu se svakim nailaskom na definiciju veličina
polja prilagođava, npr.:
int n = 0;
while (1) {
n = n + 1;
float p[n];

}
Međutim, nema garancije da će pri promjeni veličine polja ostati sačuvani podaci koji su
u njemu pohranjeni. Malo modificirani problem ispisa elemenata manjih, jednakih
odnosno većih od prosjeka, u kojemu se podaci unose sve dok se ne unese neka vrijednost
koja označava završetak unosa, krenuvši od polja veličine jedan koje se postepeno
proširuje, na ovaj način uopće nije moguće riješiti, već je potrebno koristiti pokazivače i
promjenu dinamičke rezervacije memorije kako je objašnjeno u poglavlju Funkcija realloc.
Također, polje definirano u nekom bloku vidljivo je samo od trenutka definicije do kraja
bloka u kojem je definirano, pa cijelu obradu nad podacima iz toga polja treba obaviti
unutar tijela petlje. Sve to dosta ograničava primjenu polja varijabilne veličine kojima se
veličina dinamički mijenja.

Jednodimenzijska polja i funkcije


Često je potrebno neku operaciju nad poljem obaviti u funkciji. Kao primjer može poslužiti
funkcija koja treba izračunati sumu svih elemenata nekog zadanog jednodimenzijskog
polja. Očito, za izračunati sumu svih elemenata polja, funkcija mora kroz parametre dobiti
elemente koje treba zbrojiti, kao i informaciju koliko tih elemenata ima. Pri tome nije bitna
stvarna veličina polja na nadređenoj razini, već samo stvarno korišteni broj elemenata, pa
se prava dimenzija polja u parametrima može izostaviti.

171
Uvod u programiranje Polja

#include <stdio.h>
int suma(int p[], int n) {
int suma = 0, i;
for (i = 0; i < n; i++) {
suma += p[i];
}
return suma;
}
int main(void) {
int a[] = {1, 2, 3};
int b[] = {3, 4, 5, 6};
printf("%d %d\n", suma(a, 3), suma(b, 4));
}
Program 41. JednodimenzijskoPoljeFunkcija.c

Za razliku od skalarnih varijabli, polje se funkciji ne predaje preko vrijednosti. Taj bi


princip zahtijevao dvostruki utrošak memorije, stvaranje kopije trajalo bi neko vrijeme, a
funkcijom ne bi bilo moguće intervenirati nad sadržajem polja, kao što se to, na primjer,
radi u sljedećoj funkciji koja udvostručava vrijednosti svih elemenata polja:
void udvostruci(int p[], int n) {
int i;
for (i = 0; i < n; i++) {
p[i] = p[i] * 2;
}
}
Umjesto toga, funkciji se predaje adresa prvog elementa polja. Funkcionalno, ova funkcija
je istovjetna funkciji
void udvostruci(int *p, int n) {
int i;
for (i = 0; i < n; i++) {
*(p+i) = *(p+i) * 2;
}
}
jer prevodilac svaku pojavu imena polja u programu tumači kao pokazivač na početak
polja, a svaki indeksni izraz pretvara u odgovarajući izraz uz korištenje pokazivača,. Kao
što je to već prije bilo rečeno, element polja s indeksom i udaljen je za i elemenata od
početka polja. Adresa elementa p[i], dakle, može se dobiti izrazom p+i, pa se sam element
dobije dereferenciranjem adrese kao *(p+i). Zato je notaciju koja koristi indeksni izraz
moguće pisati ravnopravno s notacijom koja koristi pokazivač i obratno. Prevodilac neće
imati nikakvih problema ako se naredba koja udvostručuje element polja napiše kao
p[i] = *(p+i) * 2;
ili
*(p+i) = p[i] * 2;
jer će je ionako prevesti u
*(p+i) = *(p+i) * 2;
Kako je ime polja zapravo adresa početka polja, adresni operator se pri pozivu ne treba i
ne smije navesti:
udvostruci (a, 3);

172
Uvod u programiranje Polja

S obzirom na činjenicu da se svi argumenti u funkciju predaju preko vrijednosti i da se za


njih rezervira prostor na stogu, promjena pokazivača na početak polja u funkciji neće
imati nikakav utjecaj na pokazivač iz glavnog programa. Stoga se iteracija po polju često
obavlja uvećanjem pokazivača. Funkcija udvostruci može se zato napisati i ovako:
void udvostruci(int *p, int n) {
int i;
for (i = 0; i < n; i++, p++) {
*p = *p * 2;
}
}
Ovako napisana funkcija nešto je brža jer se umjesto dva zbrajanja potrebna da se
izračunaju adresa s koje se uzima stara vrijednost i adresa na koju se pohranjuje
vrijednost obavlja samo jedno uvećanje pokazivača. Stanja varijabli u programu i funkciji
pozvanoj naredbom
udvostruci (a, 3);
mogu se predočiti sljedećom sekvencom gdje slike redom prikazuju vrijednosti lokalnih
varijabli funkcije prije ulaska u petlju, nakon obavljanja svake naredbe *p = *p * 2; te
na kraju nakon izlaska iz petlje:

main udvostruci p
a[0] a[0] a[0] i n
1 2 3 ? 0 3

main udvostruci p
a[0] a[0] a[0] i n
2 2 3 ? 0 3

main udvostruci p
a[0] a[0] a[0] i n
2 4 3 ? 1 3

main udvostruci p
a[0] a[0] a[0] i n
2 4 6 ? 2 3

main udvostruci p
a[0] a[0] a[0] i n
2 4 6 ? 3 3

Znakovni nizovi polja podataka tipa char, koja u sebi imaju oznaku kraja niza '\0'. Funkcije
koje rade s takvim nizovima ne zahtijevaju parametar kojim se zadaje duljina, jer je
informacija o duljini ugrađena u sam niz. Kao školski primjer za funkciju kojoj je

173
Uvod u programiranje Polja

parametar neki znakovni niz može poslužiti izvedba funkcije strlen, koja je inače dio
standardne biblioteke i čiji se prototip nalazi u <string.h>:
size_t strlen(const char *s) {
size_t r = 0;
for (;*s;r++, s++);
return r;
}
Program 42. strlen.c

size_t je cjelobrojni tip definiran naredbom #define koji nije propisan standardom,
najčešće unsigned int, a const je naputak prevodiocu da svaki pokušaj promjene
parametra dojavi kao grešku.
Funkciji se s nadređene razine predaje adresa nekog znakovnog niza koji bi u sebi morao
imati oznaku kraja:
char niz[] = "Neki tekst";
r = strlen(niz);
ili
r = strlen("Neki tekst");
ili
char niz[MAX];
scanf("%s", niz);
r = strlen(niz);
Funkcija dobiva kopiju adrese početka znakovnog niza koja se sprema u lokalni pokazivač
s. Sve dok kôd znaka na koji pokazuje pokazivač s nije nula, tj. dok pokazivač ne pokaže na
oznaku kraja niza, uvjet ponavljanja petlje je istinit (*s, znak na koji pokazuje pokazivač s
nije nula), pa se brojač znakova uvećava za jedan, kao i adresa zapisana u pokazivaču s,
čime se ostvaruje pokazivanje na sljedeći znak u nizu.
U trećem primjeru za poziv funkcije strlen, u kojem se sadržaj niza učitava funkcijom scanf,
treba obratiti pažnju na činjenicu da je niz polje i da se varijabla niz shvaća i kao adresa
prvog elementa polja. Funkcija scanf očekuje adresu na koju se treba spremiti podatak
utipkan s tipkovnice, pa uz argument nema adresnog operatora.
Bitno je također i da za podatke bude rezervirano dovoljno memorije. Funkcija scanf ne
dobiva nikakvu informaciju o tome koliko je memorije rezervirano za niz i nastavit će
upisivati znakove utipkane na tipkovnici u niz sve dok se ne utipka bijela praznina
(praznina, tabulator, enter). Ako za polje niz nije rezervirano dovoljno memorije,
upisivanje će se nastaviti u memorijske lokacije koje pripadaju varijablama koje se u
memoriji nalaze iza toga polja.
Jednodimenzijsko polje može se u funkciji dočekati i kao polje varijabilne veličine, ali pri
tome parametar koji će sadržavati broj elemenat polja mora prethoditi imenu polja, kao u
sljedećem primjeru u kojemu se traži najveći element polja. Koristit će se standardni
algoritam u kojemu se prvi element proglašava najvećim, a za svaki sljedeći element
ispituje se je li taj element veći od do tada nađenog najvećeg. Ako jest, njega se proglašava
najvećim.
int najveci(int n, int p[n]) {
int r = p[0], i;
for (i = 1; i < n; i++) {
if (p[i] > r) {

174
Uvod u programiranje Polja

r = p[i];
}
}
return r;
}
I ovdje je moguće koristiti notaciju s pokazivačima, s napomenom da je, nakon uzimanja
vrijednosti prvog elementa, pokazivač potrebno pomaknuti na sljedeći element:
int najveci(int n, int p[n]) {
int r = *p, i;
for (i = 1, ++p; i < n; i++, p++) {
if (*p > r) {
r = *p;
}
}
return r;
}
Alternativno, pomicanje na sljedeći element moglo se načiniti i odmah kod prvog
dereferenciranja pokazivača operatorom uvećanja za 1 (++), obvezno u postfiksnoj
notaciji jer vrijednost treba uzeti iz prvog elementa polja, a tek nakon toga pokazivač
pomaknuti na sljedeći element.

int r = *p++, i;
for (i = 1, ++p; i < n; i++, p++) {

Operatori uvećanja odnosno umanjenja često se koriste u kontekstu pokazivača i
dereferenciranja, ali je pri tome nužan oprez jer se lako može pogriješiti.
Izrazi ++*p, *++p, *p++ i (*p)++ izgledaju slično, ali su funkcionalno potpuno različiti, što
je najbolje, bez puno riječi, prikazati slikama, na kojima je prvo prikazano stanje prije
izvođenja naredbe, a zatim stanje nakon izvođenja naredbe.
r = ++*p;
r p a[0] a[0]
? 1 2 …

r p a[0] a[0]
2 2 2 …

r = *++p;
r p a[0] a[0]
? 1 2 …

r p a[0] a[0]
2 1 2 …

r = *p++;

175
Uvod u programiranje Polja

r p a[0] a[0]
? 1 2 …

r p a[0] a[0]
1 1 2 …

r = (*p)++;
r p a[0] a[0]
? 1 2 …

r p a[0] a[0]
1 2 2 …

Slijedi program kojim se mogu provjeriti gornje tvrdnje:


#include <stdio.h>
int main(void) {
int a[] = {1, 2}, r;
int *p = a;
//r = ++*p;
//r = *++p;
//r = *p++;
r = (*p)++;
printf("%u %u %d %d %d", a, p, r, a[0], a[1]);
}
Program 43. UvecanjePokazivacaSDereferenciranjem.c

Funkcije iz standardne biblioteke za rukovanje nizovima


Standardna biblioteka sadrži više funkcija za rukovanje znakovnim nizovima. Prototipovi
tih funkcija sadržani su u <string.h>. Ovdje će biti pobrojane značajnije funkcije, od kojih
će za većinu biti objašnjena i izvedba jer su edukativni primjeri funkcija kojima su ulazni
argumenti znakovni nizovi i pokazivači.
Funkcija strlen opisana je i realizirana u prethodnom poglavlju.
Funkcija strcpy kopira sadržaj jednoga niza u drugi. Prototip je
char *strcpy(char *dest, const char *src);
gdje je src pokazivač na izvorišni, a dest na odredišni niz. src je const char * jer se sadržaj
memorije na koju pokazuje u funkciji ne smije promijeniti
Izvedba funkcije je
char *strcpy(char *dest, const char *src) {
// zapamti pokazivač na početak odredišta
char *p = dest;
// sve dok src pokazuje na znak koji nije '\0'
// kopiraj znak na koji pokazuje src tamo kamo pokazuje dest
// i nakon toga pomakni src i dest prema naprijed
while (*src) *dest++ = *src++;
// oznaka kraja time nije kopirana, a dest sada pokazuje iza
// zadnjeg kopiranog znaka, pa tamo treba upisati oznaku kraja
*dest = '\0';
return p;

176
Uvod u programiranje Polja

}
Program 44. strcpy.c

Za dest mora biti rezervirano barem onoliko mjesta koliko je znakova u src, uključujući i
oznaku kraja niza, inače će strcpy kopirati preko varijabli koje su u memoriji smještene
iza dest.
Funkcija će može pozvati na sljedeći način:
char a[] = "Neki tekst";
char b[128];
strcpy(b, a);
Rezultat funkcije je pokazivač na početak odredišta koji u ovom primjeru nije korišten, ali
postoji da bi se odmah omogućilo korištenje rezultata u nekoj drugoj funkciji
printf("%s", strcpy(b, a));
ili istoj funkciji ulančanim pozivom
strcpy(c, strcpy(b, a));
Ovime se a kopira u b, a zatim b u c.
Funkcija strncpy s prototipom
char *strncpy(char *dest, const char *src, size_t count);
kopira najviše count znakova iz src u dest. Ako je u src manje znakova nego što iznosi count,
tada će se dest dopuniti znakovima '\0' do ukupne duljine count. Ako je u src upravo toliko
znakova nego što iznosi count, ili ih ima više, u dest se neće dodati '\0'. Implementacija je
slična kao za strcpy
char *strncpy(char *dest, const char *src, size_t count) {
// zapamti pokazivač na početak odredišta
char *p = dest;
// sve dok src pokazuje na znak koji nije '\0'
// i dok je kopirano manje od count znakova
// kopiraj znak na koji pokazuje src tamo kamo pokazuje dest
// i nakon toga pomakni src i dest prema naprijed
while (*src && count > 0) {
*dest++ = *src++;
count--;
}
// nastavi dodavati oznaku kraja do ukupno count znakova
while (count > 0) {
*(dest++) = '\0';
count--;
}
return p;
}
Program 45. strncpy.c

I ovdje treba paziti da je za dest rezervirano dovoljno (najmanje count) mjesta.


Funkcija strcat84 ima prototip
char *strcat(char *dest, const char *src);

84 Akronim od string concatenate: spojiti niz u lanac ili seriju

177
Uvod u programiranje Polja

i kopira src na kraj dest. Kôd je gotovo potpuno jednak kôdu funkcije strcpy, samo što se,
odmah nakon što se zapamti gdje započinje odredište, treba pomaknuti na kraj odredišta.

while (*dest) dest++;

Program 46. strcat.c

Ovdje treba paziti da je ukupno za dest rezervirano barem strlen(dest)+strlen(src)+1


mjesta. Sljedeći odsječak
char a[128] = "Neki";
char b[] = "Tekst";
printf("%s\n", strcat(a, b));
ispisat će
NekiTekst
Funkcija strncat ima prototip
char *strncat(char *dest, const char *src, size_t count);
i kopira najviše count znakova iz dest u src, s time da na kraj dest uvijek dodaje '\0'.
Uspoređivanje nizova obavlja se funkcijom strcmp. Prototip je
int strcmp( const char *lhs, const char *rhs );
Funkcija vraća negativnu vrijednost ako je lhs (left hand string, lijevi niz) abecedno prije
rhs (right hand string, desni niz), pozitivnu vrijednost ako je lhs abecedno nakon rhs, a
nulu ako su jednaki. Funkcija iz biblioteke uvijek vraća -1, 0 ili 1, ali je jednostavnija
izvedba koja vraća razliku između kôdova znakova na kojima se prvi puta pronađe
različitost. Izvedba je iznenađujuće kratka:
int strcmp( const char *lhs, const char *rhs ) {
for (;*lhs == *rhs && *lhs; lhs++, rhs++);
return *lhs - *rhs;
}
Program 47. strcmp.c

Ako su znakovi na koje pokazuju lhs i rhs različiti, petlja se prekida i na pozivajuću razinu
vraća razlika se između kôdova tih znakova. Ovime je pokrivena i situacija u kojoj je jedan
niz kraći od drugoga, tj. znak u jednom nizu je '\0' a u drugome nije. Ako su znakovi na
koje pokazuju lhs i rhs jednaki, a oba su '\0', dovoljno je ispitati bilo kojega od njih, u ovom
slučaju lijevoga. Petlja se tada prekida i vraća se razlika između '\0' i '\0', dakle nula. Ako
su znakovi na koje pokazuju lhs i rhs jednaki, a lijevi nije '\0', što znači da ni desni nije '\0',
i jedan i drugi pokazivač se pomiču na sljedeći znak u nizu.
strcmp razlikuje velika i mala slova (case sensitive comparison). strcmp("abc", "ABC") u
ovdje prikazanoj izvedbi vraća 32. Neki prevodioci imaju funkciju
int strcmpi( const char *lhs, const char *rhs );
koja obavlja usporedbu ne praveći razliku između velikih i malih slova (case insensitive
comparison) ali ona nije dio standarda. Ako ne postoji u sklopu prevodioca, lako se može
izvesti:
int strcmpi( const char *lhs, const char *rhs ) {
for (;tolower(*lhs) == tolower(*rhs) && *lhs; lhs++, rhs++);
return tolower(*lhs) - tolower(*rhs);

178
Uvod u programiranje Polja

}
Program 48. strcmpi.c

Funkcija
int strncmp(const char *lhs, const char *rhs, size_t count);
uspoređuje samo prvih count znakova u lhs i rhs.
Treba naglasiti da ove funkcije uspoređuju 8-bitne znakove, što znači da će ispravno raditi
samo za znakove engleske abecede85.
Funkcija strchr s prototipom
char *strchr(const char *str, int ch);
vraća pokazivač na prvu pojavu znaka ch unutar niza str. Ako unutar niza nije pronađen
traženi znak, funkcija vraća NULL.
Izvedba je vrlo jednostavna:
char *strchr(const char *str, int ch) {
while (*str && *str != ch) str++;
return *str ? (char *) str : NULL;
}
Program 49. strchr.c

Kako se za iteriranje koristi str koji je const char *, ako je znak pronađen, prije povratka iz
funkcije potrebno je obaviti pretvorbu u char * jer će se rezultat možda koristiti za
promjenu str, kao u sljedećem odsječku koji prvu promjenu znaka 'x' pretvara u 'y'.
char niz[] = "abcxdef";
char *p;
p = strchr(niz, 'x');
if (p) *p = 'y';
Funkcija strrchr s prototipom
char *strrchr(const char *str, int ch);
vraća pokazivač na posljednju pojavu znaka ch unutar niza str. Ako unutar niza nije
pronađen traženi znak, funkcija vraća NULL.
Izvedba je također trivijalna:
char *strrchr(const char *str, int ch) {
const char *p = NULL;
while (*str) {
if (*str == ch) p = str;
str++;
}
return (char *)p;
}
Program 50. strrchr.c

Pozicija nekog niza u drugome može se pronaći funkcijom


char *strstr(const char* str, const char* substr);

85 Usporedba nizova s dijakritičkim znakovima i ostali koncepti lokalizacije (fukcije setlocale, strcoll, wcscmp
i dr.) prelaze okvire ove knjige

179
Uvod u programiranje Polja

koja vraća pokazivač na mjesto na kojemu podniz substr počinje unutar niza str. Ako se
podniz ne može pronaći unutar niza, funkcija vraća pokazivač NULL. Sljedeća izvedba nije
najučinkovitija jer poziva druge funkcije, ali je konceptualno jasna: izračuna se duljina od
substr pa se redom traže pojave prvog znaka od substr. Od svake pozicije na kojima je
pronađen prvi znak od substr, uspoređuju se str i substr u duljini podniza funkcijom
strncmp.
char *strstr (const char *str, const char * substr) {
int len = strlen(substr);
const char *p = str;
for (; (p = strchr(p, *substr)) != 0; p++) {
if (strncmp(p, substr, len) == 0) return (char *) p;
}
return NULL;
}
Pomoćni pokazivač p koji pokazuje pojave prvog znaka od substr također treba biti const
char * jer bi inače preko toga pokazivača bilo moguće promijeniti sadržaj od str, ali ga prije
povratka trba pretvoriti u char *, jer se ne može znati što će se dalje s tom informacijom
raditi – možda će poslužiti za promjenu niza src na odgovarajućoj poziciji.
Funkcija
char* strpbrk86(const char* dest, const char* breakset);
vraća pokazivač na prvu pojavu nekog od znakova iz breakset u dest. Izvedba uz pomoć
strchr je jednostavna
char *strpbrk(const char * dest, const char * breakset) {
for (; * dest; dest++) {
if (strchr(breakset, *dest) != 0) {
return ((char *) dest);
}
}
return NULL;
}
Program 51. strpbrk.c

Dvodimenzijska i višedimenzijska polja fiksne veličine


Broj dimenzija polja nije ograničen, pa se tako u primjeru
int m[3][4];
definira dvodimenzijsko polje (matrica) veličine 3 · 4.
U matematičkoj notaciji mij podrazumijeva element iz i-tog retka i j-tog stupca matrice m.
Korisno je tu konvenciju zadržati i ovdje, s time da se takav element u C-u referencira s
m[i][j].
I ovdje indeksi kreću od 0, pa je m[2][3] zapravo element iz trećeg retka i četvrtog stupca
matrice.
Dvodimenzijsko polje može se ilustrirati primjerom u kojemu je potrebno generirati i
ispisati jediničnu matricu zadanog reda n. Jedinična matica je matrica koja na glavnoj

86 Akronim od string pointer break, jer vraća pokazivač na prvi odjeljivač (separator, break)

180
Uvod u programiranje Polja

dijagonali ima jedinice, a ostali su elementi popunjeni nulama. Npr, za zadani red matrice
3 treba ispisati
1 0 0
0 1 0
0 0 1
Polja fiksne veličine zahtijevaju rezervaciju memorije za najgori slučaj, pa je to najbolje
napraviti koristeći se simboličkom konstantom. Uvijek treba provjeravati je li zadani red
matrice u dozvoljenom rasponu, te ako nije, ponoviti učitavanje. Glavnu dijagonalu
identificirat ćemo preko indeksa retka i stupca elementa matrice: ako su jednaki, tada je
element na glavnoj dijagonali, inače nije.
U prvom rješenju iterirat će se po svim retcima i, za svaki redak iterirat će se po svim
stupcima j i element s indeksima [i][j] postaviti na odgovarajuću vrijednost.
Ispis matrice iterira po matrici na isti način, s time što se nakon pojedinačnih elemenata
retka ostavlja jedna praznina, a kad se ispiše cijeli redak, ispisuje se \n.
#include <stdio.h>
#define MAX 10
int main(void) {
int m[MAX][MAX], i, j, n;
// učitavanje reda ponavlja se sve dok je podatak neispravan
do {
scanf("%d", &n);
} while (n <= 0 || n > MAX);
// generiranje matrice
for (i = 0; i < n; i++) {
for (j = 0; j < n; j++) {
if (i == j) {
m[i][j] = 1;
} else {
m[i][j] = 0;
}
}
}
// ispis
for (i = 0; i < n; i++) {
for (j = 0; j < n; j++) {
printf("%d ", m[i][j]);
}
printf("\n"); // skok u novi red nakon cijelog retka
}

return 0;
}
Program 52. JedinicnaMatrica.c

Program se dalje može poboljšati. Pri tome nećemo razmatrati ispis za vrijeme
generiranja, jer da se htjelo samo ispisati jediničnu matricu, polje ne bi bilo ni potrebno:
for (i = 0; i < n; i++) {
for (j = 0; j < n; j++) {
printf("%d ", i == j ? 1 : 0);
}
printf("\n");
}

181
Uvod u programiranje Polja

U ponuđenom rješenju za svaki element matrice ispitujemo je li na glavnoj dijagonali ili


nije. To znači da imamo ukupno n2 ispitivanja i n2 pridruživanja. Učinkovitije bi bilo sve
elemente postaviti na nulu, a onda elemente glavne dijagonale postaviti na jedan:
for (i = 0; i < n; i++) {
for (j = 0; j < n; j++) {
m[i][j] = 0;
}
}
for (i = 0; i < n; i++) {
m[i][i] = 1;
}

Ovako imamo samo n2+n pridruživanja. Kako je vrijeme ispitivanja slično vremenu
pridruživanja, obavljeno je ukupno manje posla.
Na kraju, inicijalizacija dvodimenzijskog polja na nulu može se napraviti u definiciji, pa je
najbolje rješenje
int m[MAX][MAX] = {0}, …;

for (i = 0; i < n; i++) {
m[i][i] = 1;
}
Kod dvodimenzijskih polja fiksne veličine još više dolazi do izražaja problem nepotrebnog
utroška memorije do kojeg dolazi jer memoriju treba rezervirati za najgori slučaj. Za n=3
u prethodnom primjeru iskorišteno je samo 9 elemenata u gornjem lijevom uglu polja, od
ukupno rezerviranih 100:
1 0 0 x x x x x x x
0 1 0 x x x x x x x
0 0 1 x x x x x x x
x x x x x x x x x x
x x x x x x x x x x
x x x x x x x x x x
x x x x x x x x x x
x x x x x x x x x x
x x x x x x x x x x
x x x x x x x x x x
Višedimenzijska polja su samo logički višedimenzijska. Memorija računala je
jednodimenzijska linearna struktura u kojoj su podaci smješteni jedan za drugim, pa je
tako dvodimenzijsko polje int m[3][4] u memoriji smješteno kao 3 jednodimenzijska
polja veličine 4 koja slijede jedno iza drugoga (jedan pravokutnik odgovara jednom
cijelom broju koji se sastoji od 4 bajta, a deblje su uokvireni retci matrice):
m[0][0] m[0][1] m[0][2] m[0][3] m[1][0] m[1][1] m[1][2] m[1][3] m[2][0] m[2][1] m[2][2] m[2][3]
… …
Jedinična je matrica iz prethodnog primjera, dakle, stvarno u memoriji smještena ovako:
1 0 0 x x x x x x x 0 1 0 x x x x x x x 0 0 1 x x x x x x x x x x x…

182
Uvod u programiranje Polja

Već je prije rečeno da prevodilac svaki indeksni izraz prevodi u odgovarajući izraz uz
korištenje pokazivača, što znači da mora odrediti udaljenost od početka polja do traženog
elementa, pri čemu je ime polja pokazivač na njegov početak. Kod jednodimenzijskih polja
to se određuje jednostavno, jer je p[i] element koji je za i elemenata udaljen od početka
polja, pa je p[i] isto što i *(p+i);
Kod dvodimenzijskih polja određivanje stvarne adrese elementa je nešto složenije, jer je
udaljenost od početka polja određena dvama indeksima, ali i stvarnom duljinom retka.
Element p[i][j] udaljen je od početka polja za i punih redaka, čemu treba dodati udaljenost
od početka retka koja iznosi j. pa je adresa toga elementa
p + i * duljina_retka + j
a sam element
*( p + i * duljina_retka + j)
Važno je još jednom naglasiti da se duljina_retka odnosi na duljinu zadanu u definiciji, a
ne na broj elemenata koliko se stvarno koristi u retku, tako da će za primjer jedinične
matrice koja je definirana kao m[MAX][MAX] i kod koje je stvarni iskorišteni broj redaka
odnosno stupaca bio n, izraz koji će referencirati element s indeksima i i j biti
*(m + MAX * i + j)
a nikako ne
*(m + n * i + j)
Činjenica da su elementi dvodimenzijskog polja u memoriji smješteni redak za retkom
dozvoljava da se polje koje treba sadržavati npr.
1 2 3
4 5 6
u programu inicijalizira kao
int p[2][3] = {1, 2, 3, 4, 5, 6};
I ovdje će prevodilac elemente za koje nije zadana vrijednost implicitno inicijalizirati
nulama pa će sljedeća definicija
int p[2][3] = {1, 2};
rezultirati poljem sadržaja
1 2 0
0 0 0
Jasnije je, međutim, eksplicitno inicijalizirati svaki redak uvođenjem vitičastih zagrada
koje omeđuju redak pa je
int p[2][3] = {1, 2, 3, 4, 5, 6};
bolje napisati kao
int p[2][3] = {{1, 2, 3},
{4, 5, 6}};
Tako je moguće i parcijalno inicijalizirati retke pa će
int p[2][3] = {{1},
{2}};
rezultirati poljem sadržaja
1 0 0

183
Uvod u programiranje Polja

2 0 0
Naredba
int p[2][3] = {{1, 2, 3, 4},
{4, 5, 6}};
rezultirat će greškom kod prevođenja jer je u prvom retku previše inicijalizatora.
Iako bi se moglo zaključiti da je definicija
int p[][] = {{1, 2, 3},
{4, 5, 6}};
ispravna jer je vitičastim zagradama jasno definiran struktura matrice, prevodilac
zahtijeva da se eksplicitno zada barem druga dimenzija koja definira duljinu retka, ključan
podatak za izračunavanje adrese, pa je sljedeća definicija ispravna
int p[][3] = {{1, 2, 3},
{4, 5, 6}};
I ovdje sizeof vraća veličinu polja u bajtovima pa
printf("%d\n", sizeof(p));
ispisuje 24.
Duljina retka u bajtovima dobije se sa sizeof(p[0]) pa će
printf("%d\n", sizeof(p[0])/sizeof(p[0][0]));
ispisati broj elemenata u retku, u ovom slučaju 3, a
printf("%d\n", sizeof(p)/sizeof(p[0]));
ukupan broj redaka polja, u ovom slučaju 2.
U sljedećem primjeru
float a[3][4][2];
definira se trodimenzijsko polje veličine 3 · 4 · 2, što možemo interpretirati kao 3 susjedna
dvodimenzijska polja veličine 4 · 2, a svako od njih čine 4 susjedna jednodimenzijska polja
veličine 2.
a[0][0][0] a[0][0][1] a[0][1][0] a[0][1][1] a[0][2][0] a[0][2][1] a[0][3][0] a[0][3][1] a[1][0][0] a[1][0][1] a[1][1][0] a[1][1][1]

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


Drugim riječima, ovo polje čine 3 sloja od kojih je svaki sloj dvodimenzijsko polje veličine
4 · 2.
Za određivanje adrese elementa polja s indeksima k, i i j potrebno je znati punu dimenziju
svakog sloja, jer je element iz k-tog sloja barem k punih slojeva udaljen od njegovog
početka. Adresa elementa a[k][i][j] iznosi
a + k * (broj_redaka * duljina_retka) + i * duljina_retka + j
a sâm element je
*(a + k * (broj_redaka * duljina_retka) + i * duljina_retka + j)
Broj dimenzija polja nije ograničen sintaksom jezika. U primjeru
double b[3][3][3][3][3][3][3][3][3][3][3][3][3][3][3][3];

184
Uvod u programiranje Polja

definira se 16-dimenzijsko polje veličine 3·3·3·3·3·3·3·3·3·3·3·3·3·3·3·3 = 316 = 43 046


721 elemenata od po 8 bajta, odnosno ukupno 344 373 768 megabajta. I ovdje je logika
ista: ovo su 3 15-dimenzijska polja smještena u memoriji jedno iza drugoga, a svako od
njih čine 3 14-dimenzijska polja i tako dalje.
Pri dimenzioniranju višedimenzijskih polja posebno treba paziti na veličinu, jer se i za njih
memorija rezervira na stogu. Polje iz ovoga primjera nikako se neće moći koristiti bez
eksplicitnog povećanja stoga opcijom prevodioca.

Dvodimenzijska i višedimenzijska polja varijabilne veličine


Problem nepotrebne rezervacije memorije za najgori slučaj još je jače izražen kod
dvodimenzijskih i višedimenzijskih polja, pa je poznavanje potrebnog broja elemenata
prije nego se rezervira memorija za polje više nego potrebno. U sljedećem ćemo primjeru
načiniti program koji će učitati veličinu matrice, zatim elemente matrice po retcima i
nakon toga matricu transponirati.
Transponiranjem matrice retci postaju stupci i obratno. Matrica
1 2 3 4
5 6 7 8
postaje
1 5
2 6
3 7
4 8
drugim riječima element s indeksima i, j zamjenjuje se elementom s indeksima j, i i
obratno.
Naizgled je trivijalno, ali programski odsječak u kojemu je m broj redaka a n broj stupaca
matrice
for (i = 0; i < m; i++) {
for (j = 0; j < n; j++) {
pom = p[i][j];
p[i][j] = p[j][i];
p[j][i] = pom;
}
}
naizgled neće odraditi ništa jer će se, primjerice, p[1][2] zamijeniti s p[2][1], da bi se
kasnije p[2][1] zamijenio s p[1][2] vrativši sve u početno stanje.
Kod kvadratnih matrica rješenje je jednostavno, u svakom retku zamjenjuju se elementi
do glavne dijagonale
for (i = 0; i < m; i++) {
for (j = 0; j < i; j++) {

Međutim, matrica ne mora biti kvadratna. Za matricu koja ima više redaka nego stupaca,
gornji algoritam bi radio korektno. Međutim, matrica iz gornjeg primjera ima dva retka i
četiri stupca, pa bi gornji algoritam ostavio nedirnutima elemente iz trećeg i četvrtog
stupca.
Vanjska petlja, dakle, mora se obaviti max(m, n) puta, a tolike moraju fizički biti i obje
dimenzije matrice. Ispravan program slijedi:

185
Uvod u programiranje Polja

#include <stdio.h>
#define max(a,b) ((a) > (b) ? (a):(b))
int main(void) {
int m, n, i, j, pom;
printf("Unesite broj redaka i broj stupaca:");
scanf("%d %d", &m, &n);
int p[max(m,n)][max(m,n)];
printf("Unesite elemente po retcima:");
for (i = 0; i < m; i++) {
for (j = 0; j < n; j++) {
scanf("%d", &p[i][j]);
}
}
// transponiranje
for (i = 0; i < max(m,n); i++) {
for (j = 0; j < i; j++) {
pom = p[i][j];
p[i][j] = p[j][i];
p[j][i] = pom;
}
}
// ispis: izmijenjena matrica ima n redaka i m stupaca
for (i = 0; i < n; i++) {
for (j = 0; j < m; j++) {
printf("%d ", p[i][j]);
}
printf("\n");
}
}
Program 53. TransponiranjeMatrice.c

Dvodimenzijska i višedimenzijska polja i funkcije


Kao i kod jednodimenzijskih polja, funkciji će se preko stoga predati samo adresa početka
polja. Osim adrese početka polja, funkcija mora dobiti i informaciju o stvarnom broju
elemenata polja, ali i o stvarnim dimenzijama, kako bi se indeksni izraz korektno
preračunao u adresu.
Kod polja varijabilne veličine rezervacija memorije odgovara stvarnom broj elemenata
retka, stupca, sloja itd. pa je pozivanje funkcije nešto jednostavnije. U sljedećem programu
realizirat će se funkcija koja računa sumu matrica varijabilne veličine
C=A+B
pri čemu je
cij = aij + bij, za svaki i i j
Kako će se u ovom programu učitati dvije matrice, ima smisla učitavanje matrice obaviti
funkcijom. Funkcija preko argumenata dobiva tekst poruke koja će se ispisati prije
učitavanja, broj redaka i stupaca koji moraju odgovarati stvarnim dimenzijama polja te
adresu početka polja.
void mInput(char *poruka, int m, int n, int a[m][n]) {
int i, j;
printf("%s", poruka);
for (i = 0; i < m; i++) {

186
Uvod u programiranje Polja

for (j = 0; j < n; j++) {


scanf("%d", &a[i][j]);
}
}
}
Funkcija koja zbraja matrice, osim broja redaka i stupaca, mora dobiti i adrese pribrojnika
te adresu na koju se sprema suma
void mAdd(int m, int n, int a[m][n], int b[m][n], int c[m][n]) {
int i, j;
for (i = 0; i < m; i++) {
for (j = 0; j < n; j++) {
c[i][j] = a[i][j] + b[i][j];
}
}
}
Na kraju, u glavnom programu saznaju se dimenzije matrica, definiraju se i učitaju
pribrojnici a i b, definira rezultat c, obavi zbrajanje i ispiše rezultat:
int main(void) {
int m, n, i, j;
printf("Unesite broj redaka i broj stupaca:");
scanf("%d %d", &m, &n);
int a[m][n];
mInput("Unesite elemente prve matrice po retcima:",
m, n, a);
int b[m][n];
mInput("Unesite elemente druge matrice po retcima:",
m, n, b);
int c[m][n];
mAdd(m, n, a, b, c);
for (i = 0; i < m; i++) {
for (j = 0; j < n; j++) {
printf("%2d ", c[i][j]);
}
printf("\n");
}
}
Program 54. ZbrajanjeMatrica.c

Format "%3d " osigurava da se svaki element matrice zauzme barem tri slovna mjesta
prilikom ispisa iza kojih slijedi jedna praznina, pa će se za rezultat u kojemu svi elementi
matrice nemaju isti broj znamenki ispisati pravilna tablica
21 132 3
4 15 26
Ispis istih vrijednosti po formatu "%d " izgledao bi neuredno:
21 132 3
4 15 26
jer tako svaki element ispisa zauzima upravo onoliko slovnih mjesta koliko ima decimala.
Naravno, za veće brojeve bilo bi potrebno povećati broj slovnih mjesta kod ispisa.
Važnost točnosti dimenzija polja predanih funkciji može se ilustrirati sljedećim
primjerom. Sljedeći program

187
Uvod u programiranje Polja

#include <stdio.h>
void f(int m, int n, int a[m][n]) {
printf("%d", a[1][1]);
}
int main(void) {
int m=3, n=2;
int a[m][n];
// polja varijabilne veličine nije moguće inicijalizirati
// u definiciji
a[0][0] = 1;
a[0][1] = 2;
a[1][0] = 3;
a[1][1] = 4;
a[2][0] = 5;
a[2][1] = 6;
f(2, 3, a); // predaju se neispravne dimenzije matrice
}
Program 55. NeispravneDimenzije.c

umjesto očekivane vrijednosti 4, ispisat će 5.


Polje je inicijalizirano vrijednostima
1 2
3 4
5 6
koje su u memoriji smještene kao
1 2 3 4 5 6
U funkciju se predaje neispravna duljina retka, n=3 umjesto stvarnih n=2, pa prevodilac
adresu elementa polja a[1][1] računa kao
a + 1 * 3 + 1 = a + 4.
Na poziciji koja je za 4 cijela broja udaljena od početka polja nalazi se upravo vrijednost
5.
Da je funkcija dobila točnu duljinu n=2, adresa elementa polja a[1][1] izračunala bi se kao
a + 1 * 2 + 1 = a + 3.
Na poziciji koja je za 3 cijela broja udaljena od početka polja nalazi vrijednost 4 koja i jest
zadana za element a[1][1].
S obzirom na činjenicu da se prva dimenzija polja (stvarni broj redaka m) u funkciji uopće
ne koristi, čak ni implicitno tijekom određivanja adrese elementa polja, može ga se
izostaviti, pa će sljedeća funkcija, ako je predan ispravan n, raditi dobro:
void f(int n, int a[][n]) {
printf("%d", a[1][1]);
}
Kod polja fiksne veličine kojima se broj stvarno korištenih elemenata u pravilu razlikuje
od fizičkih dimenzija polja, funkciji treba predati informaciju i o jednome i o drugome. Ovo
će se ilustrirati na primjeru funkcije koja množi dvije matrice.
Množenjem matrica
C=A*B

188
Uvod u programiranje Polja

gdje je A matrica veličine m ∙ n, a B matrica veličine n ∙ o, nastaje matrica C veličine m ∙ o


kojoj su elementi definirani izrazom
n−1

cij = ∑ 𝑎𝑖𝑘 ∙ bkj , za svaki i = 0, … , 𝐦 − 1 𝑖 𝑗 = 0, … , 𝑜 − 1


k=0
Drugim riječima, element cij rezultata umnožak je i-tog retka prve i j-tog stupca druge
matrice, pa broj stupaca prve matrice mora biti jednak broju redaka druge matrice.
Radi jednostavnosti, sve tri matrice bit će dimenzionirane kao kvadratne, veličine 100 ∙
100, a konkretne dimenzije matrice i same vrijednosti elemenata matrice, umjesto
učitavanjem, bit će zadane u definicijama. Kad bi se dimenzije matrica razlikovale (uz
nužan uvjet da je broj redaka matrice c veći ili jednak broju redaka matrice a, a broj
stupaca matrice c veći ili jednak broju stupaca matrice b), bilo bi nužno u funkciju
proslijediti fizičku duljinu retka svake od njih. Kako to ovdje nije slučaj, dovoljno je
proslijediti duljinu retka d, koja je jednaka za sve tri matrice.
#include <stdio.h>
#define MAX 100
void mMult(int m, int n, int o, int d,
int a[][d], int b[][d], int c[][d]) {
int i, j, k;
for (i = 0; i < m; i++) {
for (j = 0; j < o; j++) {
c[i][j] = 0;
for (k = 0; k < n; k++) {
c[i][j] += a[i][k] * b[k][j];
}
}
}
}
int main(void) {
int i, j, m = 2, n = 3, o = 4;
int a[MAX][MAX] = {{1, 2, 3},
{4, 5, 6}};
int b[MAX][MAX] = {{ 7, 8, 9, 10},
{11, 12, 13, 14},
{15, 16, 17, 18}};
int c[MAX][MAX];
mMult(m, n, o, MAX, a, b, c);
for (i = 0; i < m; i++) {
for (j = 0; j < o; j++) {
printf("%3d ", c[i][j]);
}
printf("\n");
}
}
Program 56. MnozenjeMatrica.c

189
Uvod u programiranje Složeni podaci

Slozeni podaci
Strukture
Do sada spomenute varijable sadržavale su pojedinačne vrijednosti, ili se, kao što je to bilo
u slučaju polja, radilo o skupu podataka istoga tipa koji su u memoriji računala smješteni
jedan iza drugoga. U praksi se neki podatak često sastoji od više komponenata. Točka u
dvodimenzijskom koordinatnom sustavu definirana je koordinatama x i y, u
trodimenzijskom sustavu njima se dodaje i z. Datum se sastoji od dana, mjeseca i godine.
Programski jezik C omogućava opisivanje takvih složenih podataka naredbom struct,
primjerice
struct tocka {
double x;
double y;
};
ili
struct datum {
char dan;
char mjesec;
short godina;
};
Ovako napisane naredbe su deklaracije: opisuju kako će podatak tipa struct tocka ili struct
datum izgledati, ali ne definiraju stvarne varijable. Sljedeće naredbe predstavljaju stvarnu
definiciju varijabli:
struct tocka t1, t2;
struct datum prviSvibanj, uskrs;
Deklaracija i definicija mogu se objediniti:
struct tocka {
double x;
double y;
} t1, t2;
Moguća je i definicija kod kojeg se tipu podatka ne daje ime:
struct {
double x;
double y;
} t;
Ovdje se definira varijabla t koja se također sastoji od komponenata x i y, ali se ovakav
pristup ne preporuča, jer je semantički manje jasan, a opis strukture ne može se ponovno
iskoristiti kod neke druge varijable.
Pojedinačne komponente strukture referenciraju se operatorom pristupa elementu
strukture točka (.), u općenitom obliku kao
složena_varijabla.komponenta
pa se tako datum Uskrsa može zadati naredbama
uskrs.dan = 21;
uskrs.mjesec = 4;
uskrs.godina = 2019;

190
Uvod u programiranje Složeni podaci

a koordinate točaka učitati naredbom


scanf("%lf%lf%lf%lf", &t1.x, &t1.y, &t2.x, &t2.y);
Koordinate x i y unutar strukture su skalarne varijable i stoga se funkciji scanf treba, kao
i u slučaju bilo koje druge skalarne varijable, predati njihova adresa.
Struktura se može inicijalizirati u definiciji navođenjem vrijednosti elemenata u vitičastim
zagradama:
struct datum d = {1, 5, 2001};
Moguće je inicijalizirati i manji broj elemenata nego što ih ima u deklaraciji strukture, pri
čemu se preostali elementi pune nulama pa
struct datum d = {1};
printf("%d.%d.%d\n", d.dan, d.mjesec, d.godina);
ispisuje
1.0.0
Takođe, moguće je eksplicitno navesti element strukture od kojeg započinje inicijalizacija
kao u primjeru
struct datum d = {.mjesec=10, 2001};
printf("%d.%d.%d\n", d.dan, d.mjesec, d.godina);
što će ispisati
0.10.2001
Struktura se funkciji predaje preko vrijednosti, koliko god velika ili složena bila. Sljedeći
primjer računa udaljenost između dviju točaka (x1, y1) i (x2, y2) po formuli
𝑑 = √(𝑥1 − 𝑥2 )2 + (𝑦1 − 𝑦2 )2
#include <stdio.h>
#include <math.h>
struct tocka {
double x;
double y;
};
double udaljenost(struct tocka t1, struct tocka t2) {
return sqrt(pow(t1.x-t2.x, 2) + pow(t1.y-t2.y, 2));
}
int main(void) {
struct tocka t1, t2;
scanf("%lf%lf%lf%lf", &t1.x, &t1.y, &t2.x, &t2.y);
printf("%f\n", udaljenost(t1, t2));
}
Program 57. UdaljenostTocaka.c

Funkcija iz prethodnog primjera, dakle, na stogu rezervira prostor za t1 i t2. Tijekom


poziva, svi elementi strukture t1 iz glavnog programa kopiraju se u t1 iz funkcije. Isto
vrijedi za t2.
Rezultat funkcije također može biti struktura. U sljedećem primjeru izračunat će se
sjecište dvaju pravaca y = a1x+b1 i y = a2x+b2
Na sjecištu dvaju pravaca y koordinate točaka obaju pravaca bit će jednake:
a1x+b1 = a2x+b2

191
Uvod u programiranje Složeni podaci

iz čega slijedi
x =(b2 – b1) / (a1 - a2)
i
y = a1x+b1 ili y = a2x+b2
Osim strukture tipa tocka, ovdje će se uvesti i struktura tipa pravac. Ako su pravci
paralelni, nazivnik izraza koji određuje x koordinatu sjecišta je nula. x koordinata sjecišta
je tada ∞, što se može ispitati uspoređivanjem sa simboličkom konstantom INFINITY
definiranom u <math.h>. Kako IEE 754 standard razlikuje +∞ i -∞, potrebno je usporediti
s obje vrijednosti, jer kod dijeljenja s nulom predznak rezultata ovisi o predznaku
brojnika.
#include <math.h>
struct pravac {
double a;
double b;
};
struct tocka {
double x;
double y;
};
struct tocka sjeciste(struct pravac p1, struct pravac p2) {
struct tocka s;
s.x = (p2.b - p1.b) / (p1.a - p2.a);
s.y = p1.a * s.x + p1.b;
return s;
}
int main(void) {
struct tocka s;
struct pravac p1, p2;
scanf("%lf%lf%lf%lf", &p1.a, &p1.b, &p2.a, &p2.b);
s = sjeciste (p1, p2);
if (s.x == INFINITY || s.x == -INFINITY)) {
printf("Pravci su paralelni\n");
} else {
printf("Pravci se sijeku u točki (%g,%g)\n", s.x, s.y);
}
}
Program 58. SjecistePravaca.c

Struktura može sadržavati polje i/ili drugu strukturu. U sljedećem primjeru deklarirat će
se struktura koja definira samo elementarne podatke za neku osobu
#define MAXIME 40
struct osoba {
char ime[MAXIME];
char prezime[MAXIME];
float visina;
struct datum datumRodjenja;
};
Ime polja unutar strukture je pokazivač na prvi element toga polja, tj. adresa prvog
elementa toga polja. Zato se prilikom učitavanja funkcijom scanf uz takve elemente
strukture ne treba pisati adresni operator:

192
Uvod u programiranje Složeni podaci

struct osoba o;
scanf("%s %s %f \n", o.ime, o.prezime, &o.visina);
Elementi strukture unutar strukture referenciraju se također točkom:
o.datumRodjenja.dan = 1;
o.datumRodjenja.mjesec = 10;
o.datumRodjenja.godina = 2000;
Dubina hijerarhije u definiciji strukture nije ograničena. Pretpostavimo da se za nekog
studenta, osim osobnih podataka, žele pamtiti i osobni podaci roditelja te ocjene iz 50
predmeta. Takva struktura mogla bi se, uz korištenje prethodno opisanih deklaracija,
deklarirati na sljedeći način:
#define MAXOCJENA 50
struct student {
struct osoba osobni;
struct osoba majka;
struct osoba otac;
char ocjene[MAXOCJENA];
};
Godina rođenja studentove majke postavlja se na sljedeći način:
struct student ivan;
ivan.majka.datumRodjenja.godina = 1980;
Veličina strukture može se doznati primjenom operatora sizeof nad tipom strukture,
primjerice
printf("%d %d %d\n", sizeof (struct datum),
sizeof (struct osoba), sizeof (struct student));
ili nad konkretnim varijablama
struct datum d;
struct osoba o;
struct student s;
printf("%d %d %d\n", sizeof (d), sizeof (o), sizeof (s));
U obje varijante ispis će biti
4 88 316
Datum se sastoji od dva podatka tipa char i jednog podatka tipa short, dakle od 1+1+2 = 4
bajta. Podaci o osobi sastoje se od dva znakovna polja veličine 40, jednog podatka tipa
float i jednog datuma, dakle od 40+40+4+4 = 88 bajtova. Podaci o studentu sastoje se od
podataka o tri osobe i znakovnog polja veličine 50 i trebali bi biti veliki 3 · 88 + 50 = 314
bajta. Prevodilac, međutim, zbog učinkovitijeg pristupa memoriji veličinu strukture
zaokružuje na prvi veći višekratnik od 2 ili 487.
I ovako velike strukture funkcijama će se predati preko vrijednosti, što znači da će u
funkciji nastati kopija svih podataka strukture s nadređene razine. To znači veći utrošak
memorije, dulje vrijeme poziva funkcije jer se podaci moraju fizički kopirati te
nemogućnost promjene elementa strukture u funkciji. Zato je strukture funkcijama bolje
predati preko pokazivača.

87 Ovo se ponašanje može promijeniti naredbom predprocesora


#pragma pack n
čime će se veličina strukture zaokružiti na višekratnik od n, ili opcijom prevodioca -fpack-struct=n

193
Uvod u programiranje Složeni podaci

Pokazivač na strukturu definira se slično kao i pokazivač na varijablu, navođenjem tipa,


zvjezdice te naziva pokazivača, a adresa strukture dobije se standardnom primjenom
adresnog operatora:
struct datum d;
struct datum *p;
p = &d;
Kao i kod skalarnih varijabli, definicija pokazivača može se čitati kao
struct datum * p;
odnosno "p je pokazivač na strukturu tipa datum"
ili kao
struct datum *p;
odnosno "*p je struktura tipa datum". Tako se preko pokazivača do elementa strukture
može doći dereferenciranjem pokazivača i primjenom operatora pristupa elementu
strukture kao
(*p).godina = 2019;
Treba primijetiti da su zagrade obvezne jer operator pristupa elementu strukture ima veći
prioritet od operatora dereferenciranja tako da se
*p.godina
evaluira kao
*(p.godina)
što uzrokuje grešku kod prevođenja.
Bolje je, međutim, koristiti posebni operator pristupa elementu strukture preko
pokazivača (->) kao u sljedećem primjeru:
p->godina = 2019;
Pokušajmo sada, uz prije opisane deklaracije, napisati funkciju koja će izračunati prosjek
ocjena za zadanog studenta, uz pretpostavku da ocjena nula ne ulazi u prosjek jer
označava predmet koji student još nije polagao.
float prosjek (struct student *s) {
int sum = 0, i, n = 0, prosjek;
for (i = 0; i < MAXOCJENE; i++) {
if (s->ocjene[i] > 0) {
sum += s->ocjene[i];
n++;
}
}
return n == 0 ? 0 : (float)sum/n;
}
Program 59. StudentProsjekOcjena.c

Simbolička konstanta MAXOCJENE može se koristiti jer se njome deklarira struktura tipa
student. Ta definicija mora biti dostupna prevodiocu tijekom prevođenja, ili tako da je
zapisana u na početku datoteke s izvornim kodom funkcije prosjek, ili tako da je u nju
uključena naredbom #include. Alternativno, veličinu polja ocjena može se saznati
operatorom sizeof:
sizeof(s->ocjene)

194
Uvod u programiranje Složeni podaci

Za ocjene tipa char rezultat bi bio ispravan, ali promjena tipa pojedinačne ocjene u npr.
short uzrokovala bi grešku jer sizeof uvijek vraća veličinu podatka u bajtovima, pa je u
slučaju primjene toga operatora nad cijelim poljem s ciljem saznavanja veličine polja
rezultat uvijek potrebno podijeliti s veličinom prvog elementa:
sizeof(s->ocjene)/sizeof(s->ocjene[0])
Struktura može biti i elementom polja. Sljedećom naredbom definira se polje točaka
veličine 1000.
struct tocka a[1000];
Element polja je struktura, pa se koordinate prve točke u takvom polju referenciraju
primjenom operatora pristupa elementu strukture:
a[0].x = 0;
a[0].y = 0;
Načinimo sada funkciju koja će vratiti pokazivač na onu točku iz polja točaka koja je
najbliža ishodištu koordinatnog sustava glavni te program koji će napuniti polje točaka
slučajnim koordinatama u intervalu [0, 3.2767] te ispisati koordinate točke najbliže
ishodištu.
#include <stdio.h>
#include <math.h>
#include <time.h>
#include <stdlib.h>
#define MAX 1000
struct tocka {
double x;
double y;
} ;
double udaljenost(struct tocka t1, struct tocka t2) {
return sqrt(pow(t1.x - t2.x, 2) + pow(t1.y - t2.y, 2));
}
struct tocka najbliza(int n, struct tocka a[]) {
struct tocka ishodiste = {0, 0};
double d, mind;
int pos = 0; // prva točka je najbliža
mind = udaljenost(a[0], ishodiste); // udaljenost prve točke
for (int i = 1; i < n; i++) { // po svim ostalim točkama
d = udaljenost(a[i], ishodiste);
if (d < mind) {// udaljenost točke manja je od minimalne
mind = d; // nova najmanja udaljenost
pos = i; // indeks najbliže točke
}
}
return a[pos];
}
int main(void) {
struct tocka a[MAX], r;
srand(time(NULL));
int i;
for (i = 0; i < MAX; i++) {
a[i].x = rand() / 10000.; // [0,32767] -> [0,3.2767]
a[i].y = rand() / 10000.;
}
r = najbliza(MAX, a);

195
Uvod u programiranje Složeni podaci

printf("%f %f\n", r.x, r.y);


}
Program 60. NajblizaTocka.c

Prilikom povratka iz funkcije obavlja se kopiranje cijelog sadržaja elementa polja a s


pozicije na kojoj je pronađena najmanja udaljenost u strukturu r u glavnom programu.
Kod malih struktura to je prihvatljivo. U primjeru u kojemu je u polju studenata potrebno
pronaći studenta s najboljim prosjekom ocjena, bolje je vratiti pokazivač na strukturu. Uz
već prije opisanu strukturu tipa student i ostvarenu funkciju prosjek, funkcija je
struct student *najbolji(int n, struct student studenti[]) {
float najPros = prosjek(&studenti[0]), pros;
int pos = 0;
for (int i=1; i < n; i++) {
pros = prosjek(&studenti[i]);
if (pros > najPros) {
najPros = pros;
pos = i;
}
}
return &studenti[pos];
}
U glavnom programu definira se polje, poziva funkcija i ispisuje ime i prezime najboljeg
studenta

#define BROJSTUDENATA 1000
struct student studenti[BROJSTUDENATA];
int n; //stvarni broj studenata
… // inicijalizacija vrijednosti
struct student *naj;
naj = najbolji(n, studenti);
printf("%s %s\n", naj->osobni.ime, naj->osobni.prezime);
}
Program 61. NajboljiStudent.c

Prioriteti oba operatora pristupa elementu strukture (. i ->) su jednaki, a asocijativnost je


s lijeva na desno. Dakle, prvo se pristupa strukturi osobni operatorom pristupa elementu
strukture preko pokazivača. osobni je struktura unutar strukture na koju pokazuje
pokazivač pa se imenu i prezimenu iz te strukture pristupa operatorom točka (.).
Strukture se često kombiniraju s typedef. Podsjetimo se, typedef daje novo ime poznatom
tipu podatka općenitom sintaksom
typedef poznati_tip novo_ime;
npr.
typedef int brojac;
Ako se umjesto poznati_tip raspiše deklaracija strukture
typedef struct {
double x;
double y;
} tocka;
definirano je novo ime tocka za

196
Uvod u programiranje Složeni podaci

struct {
double x;
double y;
}
pa se to novo ime može koristiti u definicijama varijabli ili funkcija primjerice
tocka t;
tocka *p;
tocka sjeciste(pravac p1, pravac p2) {
….
U posljednjem primjeru pretpostavlja se da je i struktura tipa pravac definirana kao
typedef struct {
double a;
double b;
} pravac;
Primjena strukture i pokazivača na strukturu može se ilustrirati i na korisnoj sistemskoj
funkciji ftime pomoću koje se može realizirati mjerenje vremena proteklog tijekom
izvođenja nekog programskog odsječka.
Funkcija ftime deklarirana je u <sys/timeb.h> i ima prototip
int ftime (struct timeb *tp);
Vraća nulu ako je pozvana uspješno, inače -1. U strukturu tipa timeb čija se adresa predaje
kao argument pohranit će vrijeme proteklo od 1.1.1970. ali s više informacija nego što ih
daje funkcija time. Struktura se sastoji od sljedećih elemenata:
time_t time broj sekundi
unsigned short millitm broj milisekundi
short timezone vremenska zona izražena razlikom u odnosu na
UTC u minutama
short dstflag TRUE ako je na snazi ljetno računanje vremena
Tipičan primjer korištenja je

#include <sys/timeb.h>

struct timeb t1, t2;
int protekloMilisekundi;

ftime (&t1);
… // programski odsječak čije se trajanje želi izmjeriti
ftime (&t2);
protekloMilisekundi = t2.time*1000 + t2.millitm –
(t1.time*1000 + t1.millitm);

Ne treba iznenaditi ako rezultat bude nula. Današnja računala u jednoj milisekundi mogu
načiniti mnogo posla jer je vrijeme obavljanja jedne instrukcije na tipičnom osobnom
računalu reda veličine nanosekunde. Zato se mjerenje trajanja bržih programskih

197
Uvod u programiranje Složeni podaci

odsječaka izvodi tako da se programski odsječak for petljom ponovi tisuću ili milijun puta,
pa se rezultat podijeli s tisuću ili milijun88.

Unije
Unije su složene strukture u kojima elementi dijele istu memorijsku lokaciju. Kod
strukture
struct s {
int a;
float b;
};
varijabla b bit će u memoriji smještena neposredno iza varijable b, što se može pokazati
ispisom adresa. Odsječak
struct s vs;
printf("%u %u\n", &vs.a, &vs.b);
ispisuje
6422308 6422312
Unija se deklarira vrlo sličnom sintaksom
union t {
int a;
float b;
};
ali kod unije svi elementi dijele istu adresu. Odsječak
union t vt;
printf("%u %u\n", &vt.a, &vt.b);
ispisuje
6422304 6422304
Veličina unije jednaka je veličini najvećeg od elemenata. Odsječak
union z {
char a;
short b;
int c;
};
printf("%d\n", sizeof(union z));
ispisuje
4
Unije mogu poslužiti kad je potreban različiti pogled na isti sadržaj memorije, kao u
sljedećem primjeru gdje se cjelobrojna varijabla može promatrati kao cjelina, ali i kao 4
pojedinačna bajta.
#include <stdio.h>
struct bajtovi {
unsigned char b1;
unsigned char b2;
unsigned char b3;

88Postoje manje prenosive funkcije, primjerice gettimeofday, koje mogu izmjeriti vrijeme do razine
mikrosekunde, ali ne i preciznije

198
Uvod u programiranje Složeni podaci

unsigned char b4;


};
union registar {
int i;
struct bajtovi b;
};
int main(void) {
union registar w;
w.i = 255 + 256;
printf("%d %d %d %d\n", w.b.b1, w.b.b2, w.b.b3, w.b.b4);
w.b.b1 = 0;
printf("%d\n", w.i);
}
Program 62. UnionBajtovi.c

Ispisuje se
255 1 0 0
256

Skupine bitova
Pretpostavimo da se u nekom grafičkom sučelju znakovi mogu ispisati u jednoj od 16
različitih boja i na razne načine naglašeni (kurziv, podvlaka i sl.). Uz svaki se znak, osim
ASCII vrijednosti u njemu pohranjene, u tome slučaju treba pratiti još i određen broj
atributa, pa bi ga se moglo predstaviti strukturom
#include <stdbool.h>
struct character {
char value;
bool italic;
bool bold;
bool underlined;
bool overstrike;
char color;
};
Svaki od ovih atributa, međutim, zauzima po jedan bajt, pa je ukupna veličina ove
strukture 6 bajta.
Za svaku od logičkih vrijednosti trebao bi biti dovoljan 1 bit, a za prikaz 16 različih
vrijednosti boja dovoljna su 4 bita.
C ima mogućnost definiranja skupina bitova (bit fields) tako da se uz svaki element
strukture navede koliko najviše bitova taj element strukture zauzima. Sintaksa je
osnovni_tip_podatka naziv_elementa : n;
gdje je n broj bitova potreban za pohranu elementa strukture. Skupine bitova mogu
sadržavati samo cjelobrojne podatke ili podatke tipa _Bool odnosno bool (ako se uključi
<stdbool.h>).
Znak iz prethodnog primjera mogao bi se pohraniti u samo 2 bajta uz deklaraciju
#include <stdbool.h>
struct packedCharacter {
char value;
bool italic:1;
bool bold:1;

199
Uvod u programiranje Složeni podaci

bool underlined:1;
bool overstrike:1;
unsigned char color:4;
};
Program 63. SkupineBitova.c

Skupini bitova može se se pristupiti kao i svakom drugom elementu strukture, primjerice
struct packedCharacter c;
c.color = 11;
c.italic = true;
Skupina bitova ne može započeti unutar jednog podatka osnovnog tipa i završiti unutar
sljedećeg podatka osnovnog tipa, pa će zato struktura
struct t1 {
char a : 5;
char b : 5;
char c : 5;
char d : 5;
char e : 5;
char f : 5;
char g : 5;
char h : 5;
char i : 5;
char j : 5;
char k : 5;
char l : 5;
};
zauzimati 12 bajta, jer svaka skupina započinje u sljedećem bajtu, dok će struktura
struct t1 {
short a : 5;
short b : 5;
short c : 5;
short d : 5;
short e : 5;
short f : 5;
short g : 5;
short h : 5;
short i : 5;
short j : 5;
short k : 5;
short l : 5;
};
zauzimati 8 bajta jer se po 3 skupine mogu ugraditi u 16 bita koliko zauzima short.
Deklaracija skupine bitova koja nema ime i ima veličinu 0 uzrokuje da sljedeća skupina
bitova započne tamo gdje započinje sljedeći osnovni podatak. Sljedećom deklaracijom
skupina b započinje u sljedećem bajtu strukture:
struct t {
char a:2;
char :0;
char b:3;
};

200
Uvod u programiranje Složeni podaci

Kako skupina bitova ne mora započeti na granici bajta, na nju se ne može primijeniti
adresni operator. Veličina skupine može biti manja od bajta, pa se ne može primijeniti ni
operator sizeof, koji vraća veličinu podatka u bajtovima.

201
Uvod u programiranje Ulaz i izlaz

Ulaz i izlaz
Do sada je za unos podataka u program korištena tipkovnica, a za ispis rezultata programa
naredbeni prozor operacijskog sustava. Ove se dvije ulazno/izlazne jedinice nazivaju
standardnim ulazom odnosno standardnim izlazom. Spomenute su bile samo funkcije
scanf i printf, koje obavljaju formatirani ulaz i izlaz. Prvi argument tih funkcija je niz
znakova nazvan formatom. Format sadrži konverzijske specifikacija koje određuju način
konverzije između niza znakova na ulazu odnosno izlazu i binarnog prikaza konkretnih
varijabli u memoriji računala.
Za komunikaciju sa standardnim ulazom i izlazom u standardnoj biblioteci programskog
jezika C ugrađene su još i funkcije koje učitavaju odnosno ispisuju jedan znak te funkcije
koje učitavaju odnosno ispisuju niz znakova, sve opisane prototipom u <stdio.h>

Unos i ispis pojedinačnog znaka


Unos i ispis pojedinačnog znaka obavlja se funkcijama
int getchar(void);
odnosno
int putchar(int ch);
Funkcija getchar vraća kôd učitanoga znaka, a putchar argument prosljeđuje na
standardni izlaz. U slučaju da je kod ispisa došlo do greške, putchar će vratiti EOF. EOF je
simbolička konstanta definirana u <stdio.h> kao
#define EOF (-1)
Sljedeći program prepisuje znakove utipkane na tipkovnicu u naredbeni prozor, sve dok
se ne utipka EOF. EOF se na Windowsima utipkava kombinacijom tipaka Ctrl-Z, a na
Linuxu kombinacijom Ctrl-D89.
#include <stdio.h>
int main(void) {
char c;
while ((c = getchar()) != EOF) {
putchar (c);
}
return 0;
}
Program 64. getchar.c

Valja naglasiti da se utipkani znakovi odmah vide u prozoru, ali da ih operacijski sustav
ne prosljeđuje istovremeno s utipkavanjem na obradu programu, nego ih sprema u
međuspremnik (buffer) sve dok se na tipkovnici ne utipka tipka Enter. Tek se u tome
trenutku cijeli sadržaj međuspremnika, uključujući i znak '\n' koji je posljedica
utipkavanja Enter, predaje na obradu.
Rezultat bi mogao biti
ABCD↵ unos
ABCD ispis

89getchar će vratiti -1 samo ako je Ctrl-Z jedini znak u retku prije Enter. Inače, vraća se zamjenski znak s
ASCII vrijednošću 26 (SUB) koji po konvenciji predstavlja nevaljali znak ili grešku.

202
Uvod u programiranje Ulaz i izlaz

EF↵ unos
EF ispis
Ctrl-Z↵ unos
Ako se gornji program preuredi tako da se prije svakog unesenog znaka ispisuje i njegov
ASCII kôd, ponašanje će biti još jasnije:
#include <stdio.h>
int main(void) {
char c;
do {
c = getchar();
printf(" %d %c", c, c);
} while (c != EOF);
return 0;
}
Program 65. getcharDo.c

Ispisat će se
ABCD↵ unos
65 A 66 B 67 C 68 D 10 ispis
EF↵ unos
69 E 70 F 10 ispis
Ctrl-Z↵ unos
-1 ispis

Funkcija int getch(void) iz <conio.h> odmah predaje utipkani znak na obradu. Utipkani
znakovi ne vide se automatski u prozoru. Ova funkcija, međutim, za Enter vraća '\r' (0x0D,
CR, carriage return) pa taj znak treba pretvoriti u '\n'. Također, za Ctrl-Z uvijek se vraća
26 (SUB) a Ctrl-C ne prekida program nego ga funkcija vraća kao vrijednost 3 (Ctrl-A → 1,
Ctrl-B → 1 itd.). Program koji prepisuje utipkano u prozor s getch() sve dok se ne utipka
Ctrl-C izgledao bi ovako
#include <stdio.h>
#include <conio.h>
int main(void) {
char c;
while ((c = getch()) != 3) {
putchar (c == '\r' ? '\n' : c);
}
return 0;
}
Program 66. getch.c

Unos i ispis niza


U starijim standardima programskog jezika C postoji funkcija
char *gets (char *str);
koja čita znakove sa standardnog ulaza na poziciju na koju pokazuje str sve dok se ne
utipka Enter i tada ih predaje na obradu programu. Enter ('\n') se ne ugrađuje u niz, ali se
zato na kraj niza dodaje '\0'. Funkcija vraća pokazivač na početak utipkanog niza, osim
ako je kao jedini znak prije Enter utipkan EOF, kada vraća NULL.
Funkcija
203
Uvod u programiranje Ulaz i izlaz

int puts(const char *str);


ispisuje na standardni izlaz niz na koji pokazuje str. Nakon što ispiše niz, ispisuje još i '\n'.
Ako se pri ispisu dogodila greška, funkcija vraća EOF. Sljedeći program prepisuje utipkano
u prozor:
#include <stdio.h>
#define MAX 80
int main(void) {
char redak[MAX], *p;
while ((p = gets(redak))) {
puts(redak); // ili puts(p) ili printf("%s\n", p)
}
return 0;
}
Program 67. gets.c

Rezultat će biti
Prvi redak↵ unos
Prvi redak ispis
Drugi redak↵ unos
Drugi redak ispis
Ctrl-Z↵ unos
Treba primijetiti da je skok u novi redak između unosa i ispisa posljedica činjenice da
operacijski sustav utipkane znakove automatski prepisuje na izlaz, pa se to događa i s
Enter. Novi redak između ispisa i sljedećeg unosa posljedica je činjenice da puts dodaje
'\n'. Sljedećim programom može se dokazati da u niz nije ugrađen '\n':
#include <stdio.h>
#define MAX 80
int main(void) {
char redak[MAX], *p;
while ((p = gets(redak))) {
while (*p) printf("%d ", *(p++));
}
return 0;
}
Program 68. gets1.c

Rezultat je
ABCD↵ unos
65 66 67 68 ispis

U sljedećem primjeru prepisivat će se uneseni retci na standardni izlaz sve dok se ne
utipka tekst gotovo
#include <stdio.h>
#include <string.h>
#define MAX 80
int main(void) {
char redak[MAX], *p;
while ((p = gets(redak))) {
if (strcmp(p, "gotovo") == 0) break;
puts(p);
}

204
Uvod u programiranje Ulaz i izlaz

return 0;
}
Program 69. getsGotovo.c

Rezultat je
Prvi redak↵ unos
Prvi redak ispis
Drugi redak↵ unos
Drugi redak ispis
gotovo↵ unos

Korištenje funkcije gets relativno je riskantno jer funkcija nema informaciju o tome koliko
je memorije rezervirano za polje u koje se smješta rezultat pa se, ako se utipka više
znakova nego što u polju ima mjesta, prijenos znakova s ulaza u memoriju nastavlja iza
polja preko varijabli koje se tamo nalaze. Zato je sa standardom C11 uvedena funkcija
char *gets_s(char *str, rsize_t n);
koja će s ulaza preuzeti najviše n-1 znak kako bi ostalo još i mjesto za '\0'. Nažalost,
standard ne propisuje obvezu ugradnje tako da je u gcc-u nema.
Funkcija gets trebala bi po standardu C11 biti uklonjena, ali gcc je još uvijek podržava, pa
je zato ovdje opisana. U ovome času bolje bi bilo ne koristiti je, nego umjesto nje koristiti
funkciju fgets opisanu u poglavlju Čitanje i pisanje tekstovnih datoteka.

Formatirani ulaz i izlaz


Formatirani ulaz i izlaz obavlja se funkcijama
int scanf(const char *format, ...);
i
int printf(const char *format, ...);
Ove su se funkcije veće mnogo puta pojavljivale u dosadašnjem tekstu u svome
elementarnom obliku i s elementarnim konverzijskim specifikacijama. U ovome poglavlju
te će funkcije biti detaljnije opisane.
I jedna i druga funkcija imaju varijabilni broj parametara. Kod obje funkcije prvi argument
bit će znakovni niz nazvan formatom, u kojem se konverzijskim specifikacijama koje se
prepoznaju po znaku % određuje način konverzije između znakovnih nizova (teksta) na
standardnom ulazu odnosno izlazu i binarnih vrijednosti pohranjenih u memoriji
računala. Slijedi varijabilni broj argumenata koji su kod funkcije scanf adrese na koje se
redom treba pohraniti vrijednosti na način opisan konverzijskim specifikacijama, a kod
funkcije printf vrijednosti koje tekst sa standardnog ulaza na način opisan konverzijskim
specifikacijama treba pretvoriti u tekst koji se prosljeđuju na standardni izlaz. Izuzetak je
ispis niza znakova za koji se funkciji printf treba predati njegova adresa. Broj argumenata
iza formata i jedne i druge funkcije mora odgovarati broju konverzijskih specifikacija.
scanf vraća broj učitanih vrijednosti, koji bi trebao odgovarati broju konverzijskih
specifikacija, a printf broj znakova proslijeđen na izlaz. Rezultat izvođenja sljedećeg
programa
#include <stdio.h>
int main(void) {
int a, b, c;

205
Uvod u programiranje Ulaz i izlaz

int nUcitano, nIspisano;


nUcitano = scanf("%d %d %d", &a, &b, &c);
nIspisano = printf("%d %d %d %d\n", nUcitano, a, b, c);
printf("%d\n", nIspisano);
return 0:
}
je
101 102 103↵ unos
3 101 102 103 ispis
14 ispis
jer su učitane tri vrijednosti, a kod ispisa je na standardni izlaz, uključujući i '\n' koji se
doslovno prepisuje, proslijeđeno ukupno 14 znakova. Jedna od situacija u kojima će scanf
vratiti rezultat manji od broja konverzijskih specifikacija je utipkavanje znaka EOF iza
kojega slijedi Enter, kao u sljedećem primjeru izvođenja istog programa:
1 2Ctrl-Z↵ Unos
2 1 2 4200987 Ispis
14 Ispis
Ovdje su utipkane vrijednosti samo za varijable a i b, a varijabla c ostala je
neinicijalizirana.
Vrijednosti na ulazu odjeljuju se bijelom prazninom (white space). Bijela praznina je jedan
ili više znakova iz sljedećeg skupa: praznina (' ', 0x20), tabulator ('\t', 0x09) ili skok u novi
red ('\n', 0x0a). Vrijednosti za a, b i c iz prethodnog programa mogle su se s jednakim
rezultatom unijeti i kao
101↵
102↵
103↵
ili
101 102 ↵
103
Konverzijske specifikacije mogu se i ne moraju unutar formata odvajati bijelom
prazninom.
scanf("%d %d %d", &a, &b, &c);
i
scanf("%d%d%d", &a, &b, &c);
radit će jednako. Za svaku započetu konverzijsku specifikaciju preskočit će se sve bijele
praznine na ulazu, započeti konverzija niza znakova s ulaza i nastaviti dok se god znakovi
mogu pretvarati po toj konverzijskoj specifikaciji. To znači da će prethodni program i za
101 102↵
103↵
ispisati
3 101 102 103
14

206
Uvod u programiranje Ulaz i izlaz

Ako se unutar ulaznog niza pronađe znak koji se po trenutačnoj konverzijskoj specifikaciji
ne može pretvoriti, funkcija prestaje s učitavanjem i vraća dotad obrađeni broj vrijednosti.
Za unos
101 .102 103↵
ispisat će se
1 101 4200896 4200987
jer točka u ulazu ne može biti obrađena konverzijskom specifikacijom %d.
U nešto složenijem primjeru
int i, j, k;
float x, y, z;
scanf("%d%d %f %f %f %d", &i, &j, &x, &y, &z, &k);
printf("%d %d %f %f %f %d", i, j, x, y, z, k);
za ulazne vrijednosti
38↵
-15.012 24+25 7.8↵
ispisat će se
38 -15 0.012000 24.000000 25.000000 7
Redom se pretvorba niza s ulaza u varijable obavlja ovako:
scanf("%d%d %f %f %f %d", &i, &j, &x, &y, &z, &w);
Preskaču se bijele praznine s ulaza i učitava vrijednost 38 do '\n' unesenog tipkom Enter.
scanf("%d%d %f %f %f %d", &i, &j, &x, &y, &z, &w);
Preskaču se bijele praznine s ulaza i učitava vrijednost -15 do točke koja se ne može
pretvoriti po %d.
scanf("%d%d %f %f %f %d", &i, &j, &x, &y, &z, &w);
Nema bijelih praznina koje treba preskočiti pa se učitava 0.012.
scanf("%d%d %f %f %f %d", &i, &j, &x, &y, &z, &w);
Preskače se bijela praznina pa se učitava 24 jer znak + ne može biti unutar broja.
scanf("%d%d %f %f %f %d", &i, &j, &x, &y, &z, &w);
Znak + može biti na početku broja pa se učitava +25.
scanf("%d%d %f %f %f %d", &i, &j, &x, &y, &z, &w);
Preskače se bijela praznina pa se učitava 7 do točke. Funkcija je obradila sve konverzijske
specifikacije pa će ostatak retka od točke na dalje biti obrađen sljedećim pozivom funkcije.
U konverzijskoj specifikaciji može se odrediti najveća duljina ulaznog niza koja će se po
toj konverzijskoj specifikaciji obraditi. U primjeru
scanf("%5d%5d%5d", &a, &b, &c);
za ulaz
12345678 12345678↵
naredba
printf("%d %d %d %d\n", nUcitano, a, b, c);
ispisat će

207
Uvod u programiranje Ulaz i izlaz

12345 678 12345


Ostatak ulaza (78) obradit će se sljedećim pozivom funkcije scanf.
Znakovni niz koji se učitava konverzijskom specifikacijom %s ne može sadržavati
praznine. Sljedeći program
#include <stdio.h>
int main(void) {
char s[128], t[128];
scanf("%s %s", s, t);
printf("\"%s\"\n", s);
printf("\"%s\"\n", t);
}
rezultira s
Jezik C↵ Unos
"Jezik" Ispis
"C" Ispis
Kombinacija \" unutar formata naputak je prevodiocu da znak " ne smatra završetkom
niza koji definira format nego da ga doslovno prepiše na izlaz.
I ovdje brojka iza % znači najveću duljinu koja se po toj konverzijskoj specifikaciji
obrađuje pa će
char r[128], s[128], t[128];
scanf("%5s%5s%5s", r, s, t);
printf("\"%s\"\n", r);
printf("\"%s\"\n", s);
printf("\"%s\"\n", t);
za ulaz
ABCDEFG ABCDEFG↵
ispisati
"ABCDE"
"FG"
"ABCDE"

208
Uvod u programiranje Ulaz i izlaz

Postoji, međutim, način da se učita niz znakova koji sadrži prazninu. Konverzijska
specifikacija
%[niz_znakova_dopušten_u_ulazu]
učitava niz s ulaza sve dok se ne pojavi prvi znak koji nije popisan u
niz_znakova_dopušten_u_ulazu
Tako će
scanf("%[ABCDEF]", niz);
printf("\"%s\"\n", niz);
za ulaz
AABBXYZ↵
ispisati
"AABB"
a ostatak retka ostaviti za sljedeću obradu.
Ova konverzijska specifikacija ne preskače bijele praznine, pa isti odsječak za ulaz
AA↵
ispisuje
"Ç@"
jer je praznina na ulazu odmah prekinula učitavanje pa u niz nije učitano ništa. U
konkretnom primjeru razvidno je da je na četvrtoj poziciji u polju niz zatečen znak '\0'
koji je prekinuo ispis.
Niz koji sadrži i prazninu očito bi se mogao unijeti tako da se u
niz_znakova_dopušten_u_ulazu upišu svi očekivani znakovi, uključujući i prazninu. To je,
naravno, nepraktično pa postoji oblik
%[^niz_znakova_nedopušten_u_ulazu]
koji se prepoznaje po znaku ^ iza uglate zagrade.
Ovdje svaki od znakova iz niz_znakova_nedopušten_u_ulazu prekida unos.
Cijeli redak, sa svim prazninama i ostalim znakovima može se, dakle, učitati sa
scanf("%[^\n]", niz);
što se može interpretirati kao "čitati dok se ne naiđe na '\n'".
Učitavanje ovom konverzijskom specifikacijom prekida se prije znaka nedopuštenog na
ulazu, pa sljedeći poziv funkcije scanf s istim argumentima neće pročitati ništa jer će ga na
početku ulaza odmah dočekati '\n'. '\n' treba preskočiti ili tako da ga se pročita u neku
varijablu koja se neće koristiti
scanf("%[^\n]%c", niz, &dummy);
ili, bolje, tako da se iskoristi modifikator konverzijske specifikacije * koji daje naputak da
se sljedeći ulazni niz preskoči po pravilima koji za takvu ulaznu specifikaciju vrijede. U
konkretnom slučaju znak '\n' se preskače sa
scanf("%[^\n]%*c", niz);
pa kompletan program koji redak za retkom sa standardnog ulaza prepisuje na standardni
izlaz sve dok se ne unese EOF glasi
#include <stdio.h>

209
Uvod u programiranje Ulaz i izlaz

int main(void) {
char niz[128];
while (scanf("%[^\n]%*c", niz) == 1) {
printf("%s\n", niz); // ili puts(niz);
}
return 0;
}
Program 70. scanf1.c

Slijedi još jedan primjer za konverzijsku specifikaciju *:


scanf("%d%*d%d", &i, &j);
printf("%d %d\n", i, j);
za ulaz
1 2 3↵
ispisuje se
1 3
Bijele praznine između konverzijskih specifikacija imaju utjecaj kod konverzijske
specifikacije %c. Ako se konverzijska specifikacija %c od prethodne ne odijeli bijelom
prazninom, bilo koji dio bijele praznine (praznina, tabulator, '\n') čitat će se kao svaki
drugi znak.
Odsječak
#include <stdio.h>
int main(void) {
char a, b, c;
while (scanf("%c%c%c", &a, &b, &c) == 3) {
printf("%d %d %d\n", a, b, c);
}
return 0;
}
Program 71. scanf2.c

ispisuje
A B↵ unos
65 32 66 ispis
CDE↵ unos
10 67 68 ispis
Ctrl-Z↵ unos
Nakon prvog unosa učitani su A, praznina i B. Znak '\n' koji je posljedica tipke Enter ostaje
nepročitan i zahvaća se drugim pozivom funkcije scanf koja učitava '\n', C, i D.
Ako se koristi konverzijska specifikacija %c, ali se ne želi učitavati znakovi koji tvore
bijelu praznine na ulazu, u formatu prije %c treba upisati prazninu. Prethodni program u
kojem je format "%c%c%c" zamijenjen s " %c %c %c" (primijetiti da ispred svake, pa i
prve, konverzijske specifikacije postoji praznina) ispisuje
A B C↵ unos
65 66 67 ispis

210
Uvod u programiranje Ulaz i izlaz

DEF↵ unos
68 69 70 ispis
Ctrl-Z↵ unos
Treba naglasiti da funkcija scanf nema nikakvu informaciju o tipu varijable čija joj se
adresa predaje kao argument, nego se način i duljina pretvorbe određuju isključivo preko
konverzijske specifikacije. To može uzrokovati neočekivano ponašanje poput odsječka
int x;
scanf("%f", &x);
printf("%d\n", x);
koji rezultira s
7↵ unos
1088421888 ispis
jer se 7 zbog specifikacije %f pretvara u binarni broj po standardu IEE754, da bi se tako
postavljeni bitovi kod ispisa zbog specifikacije %d smatrali potencijama broja 2.
Odsječak
short y, x;
printf("%d %d\n", x, y);
scanf("%d", &x);
printf("%d %d\n", x, y);
ima rezultat
128 64 ispis
7↵ unos
7 0 ispis
jer su prva 2 bajta na adresi varijable x ispravno postavljena, zahvaljujući činjenici da su
najmanje značajni bajtovi na najnižim adresama (Little Endian), ali funkcija mijenja i dva
bajta koja se u memoriji nalaze iza x, u ovom slučaju varijablu y.
Očito treba uvesti ili novu specifikaciju za pretvorbu u short, ili na neki načini modificirati
specifikaciju %d.
Duljina pretvorbe može se zadati modifikatorom duljine koji za tip short glasi h, pa će tako
ispravna naredba u prethodnom primjeru glasiti
scanf("%hd", &x);
Uz ovakvu specifikaciju y se ne mijenja i rezultat je
128 64 ispis
7↵ unos
7 64 ispis
Općeniti oblik konverzijske specifikacije za funkciju scanf glasi
%(*)(l)(m)t
% označava početak konverzijske specifikacije
* označava da podatak s ulaza treba preskočiti
l označava najveći broj znakova koji treba obuhvatiti učitavanjem

211
Uvod u programiranje Ulaz i izlaz

m označava modifikator duljine


t označava način konverzije
Komponente konverzijske specifikacije navedene u zagradama nisu obvezne.
Slijedi tablica vrijednosti za t 90 s pripadajućim tipom argumenta i za to potrebnim
modifikatorom duljine.
Način konverzije Napomena Tip argumenta Modifikator
duljine
c unsigned char * -
s char * -
[(^)skup_znakova] char * -
d cijeli broj zadan signed char * hh
dekadskim znamenkama short * h
int * -
long int * L
long long int * ll
u cijeli broj zadan kao %d, s kao %d
dekadskim znamenkama dodatkom
unsigned
o cijeli broj zadan oktalnim kao %d kao %d
znamenkama
x, X cijeli broj zadan kao %d kao %d
heksadekadskim
znamenkama
i baza broja zaključuje se iz kao %d kao %d
prvih znakova (0, 0x)
e, E, f, F, g, G realni broj zadan float * -
dekadskim znamenkama double * l
i, neobvezno, long double * L91
eksponentom

Općeniti oblik konverzijske specifikacije za funkciju printf glasi


%(-)(+)(praznina)(#)(0)(l ili *)(.p ili .*)(m)t
% označava početak konverzijske specifikacije
- označava da podatak treba biti lijevo pozicioniran unutar l znakovnih mjesta
+ označava da ispred svake pozitivne numeričke vrijednosti ispisati predznak +
praznina označava da će se ispred svake numeričke vrijednosti koja ne započinje
predznakom ispisati jedna praznina

90 Izostavljeni su rijetko korišteni načini konverzije a, A i p.


91 Učitavanje podatka tipa long double ne radi u gcc-u

212
Uvod u programiranje Ulaz i izlaz

# označava da će se ispis vrijednosti obaviti na alternativni način


0 označava da će se, ukoliko je broj znamenki zajedno s predznakom manji od zadanog
broja znakovnih mjesta za ispis l, numerički podatak ispisati s vodećim nulama
l označava najmanji broj znakovnih mjesta za ispis vrijednosti. Ako se umjesto konkretne
vrijednosti napiše *, broj znakovnih mjesta za ispis zadaje se dodatnim argumentom
funkcije.
.p označava preciznost ispisa zadanu brojem decimalnih mjesta ili brojem znakova. Ako
se umjesto konkretne vrijednosti napiše *, preciznost ispisa zadaje se dodatnim
argumentom funkcije.
m označava modifikator duljine
t označava način konverzije

Način konverzije Napomena Tip argumenta Modifikator


duljine
c unsigned char -
s char * -
d, i cijeli broj ispisan signed char hh
dekadskim znamenkama short h
int -
long int L
long long int ll
u cijeli broj bez predznaka kao %d, s kao %d
ispisan dekadskim dodatkom
znamenkama unsigned
o cijeli broj ispisan kao %d kao %d
oktalnim znamenkama
x, X cijeli broj ispisan kao %d kao %d
heksadekadskim
znamenkama. Za x,
znamenke veće od 9
ispisuju se malim slovima,
za X velikim
f, F realni broj ispisan float -
dekadskim znamenkama double -
long double L
e, E realni broj ispisan kao %f kao %f
dekadskim znamenkama i
eksponentom. Za e slovo
u eksponentu bit će malo,
inače veliko
g, G realni broj ispisan kao %f kao %f
dekadskim znamenkama

213
Uvod u programiranje Ulaz i izlaz

i, ovisno o preciznosti i
veličini broja,
eksponentom. Za g slovo
u eksponentu bit će malo,
inače veliko
p adresa ispisana void * -
heksadekadskim
znamenkama
Slijedi primjer ispisa po različitim tipovima konverzije
int v;
printf("|%d|%o|%x|%X|\n",
123, 123, 123, 123);
printf("|%f|%F|%e|%E|%g|%G|\n",
10., 10., 10., 10., 10., 10.);
printf("|%f|%F|\n|%e|%E|\n|%g|%G|\n",
1234567890., 1234567890.,
1234567890., 1234567890.,
1234567890., 1234567890.);
printf("|%p|\n", &v);
Ispisuje se
|123|173|7b|7B|
|10.000000|10.000000|1.000000e+001|1.000000E+001|10|10|
|1234567890.000000|1234567890.000000|
|1.234568e+009|1.234568E+009|
|1.23457e+009|1.23457E+009|
|0061ff2c|
Sljedeći primjer oslikava upravljanje pozicioniranjem unutar zadane širine polja za ispis
printf("|%5d|%-5d|%5s|%-5s|\n", 5, 5, "ABC", "ABC");
Ispis je
| 5|5 | ABC|ABC |
I numerički i nenumerički podaci ispisuju se desno pozicionirani unutar zadane širine
polja za ispis, ako u konverzijskoj specifikaciji nema znaka -.
Primjer za prazninu unutar konverzijske specifikacije je
printf("|%d|% d|%+ d|%d|% d|%+ d|\n", 5, 5, 5, -5, -5, -5);
s rezultatom
|5| 5|+5|-5|-5|-5|
Utjecaj znaka + može se vidjeti u primjeru
printf("|%5d|%+5d|%+-5d|%5d|%+5d|%+-5d|\n", 5, 5, 5, -5, -5, -5);
gdje se ispisuje
| 5| +5|+5 | -5| -5|-5 |
Alternativni način ispisa mijenja način ispisa oktalnih i heksadekadskih vrijednosti, te
ponašanje konverzijske specifikacije g.
printf("|%o|%x|%X|%#o|%#x|%#X|%g|%#g|\n",
65, 65, 65, 65, 65, 65, 5., 5.);

214
Uvod u programiranje Ulaz i izlaz

ispisuje
|101|41|41|0101|0x41|0X41|5|5.00000|
Primjer za vodeće nule, ujedno i za minimalnu širinu ispisa je
printf("|%5d|%05d|%+5d|%+05d|%5d|%05d|\n",
5, 5, 5, 5, -5, -5);
printf("|%10f|%010f|\n|%+10f|%+010f|\n|%10f|%010f|\n",
5., 5., 5., 5., -5., -5.);
s ispisom
| 5|00005| +5|+0005| -5|-0005|
| 5.000000|005.000000|
| +5.000000|+05.000000|
| -5.000000|-05.000000|
Ako je za ispis vrijednosti potrebno više znakovnih mjesta od zadane minimalne širine
ispisa, ispis će se proširiti na potreban broj mjesta pa
printf("|%5d|%5d|%5d|\n", 10, 10000000, 20);
ispisuje
| 10|10000000| 20|
Sljedeći primjer prikazuje efekt preciznosti za numeričke vrijednosti
printf("|%10.3d|%10.2f|%10.4g|\n", 1, 1.234567, 123.4567);
Ispisuje se
| 001| 1.23| 123.5|
Kod cijelih brojeva, dodaju se vodeće nule do broja znamenki određenim preciznošću. Kod
realnih brojeva, u konverzijskoj specifikaciji %f preciznost se odnosi na broj decimala iza
decimalne točke, a u konverzijskoj specifikaciji %g na ukupan broj decimala. Kod obje
specifikacije zadnja znamenka se zaokružuje.
Preciznost djeluje i na nizove, gdje se ispisuje najviše onoliko znakova koliko je zadano
preciznošću:
printf("|%5s|%10.5s|%.5s|%.10s|\n",
"ABCDEF", "ABCDEF", "ABCDEF", "ABCDEF");
ispisuje
|ABCDEF| ABCDE|ABCDE|ABCDEF|
Formatirano se čitati može i iz znakovnog niza, kao što se formatirano može pisati u
znakovni niz. Za to služe funkcije
int sscanf(const char* buffer, const char* format, ...);
i
int sprintf(char *buffer, const char *format, ...);
koje su po funkcionalnosti identične sa scanf i printf osim što podatke učitavaju iz
znakovnog niza na koji pokazuje prvi argument odnosno podatke upisuju u znakovni niz
na koji pokazuje prvi argument.
Kao primjer poslužit će program u kojem je u nekom znakovnom nizu pohranjeno ime i
prezime osobe, a koje treba prepisati u drugi niz u obliku prezime, ime. Program
#include <stdio.h>
#define MAX 80

215
Uvod u programiranje Ulaz i izlaz

int main(void) {
char imePrezime[]="Ljudevit Gaj";
char ime[MAX], prezime[MAX], prezimeIme[2*MAX];
sscanf(imePrezime, "%s %s", ime, prezime);
sprintf(prezimeIme, "%s, %s", prezime, ime);
printf("%s\n", prezimeIme);
return 0;
}
Program 72. sprintf.c

ispisuje
Gaj, Ljudevit

Trajna pohrana podataka


Središnja memorija današnjih računala (random access memory, RAM) brz je, pouzdan i
relativno pristupačan medij za pohranu podataka, ali s velikom manom. Svi podaci u njoj
zapisani nestaju prestankom napajanja - električni naboj u bistabilu koji pohranjuje
jedinicu ili nulu vrlo brzo se prazni. Za dugotrajniju pohranu podataka nužno je koristiti
magnetske medije, optičke medije ili SSD (solid state drives) diskove.
Na optički medij zapisuje se tako da se laserskom zrakom promijeni svojstvo
reflektivnosti na površini optičkog medija, pa će površina koja reflektira svjetlo
predstavljati jednu, a površina koja ne reflektira svjetlo drugu binarnu znamenku. U
magnetooptičkim medijima promjena reflektivnosti zasniva se na magnetooptičkom
efektu koji uzrokuje promjenu u reflektivnosti ovisno o magnetizaciji materijala koja se
mijenja zagrijavanjem laserskom zrakom. Podaci zapisani na optički medij traju više
godina.
Na magnetski medij informacija se zapisuje polarizacijom čestica u sloju magnetskog
materijala nanesenom na neku podlogu. Tako zapisana informacija traje više godina,
nakon čega ju je potrebno osvježiti. Magnetski mediji dijele se na medije sa slijednim
pristupom podacima (sequential access) i medije sa izravnim pristupom podacima
(random access).
Mediji sa slijednim pristupom podacima dolaze uglavnom u obliku kaseta u kojima je
magnetska traka. Magnetska traka mora se premotati kako bi pozicija s koje se želi
pročitati ili na koju se želi upisati podatak došla ispod stacionarne glave za čitanje i pisanje
što traje više sekundi.
Magnetski mediji s izravnim pristupom podacima, čvrsti diskovi (hard disk drive, HDD),
ostvareni su pločama koje se vrte oko svoje osi određenom kutnom brzinom ω i rukom na
kojoj se ugrađene glave za čitanje i pisanje koja se pomiče u lûku. Time se omogućava da
se glava pozicionira iznad bilo koje pozicije na ploči.

216
Uvod u programiranje Ulaz i izlaz

Slika 37. Čvrsti disk (Pixabay, CC0)

Pristup podacima, dakle, nije sasvim izravan. Ako se zanemari vrijeme utrošeno za
pomicanje ruke i vrijeme potrebno za elektronički prijenos podatka, može se provesti
sljedeće razmatranje.
Podatak koji je prošao ispod glave za čitanje neposredno prije nego što je izdan zahtjev za
čitanje podatka, mora obići puni krug prije nego što se ponovno nađe na poziciji s koje ga
glava može pročitati. Broj okretaja diska u minuti n može se izraziti kao
n = 2Πω / 60
i iznosi tipično 5400, 7200, 10000 ili 1500 okretaja u minuti (revolutions per minute, rpm).
Primjerice, disk brzine 7200 rpm ima 7200/60 = 120 okretaja u sekundi (revolutions per
second, rps), što znači da vrijeme jednog okretaja iznosi 1/120=8.333 ms. Zaokruženo,
vrijeme potrebno da se pročita podatak s magnetskog diska je reda veličine 10 ms. Za
usporedbu, vrijeme pristupa podatku u središnjoj memoriji ovisi o frekvenciji f sata
sklopovlja, koje se danas mjeri gigahercima (1 GHz = 109 Hz), pa je to vrijeme reda veličine
1/f, dakle reda veličine nanosekunde (1ns = 10-9 s). Omjer vremena potrebnog za pristup
podatku na disku i podatku u središnjoj memoriji je, dakle, 10∙10-3 / 10-9 = 10∙106. Drugim
riječima, pristup podacima u memoriji je 10 milijuna puta brži od pristupa podacima na
disku. Neće, međutim, pristup do svakog podatka na disku trajati toliko dugo.
Fizički, podaci na čvrstom disku podijeljeni su na

217
Uvod u programiranje Ulaz i izlaz

Slika 38. Fizička organizacija čvrstog diska (Public Domain)

A – staze (tracks), koncentrične pojaseve iznad kojih prolazi glava za čitanje i pisanje ako
nema pomaka ruke,
B – sektore (sectors), segmente staze koji su najmanji skupovi podataka na disku koji se
mogu adresirati i koji sadrže, ovisno o izvedbi, 512 B do 4 kB podataka,
C – geometrijske sektore (kružne isječke koji sadrže odgovarajuće sektore svih staza) i
D – klastere (clusters), skupine susjednih sektora koji su najmanja jedinica rezervacije
prostora za datoteke.
S diska se uvijek, osim traženog podatka, pročita i najmanje cijeli sektor u kojem se taj
podatak nalazi. Učitani podaci spremaju se u međuspremnik (buffer) diska i/ili
operacijskog sustava pa je moguće da će sljedeći potrebni podatak već u međuspremniku
iz kojega ga se može dohvatiti brzinom sklopovlja. Kod upisa na disk, podaci se također
spremaju u međuspremnik, a na disk upisuju tek kad sljedeći podatak koji treba upisati
ne pripada području na disku koje pokriva međuspremnik.
Čvrsti diskovi imaju najveći kapacitet, najdulje vrijeme čuvanja i najnižu cijenu po jedinici
količine informacija, pa su najzastupljeniji od svih spomenutih medija.
SSD diskovi podatke čuvaju u integriranim sklopovima koji zadržavaju naboj i poslije
isključenja napajanja. Vrijeme čuvanja podatka iznosi nekoliko godina, ali je kraće nego
kod magnetskog diska. Takvi su integrirani sklopovi, međutim, dosta sporiji od središnje
memorije. Vrijeme pristupa podatku je reda veličine 100 μs, što je stotinjak puta brže od
čvrstog diska, ali je cijena po jedinici količine podataka bitno veća.
Program pisan u višem programskom jeziku komunicirat će s medijima za pohranu
posredstvom operacijskog sustava. Operacijski sustav je program koji se pokreće po
uključivanju računa te povezuje sklopovlje računala i programsku opremu. Jedna od
njegovih zadaća je i briga o logičkoj organizaciji podataka koje operacijski sustav prema
korisniku prezentira kao skupove mapa92 i datoteka.
Datoteka je imenovani skup podataka koji sačinjavaju logičku cjelinu, pohranjen na
nekom od medija za pohranu. Mapa je datoteka koja sadrži popis drugih datoteka i
podatke o njima. Mape su organizirane hijerarhijski, tvoreći strukturu nalik na stablo.

92 eng. folder, directory: često se koriste i termini imenik, kazalo, direktorij

218
Uvod u programiranje Ulaz i izlaz

mapa

datoteka

Slika 39. Logička organizacija čvrstog diska

Ulazni i izlazni tokovi podataka


U svim dosadašnjim primjerima podaci su se učitavali sa standardnog ulaza (tipkovnice,
standard input, stdin) i ispisivali na standardni izlaz (naredbeni prozor, standard output,
stdout). Znakovi utipkani s tipkovnice prosljeđivali su se kao niz bajtova programu.
Program je taj niz bajtova obrađivao i rezultate obrade prosljeđivao u naredbeni prozor,
također kao niz bajtova. Niz bajtova koji s tipkovnice dolazi u program može se nazvati
ulaznim tokom podataka (input stream), a niz bajtova koji iz programa odlazi u prozor
izlaznim tokom podataka (output stream).

stdin stdout

Slika 40. Standardni ulaz i izlaz

Nema razloga da se ovaj isti koncept, osim za tipkovnicu i prozor, ne primijeni i na druge
ulazno/izlazne jedinice, poput čvrstog diska, komunikacijskih uređaja ili čak drugih
programa.

219
Uvod u programiranje Ulaz i izlaz

stdin stdout

Slika 41. Drugi tokovi podataka

Standardni ulazni i izlazni tok podataka može se preusmjeriti jednostavnom naredbom


operacijskog sustava. Naredba
program > ime_datoteke
preusmjerava standardni izlazni tok podataka u datoteku zadanog imena.
Naredba
program < ime_datoteke
preusmjerava standardni ulazni tok podataka tako da podaci, umjesto s tipkovnice, dolaze
iz datoteke zadanog imena93.
Moguće je istovremeno preusmjeriti i ulazni i izlazni tok primjerice
main < ulaz.txt > izlaz.txt
pa će podaci koje program učitava funkcijom scanf dolaziti iz datoteke s nazivom ulaz.txt,
a ispis generiran funkcijom printf biti zapisivani u datoteku s nazivom izlaz.txt.

93Windows PowerShell ne podržava operator <. Windows Command Prompt i sve inačice Unixa i Linuxa
podržavaju taj operator.

220
Uvod u programiranje Ulaz i izlaz

ulaz.txt izlaz.txt
stdin stdout

Slika 42. Preusmjeravanje standardnog ulaza i izlaza

Tok podataka (stream) u C-u opisuje se strukturom koja deklarirana u <stdio.h> i kojoj je
naredbom typedef pridružen naziv FILE. Naziv FILE zadržan je iz povijesnih razloga,
umjesto naziva STREAM koji bi svakako bio primjereniji jer se engleska riječ file označava
datoteku, a tok podataka može biti usmjeren i drugdje. Ta struktura sadrži sve podatke o
toku koji su potrebni za komunikaciju s operacijskim sustavom.
Standardna biblioteka programskog jezika C sadrži funkcije koje podatke učitavaju iz
zadanog toka podataka, odnosno podatke ispisuju u zadani tok podataka. Sve do sada
obrađene funkcije za ulaz i izlaz imaju i svoju inačicu koja komunicira sa zadanim tokom.
Tako se, primjerice, inačica funkcije printf koja podatke ispisuje u zadani tok zove fprintf
i ima prototip
int fprintf(FILE *stream, const char *format, ...);
Funkcionalnost ove funkcije identična je funkcionalnosti funkcije printf, kao što je i
funkcionalnost funkcije
int fscanf(FILE *stream, const char *format, ...);
identična funkcionalnosti funkcije scanf.
Svaki program u C-u na samom početku rada stvara tri toka podataka i definira tri
pokazivača na podatak tipa FILE koji su definirani u <stdio.h>. Ovi se tokovi bez dodatnih
deklaracija ili definicija odmah mogu početi koristiti:
FILE *stdin;
FILE *stdout;
FILE *stderr;
Elementarni program "Hello, world" može zbog toga izgledati i ovako:
#include <stdio.h>
int main (void) {
fprintf(stdout, "Hello, world\n");
return 0;
}
stdout je povezan s naredbenim prozorom, stdin s tipkovnicom, a stderr također s
naredbenim prozorom, s time da se stderr ne preusmjerava u datoteku operatorom
preusmjeravanja u operacijskom sustavu '>'. Razlog tome je potreba da eventualna greška
u programu bude odmah vidljiva korisniku koji je program pokrenuo. Pretpostavimo da
treba napisati program koji će sa standardnog ulaza učitavati realne brojeve, a na

221
Uvod u programiranje Ulaz i izlaz

standardni izlaz ispisivati učitane brojeve i njihove korijene. Ako se u skupu učitanih
brojeva nalazi i negativni broj, to treba odmah dojaviti u prozor čak i ako je program
pokrenut uz korištenje operatora preusmjeravanja:
#include <stdio.h>
#include <math.h>
int main(void) {
double d;
while (fscanf(stdin, "%lf", &d) == 1) {
if (d >= 0) {
fprintf(stdout, "sqrt(%g)=%g\n", d, sqrt(d));
} else {
fprintf(stderr, "%g je negativan\n", d);
}
}
return 0;
}
Program 73. PrimjerStderr.c

Ako se u datoteku ulaz.txt unese


2
3
-1
4
5
naredbom
main < ulaz.txt
u prozor će se ispisati
sqrt(2)=1.41421
sqrt(3)=1.73205
-1 je negativan
sqrt(4)=2
sqrt(5)=2.23607
Naredbom
main < in.txt > out.txt
u prozor se ispisuje samo
-1 je negativan
a u datoteku out.txt bit će zapisano
sqrt(2)=1.41421
sqrt(3)=1.73205
sqrt(4)=2
sqrt(5)=2.23607
Grafički se ova situacija može prikazati sljedećom slikom

222
Uvod u programiranje Ulaz i izlaz

ulaz.txt izlaz.txt
stdin stdout

stderr

Slika 43. Preusmjeravanje uz stderr

Čitanje i pisanje tekstovnih datoteka


Tekstovne datoteke prepoznaju se po tome što je tekst u njima čitak ako se otvore
elementarnim uređivačem teksta poput notepada ili se čitko ispisuju u prozoru
naredbama operacijskog sustava
type ime_datoteke
na Windowsima ili
cat ime_datoteke
na Linuxu ili Unixu.
Sastoje se samo od znakova koji se mogu ispisati (znakova za koje funkcija isprint vraća
1) te znakova koji uzrokuju skok u novi red.
Datoteka koja se u uređivaču teksta vidi kao
Prvi red
Drugi red
Treći red
na disku je u operacijskom sustavu Windows zapisana bajt po bajt kao
P r v i r e d \r \n D r u g i r e d \r \n T r e ć i r e d \r \n
0 1 2 … 31

i velika je 32 bajta. U stvarnosti, zauzima cijeli klaster koji je obično velik 4 kB.
Za funkcije koje učitavaju retke, međutim, znakovi '\r' (0x0D, carriage return) kao da ne
postoje. Funkcije taj znak preskaču i prepoznaju ili učitavaju samo '\n'.
Na Linuxu i Unixu retci su odijeljeni samo znakom '\n':
P r v i r e d \n D r u g i r e d \n T r e ć i r e d \n
0 1 2 … 28

pa je veličina te datoteke 29 bajta uz zauzeće cijelog klastera.


U programu kojim se želi izravno čitati iz datoteke ili pisati u datoteku, prvo je potrebno
stvoriti odgovarajući ulazni ili izlazni tok, što je u žargonu često izrečeno kao "otvoriti
datoteku". Tok se stvara pozivom funkcije
FILE *fopen(const char *filename, const char *mode);

223
Uvod u programiranje Ulaz i izlaz

filename je niz znakova koji sadrži putanju do datoteke ili, općenito, ulazno izlazne
jedinice s kojom se želi komunicirati. Ako je tok uspješno stvoren, funkcija vraća
pokazivač na podatak tipa FILE. U suprotnom, funkcija vraća pokazivač NULL.
Ako se navede samo ime datoteke, podrazumijeva se mapa u kojoj se nalazi i izvršni
program. Putanja se može napisati kao apsolutna, navođenjem cijele hijerarhije npr.
"C:/Users/Public/podaci.txt"
ili relativna, gdje ".." znači mapa nadređena mapi u kojoj se izvodi program
"../Tekstovi/tekst1.txt"
Ulazno/izlazne jedinice također imaju svoja imena na razini operacijskog sustava.
Primjerice, serijska vrata na Windowsima zovu se "COM1:", na Linuxu "/dev/tty0", ali se
komunikacija s njima ovdje neće razmatrati.
mode je znakovni niz kojim se opisuje što se i kako s podacima želi raditi:
r iz datoteke će se samo čitati. Pokušaj pisanja u datoteku neće uspjeti. Datoteka mora
već postojati ili će fopen vratiti NULL
w u datoteku će se samo pisati. Pokušaj čitanja iz datoteke neće uspjeti. Ako datoteka
ne postoji, stvorit će se. Ako datoteka postoji, sadržaj će se obrisati.
a u datoteku će se dodavati na kraj. Pokušaj čitanja iz datoteke neće uspjeti. Ako
datoteka ne postoji, stvorit će se.
r+ iz datoteke će se čitati, ali u nju i pisati. Datoteka mora već postojati ili će fopen vratiti
NULL
w+ u datoteku će se pisati, ali iz nje i čitati. Ako datoteka ne postoji, stvorit će se. Ako
datoteka postoji, sadržaj će se obrisati.
a+ u datoteku će se dodavati na kraj, ali će se iz nje i čitati. Ako datoteka ne postoji,
stvorit će se.
Za čitanje i pisanje binarnih datoteka, o čemu će biti govora kasnije, u mode treba dodati
još i b.
Ako se uz w ili w+ doda još i x, fopen će vratiti NULL ako datoteka postoji, umjesto da se
obriše sadržaj.
Pozivom funkcije
int fclose(FILE *stream);
tok se zatvara i svi se podaci iz međuspremnika fizički zapisuju na disk. Funkcija vraća
nulu ako je tok zatvoren uspješno, a EOF u slučaju greške, primjerice ako je stream NULL
jer datoteka nije uspješno otvorena.
Iz datoteke se može čitati inačicama funkcija getchar, gets i scanf, a one su
int fgetc(FILE *stream);
char *fgets(char *str, int count, FILE *stream);
i
int fscanf(FILE *stream, const char *format, ... );
Funkcija fgetc vraća sljedeći pročitani znak, odnosno EOF u slučaju greške ili pokušaja
čitanja iza kraja datoteke. Sljedeći program prepisuje u prozor sadržaj datoteke tekst.txt
koja se nalazi u istoj mapi kao i program.
#include <stdio.h>

224
Uvod u programiranje Ulaz i izlaz

#include <stdlib.h>
int main(void) {
FILE *f;
char c;
f = fopen("in.txt", "r");
if (f == NULL) {
fprintf(stderr, "Datoteka ne postoji");
return EXIT_FAILURE;
}
while ((c = fgetc(f)) != EOF) {
putchar(c);
}
fclose(f);
return EXIT_SUCCESS;
}
Program 74. fgetc.c

Simboličke konstante EXIT_SUCCESS i EXIT_FAILURE definirane su u <stdlib.h> kao 0


odnosno 1, a njihovo korištenje doprinosi semantičkoj jasnoći programa.
Za poruku o grešci bolje je koristiti funkciju
void perror(const char *s);
koja iza poruke definirane nizom s na stderr ispisuje i detaljniji opis greške.
Umjesto
fprintf(stderr, "Datoteka ne postoji");
tako je bolje napisati
perror("Greška kod otvaranja datoteke \"in.txt\"");
Ako datoteka ne postoji, ispisat će se
Greška kod otvaranja datoteke "in.txt": No such file or directory
Funkcija fgets čita podatke iz datoteke dok ne naiđe na '\n'. '\n' ostaje u pročitanim
podacima, a na kraj podataka dodaje se '\0'. Ukupna duljina pročitanoga, uključujući i '\n',
neće premašiti broj znakova koji se predaje u pozivu. Za prijepis u prozor logično je
uporabiti puts, ali puts na kraj ispisanoga dodaje još i '\n' pa će posljedica biti ispis s
proredom. Zato je iz učitanoga prvo potrebno ukloniti '\n' što se može elegantno napraviti
uz pomoć strchr.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX 80
int main(void) {
FILE *f;
char redak[MAX];
char *kraj;
f = fopen("in.txt", "r");
if (f == NULL) {
perror("Greška kod otvaranja datoteke \"in.txt\"");
return EXIT_FAILURE;
}
while (fgets(redak, MAX, f) != NULL) {
if ((kraj = strchr(redak, '\n')) != NULL) {
*kraj = '\0';

225
Uvod u programiranje Ulaz i izlaz

}
puts(redak);
}
fclose(f);
return EXIT_SUCCESS;
}
Program 75. fgets.c

Ako se za učitavanje želi koristiti fscanf, prikladan format je


"%80[^\n]%*c"
što znači da treba učitati najviše 80 znakova koji nisu '\n' i potom preskočiti '\n'.
Brojka u formatu treba biti usklađena sa simboličkom konstantom MAX, pa je i format
dobro definirati naredbom #define neposredno uz MAX, da se ne zaboravi promijeniti
format ako se promijeni MAX.
#define MAX 80
#define FORMAT "%80[^\n]%*c"
Alternativno i bolje, format se može dinamički generirati funkcijom sprintf, ali je format
za generiranje formata relativno nečitak:
char format[MAX];
sprintf(format, "%%%d[^\\n]%%*c", MAX);
Postotak u formatu označava početak konverzijske specifikacije. Za očekivati bi bilo da se
znak postotka koji se doslovno prepisuje na izlaz u format ugrađuje s \%, ali to nije tako.
%% se prevodi u %, slijedi %d što funkcija supstituira vrijednošću argumenta MAX, [^ se
doslovno prepisuje, \\ se prevodi u \, n] se doslovno prepisuje, %% se prevodi u % i *c
doslovno prepisuje. Rezultat pohranjen u niz format je
% 8 0 [ ^ \ n ] % * c \0 ? ? ? ? …
što odgovara potrebama. ?
Kompletan program je
#include <stdio.h>
#include <stdlib.h>
#define MAX 80
int main(void) {
FILE *f;
char redak[MAX+1];
char format[MAX];
f = fopen("in.txt", "r");
if (f == NULL) {
perror("Greška kod otvaranja datoteke \"in.txt\"");
return EXIT_FAILURE;
}
sprintf(format, "%%%d[^\\n]%%*c", MAX);
while (fscanf(f, format, redak) == 1) {
printf("%s\n", redak);
}
fclose(f);
return EXIT_SUCCESS;
}
Program 76. fscanf.c

226
Uvod u programiranje Ulaz i izlaz

Redak treba biti za jedan veći od MAX jer fscanf čita najviše MAX znakova iz datoteke čemu
dodaje '\0'.
Inačice funkcija za ispis putchar, puts i sprintf su
int fputc(int ch, FILE *stream);
int fputs(const char *str, FILE *stream);
i
int fprintf(FILE *stream, const char *format, ...);
Slijede programski odsječci koji prepisuju iz datoteke in.txt u datoteku out.txt u
skraćenom obliku, bez ispitivanja je li stvaranje tokova uspjelo, što bi u stvarnom
programu svakako trebalo načiniti.
Zajednički dio je
FILE *fi, *fo;
fi = fopen("in.txt", "r");
fo = fopen("out.txt", "w");

fclose(fi);
fclose(fo);
Uz korištenje fgetc i fputc odsječak je
char c;
while ((c = fgetc(fi)) != EOF) {
fputc(c, fo);
}
Uz korištenje fgets i fputs odsječak je
#define MAX 80

char redak[MAX];
while (fgets(redak, MAX, fi) != NULL) {
fputs(redak, fo);
}
jer su te dvije funkcije komplementarne pa fputc ne dodaje na kraj ispisa '\n'.
Na kraju, uz fscanf i fprintf te format za čitanje generiran na prethodno opisani način
odsječak je
while (fscanf(fi, format, redak) == 1) {
fprintf(fo, "%s\n", redak);
}
Tekstovne datoteke nazivaju se još i formatiranima, jer se vrijednosti numeričkih varijabli
iz memorije pretvaraju u znakovne nizove preko odgovarajućeg formata. U sljedećem
primjeru u datoteku potencije.txt upisat će se tablica 2n za n ∈ [0, 31]. Prvo se ispisuje
zaglavlje tablice, a zatim vrijednosti formatirane tako da budu uredno potpisane.
#include <stdio.h>
#include <stdlib.h>
int main(void) {
FILE *fo;
unsigned int n, pot = 1;
fo = fopen("potencije.txt", "w");
fprintf(fo, " n 2^n\n");
fprintf(fo, "-------------\n");

227
Uvod u programiranje Ulaz i izlaz

for (n = 0; n <= 31; n++) {


fprintf(fo, "%2d %10u\n", n, pot);
pot *= 2;
}
fclose(fo);
return EXIT_SUCCESS;
}
Program 77. PotencijeUDatoteku.c

Datoteka potencije.txt sadržavat će


n 2^n
-------------
0 1
1 2
2 4
3 8

29 536870912
30 1073741824
31 2147483648
Ovdje treba primijetiti da binarni zapisa podataka u memoriji nije jednak binarnom zapisu
podataka u datoteci. Za vrijednosti iz posljednjeg retka tablice događa se sljedeća
konverzija (jedan kvadratić na slici odgovara jednom bajtu, a sadržaj je prikazan u
heksadekadskoj notaciji):
n pot
00 00 00 1F 31 80 00 00 00 2147483648 memorija
%2d %10d

… 0A 33 31 20 32 31 34 37 43 38 33 36 34 38 0A potencije.txt
\n '3' '1' ' ' '2' '1' '4' '7' '4' '8' '3' '6' '4' '8' \n

Slika 44. Formatirano zapisivanje u datoteku

Varijabla n koja u memoriji zauzima 4 bajta zbog formatiranja će u datoteci zauzimati 2


bajta. Varijabla pot, koja u memoriji također zauzima 4 bajta, u datoteci će zauzeti 10 bajta.

Čitanje i pisanje binarnih datoteka


Kod binarnih datoteka binarni zapis podatka u datoteci potpuno odgovara binarnom
zapisu podatka u memoriji. Iz binarnih datoteka čita se funkcijom
size_t fread(void *buffer, size_t size, size_t count,
FILE *stream);
a u njih se zapisuje funkcijom
size_t fwrite(const void *buffer, size_t size, size_t count,
FILE *stream);
buffer je adresa u memoriji na koju se pohranjuju podaci kod čitanja odnosno s koje se
uzimaju podaci kod pisanja, size je veličina pojedinačnog objekta koji se čita odnosno
zapisuje, count broj objekata koji se čitaju odnosno zapisuju jednim pozivom funkcije, a
stream pokazivač na podatak tipa FILE koji vraća funkcija fopen.

228
Uvod u programiranje Ulaz i izlaz

Tablica potencija broja 2 iz prethodnog primjera mogla bi se pohraniti u binarnu datoteku


sljedećim programom.
#include <stdio.h>
#include <stdlib.h>
int main(void) {
FILE *fo;
unsigned int n, pot = 1;
fo = fopen("potencije.bin", "wb");
for (n = 0; n <= 31; n++) {
fwrite(&n, sizeof(n), 1, fo);
fwrite(&pot, sizeof(n), 1, fo);
pot *= 2;
}
fclose(fo);
return EXIT_SUCCESS;
}
Program 78. PotencijeUBinarnuDatoteku.c

Datoteka je binarna, pa je dobro to jasno naznačiti odgovarajućim nastavkom imena. Na


Windowsima je u argument mode funkcije fopen potrebno dodati oznaku da se radi o
binarnoj datoteci 'b'. Za svaki n, s adrese varijable n uzima se 1 podatak veličine varijable
n i upisuje u datoteku. Na isti način upisuje se i vrijednost varijable pot.
Sada je binarna slika u datoteci jednaka binarnoj slici u memoriji:
n pot
00 00 00 1F 31 80 00 00 00 2147483648 memorija

… 00 00 00 1F 80 00 00 00 potencije.bin
Slika 45. Zapisivanje u binarnu datoteku

Funkcija fwrite vraća broj upisanih objekata. Taj broj morao bi odgovarati argumentu
count - broju objekata koje se želi zapisati. Rezultat će biti nula ako datoteka nije otvorena
na odgovarajući način, npr. ako mode ima vrijednost "rb" ili ako se dogodila neka druga
greška. Rezultat može biti veći od nule ali manji od count ako na disku više nema dovoljno
slobodnog mjesta za upisati sve podatke. Zato bi nakon svakog pokušaja upisa trebalo
provjeravati rezultat funkcije i dojaviti grešku:
if (fwrite(&n, sizeof(n), 1, fo) != 1) {
perror("Nije uspjelo pisanje");
return EXIT_FAILURE;
}
U slučaju da je datoteka otvorena s "rb", prethodni odsječak će ispisati
Nije uspjelo otvaranje: Bad file descriptor
Ovako zapisane podatke čita se funkcijom fread. Slijedi program koji će pročitati i na
zaslon ispisati podatke zapisane prethodnim programom.
#include <stdio.h>
#include <stdlib.h>
int main(void) {
FILE *fi;
unsigned int n, pot;

229
Uvod u programiranje Ulaz i izlaz

fi = fopen("potencije.bin", "rb");
if (fi == NULL) {
perror("Nije uspjelo otvaranje datoteke potencije.bin");
return EXIT_FAILURE;
}
while (fread(&n, sizeof(n), 1, fi) == 1) {
if (fread(&pot, sizeof(n), 1, fi) != 1) {
perror("Greška kod čitanja");
return EXIT_FAILURE;
}
printf("%2d %10u\n", n, pot);
}
fclose(fi);
return EXIT_SUCCESS;
}
Program 79. PotencijeIzBinarneDatoteke1.c

Podataka u datoteci ima ograničena količina, pa će funkcija fread sigurno jednom naići na
kraj datoteke i vratiti rezultat 0. Međutim, ako je uspješno pročitan n, tada uz njega mora
postojati i pripadni pot, pa neuspješno čitanje varijable pot signalizira neočekivanu
grešku.
Funkcijama fread i fwrite moguće je odjednom pročitati i više objekata. Datoteka iz
prethodnih primjera sastoji se od 32 para (n, pot) zapisanih jedan za drugim, i ima
strukturu jednaku kao polje struktura
struct potencija {
unsigned int n;
unsigned int pot;
} p[32];
pa je cijeli sadržaj moguće pročitati jednom naredbom:
fread (p, sizeof(p), 1, fi);
ili
fread (p, sizeof(struct potencija), 32, fi);
i tada ispisati s
for (int i=0; i < 32; i++) {
printf("%2d %10u\n", p[i].n, p[i].pot);
}
Program 80. PotencijeIzBinarneDatoteke2.c

U binarnim datotekama koje zadrže polja uobičajeno je na početak datoteke ugraditi


informaciju o veličini, pa bi se tako programom koji je stvorio i napunio vrijednostima
neko polje

int n;

int p[n];

takvo polje zapisalo s dvije naredbe
fwrite(&n, sizeof(int), 1, f);
fwrite(p, sizeof(int), n, f);

230
Uvod u programiranje Ulaz i izlaz

Program koji te podatke koristi pročitao bi prvo veličinu, zatim definirao polje
odgovarajuće veličine i potom pročitao podatke
int n;
fread(&n, sizeof(int), 1, f);
int p[n];
fread(p, sizeof(int), n, f);

Slijedne i direktne datoteke


Pretpostavimo da se u nekoj tekstovnoj datoteci nalaze podaci o osobama. Svaki redak
predstavlja podatke o jednoj osobi, a podaci su, redom, šifra (cijeli broj), ime (niz znakova
koji ne sadrži prazninu), prezime (niz znakova koji ne sadrži prazninu) te težina (cijeli
broj).
Datoteka koja će se u uređivaču teksta vidjeti kao
1 Ana Anić 65
2 Petar Petrović 101
3 Ante Antić 80
na disku je pohranjena na sljedeći način:
1 A n a A n i ć 6 5 \r \n 2 P e t a r P e t r o v i ć →
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30

1 0 1 \r \n 3 A n t e A n t i ć 8 0 \r \n
31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53

Retci su različite duljine: prvi redak je duljine 15, drugi 22, treći 17 bajta. Ukupno je
datoteka velika 54 bajta. Sadržaj datoteke otvorene heksadekadskim uređivačem teksta
izgleda ovako:

Program koji će ove podatke prepisati u binarnu datoteku mora slijedno čitati redak po
redak tekstovne datoteke
#include <stdio.h>
#include <stdlib.h>
#define MAX 16
int main(void) {
FILE *fi, *fo;
char ime[MAX], prezime[MAX];
int sifra, tezina;
fi = fopen("osobe.txt", "r");
fo = fopen("osobe.bin", "wb");
while (fscanf(fi, "%d %s %s %d",
&sifra, ime, prezime, &tezina) == 4) {
fwrite(&sifra, sizeof(sifra), 1, fo);
fwrite(ime, sizeof(ime), 1, fo);
fwrite(prezime, sizeof(prezime), 1, fo);
fwrite(&tezina, sizeof(tezina), 1, fo);
}
fclose(fo);
return EXIT_SUCCESS;
}

231
Uvod u programiranje Ulaz i izlaz

Program 81. TekstUBinarnu.c

Umjesto upisa pojedinačnih podataka, bolje je definirati strukturu


struct osoba {
int sifra;
char ime[MAX];
char prezime[MAX];
int tezina;
} o;
pa će kôd za upis biti bitno kraći
while (fscanf(fi, "%d %s %s %d",
&o.sifra, o.ime, o.prezime, &o.tezina) == 4) {
fwrite(&o, sizeof(o), 1, fo);
}
Program 82. TekstUBinarnuStruct.c

Na disku nastaje datoteka koja u heksadekadskom uređivaču teksta izgleda ovako:

Pojedini zapis ove datoteke je duljine 40 bajta (4 + 16 + 16 +4) pa je ukupna veličina


datoteke 120 bajta.
Jednaka duljina zapisa omogućava da se, poznajući duljinu zapisa w i redni broj zapisa n
lako izračuna pozicija toga zapisa u datoteci. Ako prvi zapis u datoteci ima redni broj n=1,
tada je pozicija n-tog zapisa
pn = (n – 1)
Sve do sada rađene operacije čitanja i pisanja u datoteci odvijale su se slijedno. Po
otvaranju datoteke pozicija zamišljene glave za čitanje ili pisanje nalazi se prije prvog
bajta u datoteci. U primjeru tekstovne datoteke osobe.txt nakon otvaranja datoteke
zamišljena glava za čitanje nalazi se na samom početku:
1 A n a A n i ć 6 5 \r \n 2 P e t a r P e t r o v i ć …
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30

Učitavanje iz datoteke naredbom


fscanf(fi, "%d %s %s %d", &o.sifra, o.ime, o.prezime, &o.tezina)
odvija se prema poznatim pravilima. Konverzijska specifikacija %d preskače bijelu
prazninu (koje ovdje nema) te čita podatak do prve sljedeće bijele praznine:
1 A n a A n i ć 6 5 \r \n 2 P e t a r P e t r o v i ć …
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30

232
Uvod u programiranje Ulaz i izlaz

Slijedi %s koja preskače bijelu prazninu, pročita podatak i stane prije prve sljedeće bijele
praznine. Isto se ponavlja sa sljedećim %s i %d. Nakon obavljanja cijele naredbe
zamišljena glava za čitanje je na poziciji prije '\r':
1 A n a A n i ć 6 5 \r \n 2 P e t a r P e t r o v i ć …
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30

Prvo sljedeće čitanje preskače bijelu prazninu ("\r\n"), pročita sljedeće podatke i tako do
kraja.
Zamišljena glava za čitanje pomiče se na sličan prema naprijed nakon svake naredbe za
čitanje ili pisanje u datoteci, bila datoteka formatirana ili binarna.
Trenutačna pozicija u datoteci u svakom trenutku može se saznati pozivom funkcije
long ftell(FILE *stream);
Po otvaranju datoteke ftell vraća rezultat 0.
Trenutačna pozicija u datoteci u svakom trenutku može se postaviti pozivom funkcije
int fseek(FILE *stream, long offset, int origin);
origin je referentna točka od koje se treba pomaknuti za zadani offset, a zadaje se nekom
od simboličkih konstanti iz <stdio.h>:
SEEK_SET – pozicioniranje u odnosu na početak datoteke, apsolutno pozicioniranje
fseek(f, 0L, SEEK_SET) postavlja trenutačnu poziciju za čitanje i pisanje na sâm
početak datoteke
fseek(f, 100L, SEEK_SET) postavlja trenutačnu poziciju 100 bajta iza početka
datoteke
SEEK_CUR – pozicioniranje u odnosu na trenutačnu poziciju, relativno pozicioniranje
fseek(f, -4L, SEEK_CUR) postavlja trenutačnu poziciju 4 bajta ispred
trenutačne pozicije
fseek(f, 4L, SEEK_CUR) postavlja trenutačnu poziciju 4 bajta iza trenutačne
pozicije
SEEK_END – pozicioniranje u odnosu na kraj datoteke, relativno pozicioniranje
fseek(f, 0L, SEEK_END) postavlja trenutačnu poziciju na kraj datoteke
fseek(f, -4L, SEEK_CUR) postavlja trenutačnu poziciju 4 bajta ispred kraja
datoteke
Odsječak
fseek(f, 0L, SEEK_END);
printf("%ld", ftell(f));
ispisuje veličinu datoteke izraženu u bajtovima.
Funkcija fseek vraća nulu ako je pozicioniranje uspjelo il EOF u slučaju greške, koja će
nastati samo ako pri pokušaju pozicioniranja u datoteku koja nije uspješno otvorena ili
pri pokušaju pozicioniranja prije početka datoteke. Pozicioniranje iza kraja datoteke je
dozvoljeno čime se omogućava stvaranje datoteke s direktnim pristupom koja se ne puni
slijedno.
Pretpostavimo da za malo modificirane podatke iz prethodnog primjera
1 Ana Anić 65
7 Petar Petrović 101
4 Ante Antić 80

233
Uvod u programiranje Ulaz i izlaz

želimo omogućiti direktan pristup preko šifre. To znači da će podaci za Anu Anić trebati
pohraniti u prvi, za Petra Petrovića u sedmi, a za Antu Antića u četvrti zapis datoteke, pa
će drugi i treći odnosno peti i šesti zapis ostati prazni.
Program koji ove podatke prepisuje u binarnu datoteku trebat će modificirati samo
jednom naredbom:
while (fscanf(fi, "%d %s %s %d",
&o.sifra, o.ime, o.prezime, &o.tezina) == 4) {
fseek(fo, (o.sifra-1L) * sizeof(o), SEEK_SET);
fwrite(&o, sizeof(o), 1, fo);
}
Nakon što se na početak datoteke upišu podaci za Anu Anić, slijedi upis podataka za Petra
Petrovića na udaljenosti pet punih zapisa od kraja datoteke. Zapisi koji nastaju takvim
proširenjem datoteke pune se nulama. Podaci za Antu Antića upisuju se unutar prethodno
proširene datoteke. Sadržaj datoteke osobe.bin sada je

Podacima o osobama sada možemo izravno pristupati preko šifre. U sljedećem primjeru
načinit će se program koji s tipkovnice učitava šifru osobe i težinu osobe te osobi sa
zadanom šifrom ažurira težinu, sve dok se za šifru osobe ne unese 0.
#include <stdio.h>
#include <stdlib.h>
#define MAX 16
int main(void) {
FILE *fo;
struct osoba {
int sifra;
char ime[MAX];
char prezime[MAX];
int tezina;
} o;
int sifra, tezina;
fo = fopen("osobe.bin", "r+b");
while (1) {
scanf("%d %d", &sifra, &tezina);
if (sifra <= 0) break;
fseek(fo, (sifra-1L) * sizeof(o), SEEK_SET);
if (fread(&o, sizeof(o), 1, fo) == 1

234
Uvod u programiranje Ulaz i izlaz

&& o.sifra == sifra) {


o.tezina = tezina;
fseek(fo, (sifra-1L) * sizeof(o), SEEK_SET);
fwrite(&o, sizeof(o), 1, fo);
printf("Ažurirani podaci za osobu %s %s\n",
o.ime, o.prezime);
} else {
printf("Ne postoji osoba sa šifrom %d\n", sifra);
}
}
fclose(fo);
return EXIT_SUCCESS;
}
Program 83. AzuriranjeBinarne.c

S obzirom da će se podaci ažurirati i da datoteka mora na disku postojati, treba je otvoriti


s "r+b". Otvaranje s "w+b" datoteku bi obrisalo.
Unesena šifra može biti takva da se preračunata pozicija zapisa određenog takvom šifrom
nalazi iza kraja datoteke. fseek omogućava pozicioniranje iza kraja datoteke, ali je tamo
dozvoljeno samo pisanje. Zato treba provjeriti je li čitanje uspjelo, pa ako jest, je li šifra
pročitana iz datoteke jednaka šifri koja je na tome mjestu upisana. Ako nije, to je prazan
zapis, pa i u jednom i u drugom slučaju treba dojaviti da osoba sa zadanom šifrom nije
pronađena.
Ako je podatak uspješno pročitan i šifra odgovara očekivanoj, treba ažurirati vrijednost u
učitanoj strukturi i zapisati izmijenjenu strukturu natrag u datoteku. Međutim, trenutačna
pozicija u datoteci sada je iza zapisa kojega treba ažurirati pa bi poziv funkcije fwrite u
tome trenutku prepisao sadržaj sljedećeg zapisa. Zato je nužno prvo pozicionirati se opet
na početak zapisa kojega se želi ažurirati i tek tada pozvati funkciju fwrite. Osim
apsolutnim pozicioniranjem, kao što je to načinjeno u programu, pozicioniranje je moguće
i relativnim pozicioniranjem pozivom funkcije fseek na način
fseek(fo, -sizeof(o), SEEK_CUR);
Općenito je ažuriranje nekog podatka na disku uvijek mnogo složenija operacija nego što
je to ažuriranje podatka u memoriji. Ažururati podatak u memoriji može se jednostavnom
primjenom operatora pridruživanja npr.
a = a + 1;
Ažuriranje podatka na disku, međutim, uvijek zahtijeva pet operacija:
• pozicioniranje na podatak na disku
• čitanje podatka u memoriju
• ažuriranje podatka u memoriji
• pozicioniranje na podatak na disku
• zapisivanje podatka na disk
U ovome je primjeru šifra osobe primarni ključ zapisa o osobi: podatak koji zapis o osobi
jednoznačno određuje. Ujedno je to i jedini podatak temeljem kojega je moguće ostvariti
izravni pristup zapisu. Dohvat zapisa preko drugih atributa moguć je jedino slijedno.
Pretpostavimo da se, umjesto šifre, želi zadavati ime i prezime osobe čiju se težinu želi
ažurirati. Ažuriranje završava kad se unese ime sa sadržajem "-". Odmah treba
napomenuti da ime i prezime ne može jednoznačno odrediti zapis o osobi – pretraga

235
Uvod u programiranje Ulaz i izlaz

telefonskog online imenika za ime i prezime Ivan Horvat vraća preko 500 zapisa, tako da
sljedeći program može poslužiti samo za ilustraciju slijednog pretraživanja.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX 16
int main(void) {
FILE *fo;
struct osoba {
int sifra;
char ime[MAX];
char prezime[MAX];
int tezina;
} o;
int tezina, procitano;
char ime[MAX], prezime[MAX];
fo = fopen("osobe.bin", "r+b");
while (1) {
scanf("%s %s %d", ime, prezime, &tezina);
if (strcmp(ime, "-") == 0) break;
fseek(fo, 0L, SEEK_SET);
while ((procitano = fread(&o, sizeof(o), 1, fo)) == 1) {
if (strcmp(o.ime, ime) == 0
&& strcmp(o.prezime, prezime) == 0) {
o.tezina = tezina;
fseek(fo, -sizeof(o), SEEK_SET);
fwrite(&o, sizeof(o), 1, fo);
break;
}
}
if (procitano == 1) {
printf("Ažurirani podaci za osobu %s %s\n",
o.ime, o.prezime);
} else {
printf("Ne postoji osoba imenom %s %s\n",
ime, prezime);
}
}
fclose(fo);
return EXIT_SUCCESS;
}
Program 84. AzuriranjeBinarneSlijedno.c

Svaki put kad se unesu novi podaci za pretraživanje potrebno je pozicionirati se na


početak datoteke kako se pretraživanje ne bi nastavilo od zapisa koji slijedi iza prethodno
pronađenog. Varijabla procitano bit će indikacijom je li se iz unutrašnje petlje u kojoj se
slijedno pretražuje izašlo što je osoba pronađena, ili zato što se slijednim pretraživanjem
osobu nije pronašlo pa je čitanje prekinuto zbog nailaska na kraj datoteke.
Alternativno, za tu svrhu mogla bi se koristiti i funkcija
int feof(FILE *stream);
koja ima vraća vrijednost različitu od nule ako se pokušalo čitati nakon kraja datoteke, a
inače vraća nulu. Ovdje treba naglasiti da će ova funkcija vratiti nulu i ako je upravo
pročitan posljednji zapis ili bajt pa se zamišljena glava za čitanje nalazi neposredno prije
236
Uvod u programiranje Ulaz i izlaz

kraja datoteke. Sljedeći odsječak, koji bi iz neformatirane datoteke trebao redom čitati
vrijednosti varijabli tipa int i ispisivati ih u prozor, neće raditi dobro
while (!feof(f)) {
fread(&v, sizeof(v), 1, f);
printf("%d\n", v);
};
jer će se ispisati vrijednost varijable v i nakon zadnjeg (neuspjelog) čitanja. Zato uvijek
nakon čitanja treba provjeriti broj pročitanih objekata.
Broj pročitanih objekata može biti nula i ako je kod čitanja došlo do greške. Ta se situacija
može provjeriti pozivom funkcije
int ferror(FILE *stream);
pa bi korektan odsječak iz prethodnog primjera glasio
while (fread(&v, sizeof(v), 1, f) == 1) {
printf("%d\n", v);
};
if (ferror(f)) {
perror("Greška kod čitanja datoteke");
} else if (feof(f)) {
printf("Svi podaci pročitani");
}

Naredbeni redak
Funkcija main u svim je dosadašnjim programima bila definirana kao
int main(void) {…
Funkcija main, međutim, može imati i parametre
int main(int argc, char *argv[]) { …
koji sadrze argumente iz naredbenog retka kojim je program pokrenut iz operacijskog
sustava. argc je broj argumenata iz naredbenog retka a argv polje pokazivača na
pojedinačne argumente.
Ako se program pokrene s
main tekst1 tekst2 tekst3
operacijski sustav će analizirati naredbeni redak, postaviti argc na 4 jer se naredbeni
redak sastoji od 4 komponente, te stvoriti 4 nezavisna niza znakova i inicijalizirati polje
pokazivača na znakovne nizove na način koji ilustrira sljedeća slika.

argv m a i n \0
argv[0]
argv[1] t e k s t 1 \0
argv[2]
argv[3] t e k s t 2 \0
argv[4]
t e k s t 3 \0

Slika 46. Argumenti naredbenog retka

237
Uvod u programiranje Ulaz i izlaz

Uzemljenje grafički simbolizira pokazivač NULL. Prvi element polja pokazivača, argv[0],
pokazuje na ime programa.
Sljedeći program ispisat će sve argumente iz naredbenog retka na tri načina. U prvoj
varijanti koristi se informacija sadržana u argc. U drugoj varijanti koristi se činjenica da
zadnji pokazivač u polju ima vrijednost NULL. U trećoj varijanti ime polja koristi se kao
pokazivač na prvi element polja koji će uvećavanjem biti preusmjeren na sljedeći element
polja.
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[]) {
int i;
for (i = 0; i < argc; i++) {
printf("%s ", argv[i]);
}
printf("\n");
for (i = 0; argv[i] != NULL; i++){
printf("%s ", argv[i]);
}
printf("\n");
for (; *argv != NULL; argv++){
printf("%s ", *argv);
}
return EXIT_SUCCESS;
}
Program 85. ArgumentiNaredbenogRetka.c

Slijedi slika koja ilustrira treću varijantu rješenja:

m a i n \0

t e k s t 1 \0

t e k s t 2 \0

t e k s t 3 \0

Slika 47. Iteracija po argumentima naredbenog retka

U sljedećem će se primjeru kopirati datoteka čije je ime zadano prvim argumentom


naredbenog retka u datoteku čije je ime zadano drugim argumentom naredbenog retka.
Datoteke mogu biti i binarne.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAXBUF 4096
int main(int argc, char *argv[]) {
// buf se koristi za dvije namjene
// u prvom dijelu za konstrukciju teksta greške
// konkatenacijom imena datoteke
// u drugom dijelu za čitanje bloka datotete
char buf[MAXBUF] = "Ne može se otvoriti datoteka ";

238
Uvod u programiranje Ulaz i izlaz

FILE *fi, *fo;


int n;
if (argc != 3) {
fprintf(stderr, "Neispravan broj argumenata");
return EXIT_FAILURE;
}
fi = fopen(argv[1], "rb");
if (fi == NULL) {
perror(strcat(buf, argv[1]));
return EXIT_FAILURE;
}
fo = fopen(argv[2], "wb");
if (fi == NULL) {
perror(strcat(buf, argv[2]));
return EXIT_FAILURE;
}
// čita se blok po blok maksimalne veličine MAXBUF
// posljednji pročitani blok bit će manji od MAXBUF
// a sljedeće čitanje vraća nulu
while ((n = fread(buf, 1, MAXBUF, fi)) > 0) {
// pročitano je n bajtova, pa toliko treba i zapisati
fwrite(buf, n, 1, fo);
}
fclose(fi);
fclose(fo);
return EXIT_SUCCESS;
}
Program 86. ArgumentiNaredbenogRetkaCopy.c

239
Uvod u programiranje Dinamička rezervacija memorije

Dinamicka rezervacija memorije


U poglavlju Polja spomenuto je da definicija polja fiksne veličine mora rezervirati
memoriju za najveći očekivani broj podataka. Alternativno, ako se broj podataka koji u
polje treba pohraniti može saznati prije definicije polja, može se koristiti polje varijabilne
veličine. Međutim, polju varijabilne veličine ne može se promijeniti veličina kad određeni
podaci više nisu potrebni ili ako se tijekom izvođenja programa pojave novi. Također,
memorija za polja varijabilne veličine, kao i za polja fiksne veličine definirana unutar
programskog bloka, rezervira se na stogu koji ima ograničenu veličinu.
Osim do sada spomenutih memorijskih segmenata koje program koristi tijekom svoga
rada za pohranu koda i varijabli (text, data, bss, stack kako je opisano u poglavlju Doseg i
vrijeme života varijabli), programu je na raspolaganju i dio memorije nazvan gomila
(heap). Veličina toga dijela memorije ovisi o prevodiocu i operacijskom sustavu, ali je
uvijek za nekoliko redova veličine veći od stoga94.
Pretpostavimo da su u datoteci temperature.txt upisane temperature izmjerene u nekom
vremenskom razdoblju. Potrebno je izračunati standardnu devijaciju tih temperatura.
Standardna devijacija je mjera raspršenosti podataka u nekom skupu. Što je veća, podaci
su međusobno različitiji. Izračunava se formulom
𝑛
1
𝜎= √ ∑(𝑥𝑖 − 𝑥̅ )2
𝑛−1
𝑖=1

gdje je 𝑥̅ prosječna vrijednost podataka iz skupa X.


Jedno od rješenja moglo bi biti čitati i zbrajati podatke iz datoteke, izračunati prosjek,
pozicionirati se na početak datoteke, pa u drugom čitanju izračunati 𝜎. Međutim, ako su
podaci potrebni i kasnije, primjerice u slučaju potrebe za određivanjem koje to
temperature odstupaju od prosjeka za više od 3 standardne devijacije, poželjno je podatke
pohraniti u polje. S dosad stečenim znanjem, ako se ne želi rezervirati prostor za polje
fiksne veličine, potrebno je podatke prvo prebrojati, tijekom prebrojavanja izračunati
prosjek, zatim se također pozicionirati na početak datoteke, definirati polje varijabilne
veličine s odgovarajućim brojem elemenata, te ponovno pročitati sve podatke.

Funkcije malloc, calloc i free


U poglavlju Polja spomenuto je da je ime polja zapravo pokazivač na početak polja. Ime
polja zato se može koristiti u notaciji pokazivača, kao u sljedećem primjeru u kojem se svi
elementi polja inicijaliziraju vrijednošću svoga indeksa.
int a[10];
for (int i = 0; i < 10; i++) {
*(a+i) = i;
}
Dakle, kad bi se nekako osiguralo da pokazivač pokazuje na kontinuirani memorijski
prostor odgovarajuće veličine, ne bi bilo potrebno definirati polje. Upravo to omogućavaju
funkcije deklarirane u <malloc.h>:

94 Pretpostavljena veličina gomile u 32-bitnoj inačici prevodioca gcc iznosi oko 2·109 bajta

240
Uvod u programiranje Dinamička rezervacija memorije

void *malloc(size_t size);


i
void *calloc(size_t num, size_t size);
Funkcija malloc pronalazi na gomili size slobodnih bajta, rezervira ih i vraća pokazivač na
prvi od njih. Sadržaj tako rezervirane memorije ne inicijalizira se.
Funkcija calloc pronalazi na gomili mjesto za num objekata pojedinačne veličine size,
rezervira ih, inicijalizira sve bajtove unutar tako rezerviranog prostora na nulu, te vraća
pokazivač na prvi od njih.
Kako ni jedna niti druga funkcija ne mogu predvidjeti za kakvu će se namjenu koristiti
tako rezervirana memorija, obje vraćaju void *, generički pokazivač čiju vrijednost treba
pridružiti nekom pokazivaču na podatke konkretnog tipa. Ako na gomili nije pronađeno
dovoljno slobodnog mjesta, funkcije vraćaju pokazivač NULL.
Rezerviranu memoriju, kad više nije potrebna, treba osloboditi pozivom funkcije
void free(void* ptr);
Slijedi program za izračunavanje standardne devijacije.
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <malloc.h>
int main(void) {
float *t, dummy, prosjek, stdev;
int n = 0, i;
FILE *f;
f = fopen("temperature.txt", "r");
if (f == NULL) {
perror("Nije uspjelo otvaranje datoteke");
return EXIT_FAILURE;
}
// prebrajanje temperatura
while (fscanf(f, "%f", &dummy) == 1) n++;
if (n < 2) {
fprintf(stderr, "Nema dovoljno podataka!\n");
return EXIT_FAILURE;
}
// rezervacija memorije za n temperatura
t = malloc(n * sizeof(*t));
if (t == NULL) {
perror("Neuspjela rezervacija");
return EXIT_FAILURE;
}
// povratak na početak datoteke radi stvarnog čitanja
fseek(f, 0L, SEEK_SET);
n = 0; // koristimo isti brojač
prosjek = 0;
while (fscanf(f, "%f", &t[n]) == 1) { // ili t + n
prosjek += t[n]; // ili *(t+n)
n++;
}
fclose(f); // f više ne treba pa odmah oslobađamo resurs
prosjek /= n;
// računanje standardne devijacije

241
Uvod u programiranje Dinamička rezervacija memorije

for (stdev = 0, i = 0; i < n; i++) {


stdev += (t[i] - prosjek) * (t[i] - prosjek);
}
stdev = sqrt (stdev / (n - 1));
printf("Standardna devijacija je %g", stdev);
free(t);
return EXIT_SUCCESS;
}
Program 87. StandardnaDevijacija.c

Kod prebrajanja je trebalo podatke učitavati u pomoćnu varijablu, iako ih se nije koristilo,
jer poziv funkcije
fscanf(f, "%*f");
uvijek vraća nulu, jer fscanf vraća broj stvarno pročitanih objekata.
Funkciji malloc predaje se veličina traženog kontinuiranog prostora izražena u bajtovima.
Potrebno je osigurati mjesta za n temperatura, svaka temperatura je float, pa se
rezervacija mogla obaviti i s
t = malloc(n * sizeof(float));
Međutim, ako se ukaže potreba da se temperature spremaju kao double, neće biti dovoljno
promijeniti samo definiciju pokazivača t, nego i poziv funkcije malloc. Poziv funkcije,
onako kako je napisan u programu
t = malloc(n * sizeof(*t));
nije osjetljiv na promjenu tipa podatka na koji pokazuje t.
Alternativno, memorija se mogla rezervirati i s
t = calloc(n, sizeof(*t));
U ovom slučaju svi elementi polja bili bi inicijalizirani na nulu, ali to u konkretnom slučaju
nije važno jer se svi oni kasnije učitavaju iz datoteke.
U cijelom programu, umjesto notacije sa uglatim zagradama, mogla se koristiti i notacija
koja koristi pokazivač jer je uvijek
p[i] ≡ *(p + i)
kao što je
&p[i] ≡ p + i
Uzastopni pozivi funkcija malloc i free mogu dovesti do segmentacije memorije.
Pretpostavimo da je funkcijom malloc redom rezervirano 4, 8 i 16 bajta memorije i da su
pokazivači na tako rezerviranu memoriju pohranjeni u pokazivačima p, r, i s. Nastaje
situacija kao na sljedećoj slici.

242
Uvod u programiranje Dinamička rezervacija memorije

stog p r s

gomila

prije slobodno
rezervirano 4 8 12

Nakon što se pozove


free(r);
vrijednost pokazivača r se ne mijenja ali se memorija na koju r pokazuje evidentira kao
slobodna

stog p r s

gomila

prije slobodno slobodno


rezervirano 4 8 12

Ako se sada obavi nova rezervacija


r = malloc(r, 12);
prvi kontinuirani prostor veličine 12 bajta nalazi se iza bloka na koji pokazuje s, pa će
nastati sljedeća situacija

stog p r s

gomila

prije slobodno
rezervirano 4 8 12 12

Blok veličine 8 bajta ostaje slobodan, ali se može iskoristiti samo za rezervaciju do 8 bajta
memorije. Moguće je, dakle, da neiskorištenih blokova s vremenom bude mnogo. Nadalje,
očito je da funkcije koje rezerviraju memoriju moraju negdje voditi evidenciju o zauzetim

243
Uvod u programiranje Dinamička rezervacija memorije

i slobodnim blokovima, pa će svaka rezervacija umanjiti količinu slobodne memorije za


više od rezerviranog broja bajtova.

Funkcija realloc
Rješenje iz prethodnog primjera gotovo je potpuno jednako rješenju s varijabilnim
poljem. Umjesto definiranja pokazivača, nakon što se prebroji koliko u datoteci ima
zapisanih temperatura, moglo se jednostavno umjesto rezervacije memorije
t = malloc(n * sizeof(*t));
na istome mjestu u programu definirati polje varijabilne veličine
float t[n];
Ostatak programa ostaje isti, osim što više nema poziva funkcije free. Razlika postoji
jedino u segmentu memorije u kojem se pronalazi kontinuirani prostor za polje. Kod
varijabilnih polja to je stog, a kod rezervacije funkcijom malloc to je gomila.
Poljima varijabilne veličine ne može se, međutim, mijenjati veličina uz garanciju očuvanja
podataka pohranjenih u njima. Kako je čitanje podataka s diska uvijek mnogo sporija
operacija nego što je to čitanje podataka iz memorije, jer ovisi o mehaničkim dijelovima,
bilo bi dobro podatke o temperaturama iz prethodnog primjera samo jednom pročitati.
Ovdje će pomoći funkcija
void *realloc(void *ptr, size_t new_size);
koja pokušava promijeniti rezervaciju bloka na koji pokazuje ptr na novu veličinu
new_size. Nova veličina pri tome može biti manja ili veća od stare veličine bloka.
Pretpostavimo se tijekom rada programa izda naredba
p = malloc(4);
Nešto kasnije, izdaje se naredba
p = realloc(p, 8);
Ovdje su moguće dvije situacije. Ako je iza bloka na koji pokazuje p memorija slobodna

stog p

gomila
prije slobodno
rezervirano 4
rezervacija se jednostavno proširuje i realloc vraća istu vrijednost pokazivača kakva je
bila prije:

stog p

gomila
prije slobodno
rezervirano 8

244
Uvod u programiranje Dinamička rezervacija memorije

Ali ako je u međuvremenu iza bloka na koji pokazuje p memorija nastala neka druga
rezervacija

stog p r

gomila
prije slobodno
rezervirano 4 4
rezervirat će se mjesto na prvoj sljedećoj poziciji na kojoj ima 8 slobodnih bajta, svi podaci
s originalne lokacije kopirat će na novu lokaciju, osloboditi stara lokacija i vratiti
pokazivač na novu lokaciju:

stog p r

gomila
prije slobodno
rezervirano 4 4 8

realloc vraća NULL ako proširenje rezervacije nije uspjelo, pa bi se naredbom


p = realloc(p, n);
u tome slučaju izgubila veza s postojećim podacima.
Bolje je zato rezultat funkcije realloc pohraniti u pomoćni pokazivač, pa preuzeti
vrijednost samo ako nije NULL
pom = realloc(p, n);
if (pom = NULL) {
p = pom;
} else {
// po potrebi, nastaviti rad uz poruku
// ili return EXIT_FAILURE;
}
Slijedi malo složeniji primjer u kojem se iz datoteke učitava skup točaka nakon čega se
pronalazi par točaka koje su najbliže jedna drugoj. Iako učinkovitiji algoritam postoji,
ovdje će biti prikazan samo najjednostavniji algoritam grube sile (brute force algorithm).
Njime se enumeriraju sve moguće kombinacije dviju točaka, izračunava njihova
udaljenost te, ako je međusobna udaljenost neke dvije točke manja od dosad pronađene
najmanje udaljenosti, pamte indeksi tih točaka i nova najmanja udaljenost. Pri tome nije
potrebno izračunavati udaljenost točke od same sebe niti udaljenost između točaka s
indeksima j i i ako je već izračunata udaljenost između točaka i i j, što se postiže
odgovarajućom gornjom granicom za indeks unutrašnje petlje. Točke će se učitati u
memoriju u jednom prolazu kroz ulaznu datoteku, tako da se prije učitavanja sljedećeg
para točaka polje točaka poveća za 1. Inicijalno pokazivač na polje točaka ima vrijednost
NULL, pa će prvi poziv funkcije realloc funkcionalno biti jednak pozivu funkcije malloc.
Funkciji koja računa međusobnu udaljenost predaju se pokazivači na točke čiju udaljenost

245
Uvod u programiranje Dinamička rezervacija memorije

treba izračunati (2 puta po 4 bajta), što je učinkovitije nego da se predaju cijele točke (2
puta po 16 bajtova).
#include <stdio.h>
#include <math.h>
#include <stdlib.h>
#include <malloc.h>
struct tocka {
double x;
double y;
} ;
double udaljenost(struct tocka *t1, struct tocka *t2) {
return sqrt(pow(t1->x - t2->x, 2) + pow(t1->y - t2->y, 2));
}
int main(void) {
struct tocka *p = NULL, *pom;
FILE *f;
int n = 0, i, j, mini, minj;
double d, mind;
f = fopen("tocke.txt", "r");
if (f == NULL) {
perror("Greška kod otvaranja");
return EXIT_FAILURE;
}
while (1) {
// pronađi mjesta za sljedeću točku
pom = realloc (p, (n + 1) * sizeof(*p));
if (pom == NULL) {
perror("Greška kod promjene rezervacije");
fprintf(stderr,
"Nastavlja se s dosad učitanim točkama\n");
break;
}
p = pom;
if (fscanf(f, "%lf %lf", &p[n].x, &p[n].y) < 2) break;
n++; // uspješno je pročitana sljedeća točka
}
fclose(f);
// početna vrijednost za najmanju udaljenost
// između bilo koje dvije točke
mind = udaljenost(p, p+1); // ili &p[0], &p[1]
// svaka točka sa svakom drugom točkom
for (i = 0; i < n; i++) {
for (j = 0; j < i; j++) {
d = udaljenost(p + i, p + j); // ili &p[i], &p[j]
if (d < mind) {
mind = d;
mini = i;
minj = j;
}
}
}
printf("Najbliže točke su (%g,%g) i (%g,%g)"
" s udaljenošću %g\n",
p[mini].x, p[mini].y, p[minj].x, p[minj].y, mind);
free(p);

246
Uvod u programiranje Dinamička rezervacija memorije

return EXIT_SUCCESS;
}
Program 88. NajblizeTocke.c

U najsloženijem primjeru učitat će se cijeli sadržaj tekstovne datoteke u memoriju tako da


ukupno zauzeće bude minimalno. Podsjetimo se, tekstovna datoteka može imati retke
različite duljine, čak i prazne retke, tako da bi sljedeća definicija takvog polja u memoriji
bila vrlo rastrošna čak i kad bi se unaprijed pročitalo koliko datoteka ima redaka:
#define MAX 512 // treba predvidjeti dovoljno za najdulji redak
char tekst[n][MAX];
Zato će se ovaj zadatak riješiti preko polja pokazivača na znakovne nizove, pri čemu će se
za svaki znakovni niz rezervirati upravo onoliko memorije koliko je potrebno.
Za datoteku
1. redak
R2

Posljednji redak
u memoriji treba ostvariti sljedeću strukturu

1 . r e d a k \0

R 2 \0

\0

P o s l j e d n j i r e d a k \0

Kad bi broj redaka datoteke koja se učitati u memoriji bio unaprijed poznat, primjerice
tako da se prvo pročita cijela datoteka s ciljem prebrajanja redaka, polje pokazivača na
pojedinačne retke moglo bi se definirati kao
char *p[n];
Međutim, ako je dozvoljeno samo jedno čitanje iz datoteke, ovo polje pokazivača treba
dinamički proširivati.
Općenito, ako postoji polje podataka bilo kojeg tipa,
tip p[n];
tada je ime polja pokazivač na prvi element polja, dakle na neki podatak određenoga tipa,
odnosno
tip *p;
U slučaju polja pokazivača na znakovne nizove
char *p[n];
tip je char * pa je pokazivač na takvo polje
char **p;
Program koji slijedi učitavat će redak po redak iz neke datoteke i nakon svakog učitanog
retka proširiti polje pokazivača za 1. Zatim će rezervirati točno onoliko memorije koliko

247
Uvod u programiranje Dinamička rezervacija memorije

treba za tako pročitani redak i pokazivač na tako rezerviranu memoriju upisati u novi
element polja pokazivača.
#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>
#include <string.h>
#define MAX 512
int main(void) {
char **p = NULL, **pom;
char redak[MAX];
FILE *f;
int n = 0, l, i;
f = fopen("main.c", "r");
if (f == NULL) {
perror("Greška kod otvaranja");
return EXIT_FAILURE;
}
while (1) {
// pronađi mjesta za sljedeći pokazivač
pom = realloc (p, (n + 1) * sizeof(*p));
if (pom == NULL) {
perror("Greška kod promjene rezervacije");
return EXIT_FAILURE;
}
p = pom;
// sljedeći redak čita se u pomoćno polje
if (fgets(redak, MAX, f) == NULL) break;
l = strlen(redak);
// fgets na pretposljednje mjesto ugrađuje /n
//pa ga treba ukloniti
redak[l-1] = '\0';
// niz je skraćen za jedan ali treba osigurati
// mjesta za \0
p[n] = malloc(l);
if (p[n] == NULL) {
perror("Greška kod rezervacije");
return EXIT_FAILURE;
}
// redak se kopira u rezerviranu memoriju
strcpy(p[n], redak);
n++;
}
fclose(f);
// kontrolni ispis
for (i = 0; i < n; i++) {
printf("|%s|\n", p[i]);
}
// oslobađanje memorije rezervirane za pojedinačne retke
for (i = 0; i < n; i++) {
free(p[i]);
}
// oslobađanje memorija rezervirane za polje pokazivača
free(p);
return EXIT_SUCCESS;
}

248
Uvod u programiranje Dinamička rezervacija memorije

Program 89. DatotekaUMemoriju.c.

249
Uvod u programiranje Rukovanje greškama

Rukovanje greskama
Programski jezik C robusnost programa uvijek podređuje brzini rada. Ugrađeni
mehanizmi sprječavanja izvanrednih situacija vrlo su slabi ili nepostojeći. Već je pokazano
da je zbog toga moguće indeksnim izrazom izaći izvan granica polja ili pokazivač usmjeriti
na pogrešnu lokaciju, što se neće lako primijetiti sve dok adresa takvog podatka pripada
adresnom prostoru programa. Standardni C ne poznaje try - catch naredbu ugrađenu u
jezike više razine (pa tako i C++) koja u slučaju bilo koje greške u bloku omeđenom
naredbama try i catch aktivira iznimku (exception) i nastavlja s izvođenjem bloka iza
naredbe catch. Zato je, gdje je god to moguće, izvanredne situacije potrebno predvidjeti u
kodu, poput do sada više puta viđenog ispitivanja je li otvaranje datoteke uspjelo, je li
funkcija malloc pronašla traženu količinu slobodne memorije ili je li pri čitanju datoteke
došlo do greške. Pri tome je moguće koristiti se i sistemskom varijablom
int errno;
koja sadrži numerički kôd greške, zatim funkcijom
char* strerror(int errnum);
koja vraća pokazivač na znakovni niz koji sadrži tekstovni opis greške s kodom errnum, te
funkcijom
void perror( const char *s );
koja je ekvivalentna sljedećoj naredbi
fprintf(stderr, "%s%s%s\n",
s == NULL ? "" : s, s == NULL ? "" : ": ", strerror(errno));
tj. ispisuje samo tekst greške ako je s NULL, inače poruku s, zatim ": " i na kraju tekst
greške.
Ovisno o karakteru greške, o čemu odlučuje programer, program može nastaviti s radom,
odnosno završiti s radom (ako je riječ o glavnom programu) naredbom
return EXIT_FAILURE;
ili, u bilo kojoj funkciji, pozivom funkcije
exit(EXIT_FAILURE);
ili, u bilo kojoj funkciji, pozivom funkcije abort koja je deklarirana u <stdio.h> kao
void abort(void);
i koja odmah prekida izvođenje programa i u operacijski sustav vraća status 3.
Status završetka programa može se provjeriti u operacijskom sustavu. U Windowsima se
to čini ispitivanjem sistemske varijable %ERRORLEVEL% a u Linuxu/Unixu ispitivanjem
sistemske varijable $?.
Robusnosti programa pomaže korištenje makro naredbe assert95 definirane u <assert.h>
kao
#ifdef NDEBUG
#define assert(condition) ((void)0)
#else
#define assert(condition) /*ovisno o prevodiocu*/
#endif

95 assert engl. ustvrditi

250
Uvod u programiranje Rukovanje greškama

condition je logički izraz koji treba biti istinit. Ako logički izraz nije istinit, program se
prekida pozivom funkcije abort s porukom
Assertion failed: condition, file: ime_izvorne_datoteke, line:
redni_broj_linije_u_izvornoj datoteci
assert može pomoći lakšem otkrivanju grešaka u programu. Ako se program koji sadrži
sljedeći odsječak
int i=1, j=0;
i = i / j;
izvodi kroz program za otkrivanje grešaka (debugger), dijeljenje s nulom izazvat će prekid
programa s porukom "Arithmetic exception" nakon čega se ispisuje poruka iz koje je jasno
gdje je greška nastala.
Tijekom izvođenja programa koji je pozvan izravno iz operacijskog sustava nema nikakve
informacije ni da je greška uopće nastala.
Ako se u program prije kritične operacije ugradi poziv funkcije assert
int i=1, j=0;
assert(j > 0);
i = i / j;
tada će se poruka "Assertion failed" ispisati i iz programa koji je pozvan izravno iz
operacijskog sustava.
Makro naredba assert doznaje ime izvorne datoteke i redni broj linije odakle je pozvana
iz sistemskih varijabli __FILE__ i __LINE__. Ove varijable, varijable __DATE__ i __TIME__ koje
sadrže datum i vrijeme prevođenja programa te varijablu __func__ koja sadrži ime funkcije
u kojoje se koristi, može se koristiti i u vlastitom kôdu.
Ako je naredbom #define ili opcijom prevodioca definirana simbolička konstanta
NDEBUG, assert je prazna naredba. Ispitivanje uvjeta navedenog navedenog u naredbi
assert troši procesorsko vrijeme, pa je, jednom kad se program dobro provjeri, sve
naredbe assert moguće isključiti prevođenjem programa uz definiranu konstantu
NDEBUG.

251
Uvod u programiranje Kompleksni brojevi

Kompleksni brojevi
Izračunavanje korijena kvadratne jednadžbe
ax2 + bx + c = 0
jedan je od školskih primjera za selekciju jer se program treba granati u ovisnosti od
vrijednosti koeficijenata. Dobar je to primjer i za primjenu kompleksnih brojeva.
Formula kojom se izračunavaju korijeni kvadratne jednadžbe je
−𝑏 ± √𝑏 2 − 4𝑎𝑐
𝑥1,2 =
2𝑎
Ako je diskriminanta b2 - 4ac negativna, rješenja su kompleksni brojevi. Općenito, realni
broj se može promatrati kao kompleksni broj kojem je imaginarni dio jednak nuli, pa se
za rješenja x1 i x2 može uvijek koristiti tip _Complex. Ako se u prevođenje uključi datoteka
<complex.h>, kompleksne varijable mogu se definirati prirodnijom sintaksom koristeći tip
complex. Definicija kompleksnog broja a + bi tvori se od tipa podatka za komponente a i b
te ključne riječi _Complex odnosno complex, primjerice
float complex x; // a i b su tipa float
double complex y; // a i b su tipa double
long double complex z; // a i b su tipa long double
Kompleksnom broju vrijednost se postavlja izrazom oblika
realni_dio + kompleksni_dio * I
primjerice
y = 5 + 3 * I;
U gcc-u je I je makro definiran u <complex.h> kao
#define I _Complex_I;
a ovaj dalje kao
#define _Complex_I (0.0F + 1.0iF)
Makro I napisan je velikim slovom da se izbjegne konflikt s vrlo često korištenom
varijablom i u kontekstu indeksa ili brojača.
Iz kompleksnog broja realni i imaginarni dio izdvajaju se makro naredbama definiranim
u <complex.h>
float crealf(float complex z);
double creal(double complex z);
long double creall(long double complex z);
odnosno
float cimagf(float complex z);
double cimag (double complex z);
long double cimagl(long double complex z);
Za kompleksne brojeve implementirani su standardni aritmetički operatori, ali ne postoji
konverzijska specifikacija za ispis kompleksnih brojeva, pa ih treba ispisati po
komponentama kao u primjeru
printf("%g + %gi", creal(y), cimag(y));

252
Uvod u programiranje Kompleksni brojevi

To će rezultirati relativno neurednim ispisom za kompleksne brojeve kojima je


imaginarni dio negativan, pa će se za primjerice
y = 5 - 3 * I;
ispisati
5 + -3i
Prirodnije bi ovaj broj bilo ispisati kao
5 – 3i
Za to se može napisati makro koji će kompleksni broj rastaviti u tri komponente koje će
se ispisati po konverzijskim specifikacijama "%g %c %g". Tako će ispis sljedećeg
programa
#include <stdio.h>
#include <stdlib.h>
#include <complex.h>
#include <math.h>
#define fComplex(x) creal(x), cimag(x)>0?'+':'-', fabs(cimag(x)
int main(void) {
double complex x1, x2, x3, x4, x5, x6;
x1 = 1 + 2*I;
x2 = 2 + 3*I;
x3 = x1 + x2;
x4 = x1 - x2;
x5 = x1 * x2;
x6 = x1 / x2;
printf("%g %c %gi\n%g %c %gi\n%g %c %gi\n%g %c %gi\n",
fComplex(x3), fComplex(x4),
fComplex(x5), fComplex(x6));
return EXIT_SUCCESS;
}
biti
3 + 5i
-1 - 1i
-4 + 7i
0.615385 + 0.0769231i
Treba napomenuti da se ovako napisan makro ne može koristiti ako argument nije samo
jedan kompleksni broj te ako se nad argumentom primjenjuju operatori uvećanja i
umanjenja za 1. Ovi su operatori definirani i nad podacima tipa _Complex tako da
uvećavaju odnosno umanjuju realni dio broja. U primjeru
… fcomplex(++x1)…
uvećavanje realnog dijela broja obavilo bi se 3 puta. Rješenje s funkcijom koja vraća
pokazivač na lokalnu statičku varijablu, primjerice
char *fComplex(complex c) {
char *s[128];
sprintf(s, "%g %c %g",
creal(x), cimag(x)>0?'+':'-', fabs(cimag(x));
return s;
}

253
Uvod u programiranje Kompleksni brojevi

također nije dobro jer se takva funkcija može koristiti samo jednom u pozivu funkcije
printf. Više poziva ove funkcije u istom pozivu funkcije printf rezultirat će ispisom istih
vrijednosti.
Slijedi program koji za utipkane koeficijente a, b i c računa korijene odgovarajuće
kvadratne jednadžbe.
#include <stdio.h>
#include <stdlib.h>
#include <complex.h>
#include <math.h>
#define fComplex(x) creal(x), cimag(x)>0?'+':'-', fabs(cimag(x))
int main(void) {
double complex x1, x2;
double a, b, c, d;
scanf("%lf %lf %lf", &a, &b, &c);
if (a == 0) {
printf("Jednadžba nije kvadratna\n");
return EXIT_FAILURE;
} else {
d = b * b - 4 * a * c;
if (d == 0) {
x1 = - b / 2 * a + 0*I;
x2 = x1;
} else if (d > 0) {
x1 = (-b + sqrt(d)) / (2 * a) + 0 * I;
x2 = (-b - sqrt(d)) / (2 * a) + 0 * I;
} else {
x1 = - b / 2 * a + sqrt(-d) / 2 * a * I;
x2 = - b / 2 * a - sqrt(-d) / 2 * a * I;
}
}
printf("x1 = %g %c %gi, x2 = %g + %gi",
fComplex(x1), fComplex(x2));
return EXIT_SUCCESS;
}
Program 90. KorijeniGlavni.c

Za unos
1 2 3
program će ispisati
x1 = -1 + 1.41421i, x2 = -1 - 1.41421i
Za unos
1 6 3
rješenje je
x1 = -0.55051 + 0i, x2 = -5.44949 + 0i
Praktičnije je načiniti funkciju koja će biti ponovno iskoristiva. Funkcija, međutim, mora
vratiti 2 rezultata (x1 i x2), a u slučaju da je unesen koeficijent a=0 mora na pozivajuću
razinu vratiti odgovarajući status. Zato bi takva funkcija trebala biti tipa _Boolean ili bool
i vratiti true ako su koeficijenti ispravno zadani, inače false.
Rješenje je

254
Uvod u programiranje Kompleksni brojevi

#include <stdio.h>
#include <stdlib.h>
#include <complex.h>
#include <math.h>
#include <stdbool.h>
#define fComplex(x) creal(x), cimag(x)>0?'+':'-', fabs(cimag(x))
bool korijeni(double a, double b, double c,
double complex *x1, double complex *x2) {
double d;
if (a == 0) return false;
d = b * b - 4 * a * c;
if (d == 0) {
*x1 = - b / 2 * a + 0*I;
*x2 = *x1;
} else if (d > 0) {
*x1 = (-b + sqrt(d)) / (2 * a) + 0 * I;
*x2 = (-b - sqrt(d)) / (2 * a) + 0 * I;
} else {
*x1 = - b / 2 * a + sqrt(-d) / 2 * a * I;
*x2 = - b / 2 * a - sqrt(-d) / 2 * a * I;
}
return true;
}

int main(void) {
double complex x1, x2;
double a, b, c;
scanf("%lf %lf %lf", &a, &b, &c);
if (korijeni(1, 2, 3, &x1, &x2)) {
printf("x1 = %g %c %gi, x2 = %g %c %gi",
fComplex(x1), fComplex(x2));
} else {
printf("Jednadžba nije kvadratna\n");
}
return 0;
}
Program 91. KorijeniFunkcija.c

Nad kompleksnim brojevima definirane su i matematičke funkcije


double carg (double _Complex);
double cabs (double _Complex);
double _Complex conj (double _Complex);
double _Complex cacos (double _Complex);
double _Complex casin (double _Complex);
double _Complex catan (double _Complex);
double _Complex ccos (double _Complex);
double _Complex csin (double _Complex);
double _Complex ctan (double _Complex);
double _Complex cacosh (double _Complex);
double _Complex casinh (double _Complex);
double _Complex catanh (double _Complex);
double _Complex ccosh (double _Complex);
double _Complex csinh (double _Complex);
double _Complex ctanh (double _Complex);
double _Complex cexp (double _Complex);

255
Uvod u programiranje Kompleksni brojevi

double _Complex clog (double _Complex);


double _Complex cpow (double _Complex, double _Complex);
double _Complex csqrt (double _Complex);
double _Complex cproj (double _Complex);
i njihove inačice tipa float (sa sufiksom f) i long double (sa sufiksom l). Nazivi funkcija
većinom se tvore od prefiksa c i naziva odgovaruće funkcije iz realne domene. carg računa
argument (fazni kut) kompleksnog broja, conj konjugirani broj, a cproj projekciju na
Riemannovoj sferi.

256
Uvod u programiranje Tablica prioriteta operatora

Tablica prioriteta operatora


Iako je većina operatora i njihovih prioriteta obrađena u poglavlju Operatori i izrazi, neki
su se novi operatori pojavili u kontekstu funkcija, polja i struktura. Slijedi kompletan
pregled operatora programskog jezika C, njihovih prioriteta i asocijativnosti.
Prioritet Operator Opis Asocijativnost
1 ++ -- povećanje i umanjenje za 1 u postfiksnoj
notaciji
() poziv funkcije
[] indeksiranje elementa polja L→D
. pristup elementu strukture
-> pristup elementu strukture preko
pokazivača
2 ++ -- povećanje i umanjenje za 1 u prefiksnoj
notaciji
+ - unarni plus i minus
! ~ logička negacija i negacija nad bitovima
(tip) pretvorba tipa D→L
* dereferenciranje
& adresa
sizeof veličina u bajtovima
3 * / % množenje, dijeljenje i cjelobrojni ostatak L→D
4 + - zbrajanje i oduzimanje L→D
5 << >> posmak bitova lijevo i desno L→D
6 < <= manje, manje ili jednako
L→D
> >= veće, veće ili jednako
7 == != jednako, različito L→D
8 & and nad bitovima L→D
9 ^ xor nad bitovima L→D
10 | or nad bitovima L→D
11 && logički and L→D
12 || logički or L→D
13 ?: ternarni operator D→L
14 = pridruživanje
+= -= skraćeno pridruživanje zbrajanjem i
oduzimanjem
*= /= %= skraćeno pridruživanje množenjem,
dijeljenjem i modulom D→L
<<= >>= skraćeno pridruživanje posmakom lijevo i
desno
&= ^= |= skraćeno pridruživanje uz and, xor i or nad
bitovima
15 , odvajanje naredbi zarezom L→D

257
Uvod u programiranje Popis slika

Popis slika
Slika 1. Abacus (Pixabay, Creative Commons CC0) ............................................................................ 3
Slika 2. Pascaline (autor Rama, CC BY-SA 30. FR) ............................................................................... 4
Slika 3. Charles Babbage (Public Domain) ............................................................................................. 5
Slika 4. Analitički stroj, Science Museum London (autor Marcin Wichary, San Francisco,
U.S.A, CC BY 2.0) ............................................................................................................................................... 6
Slika 5. Ada Lovelace (Public Domain) .................................................................................................... 7
Slika 6. Hollerithov stroj (Public Domain) ............................................................................................. 8
Slika 7. Alan Turing (Public Domain)....................................................................................................... 8
Slika 8. Program za Turingov stroj ........................................................................................................ 10
Slika 9. Programiranje Eniaca (Public Domain) ............................................................................... 11
Slika 10. Von Neumannova arhitektura ............................................................................................... 12
Slika 11. Grace Hopper (Public Domain) ............................................................................................. 13
Slika 12. Prvi bug (Public Domain) ........................................................................................................ 13
Slika 13. Bardeen, Shockley, Brattain (Public Domain) ................................................................. 14
Slika 14. Stanje bistabila u vremenu ..................................................................................................... 17
Slika 15. Dijagram toka određivanja većeg od dva broja .............................................................. 51
Slika 16. Dijagram toka prevođenja izvornog kôda u izvršni kôd ............................................. 63
Slika 17. Jednostrana selekcija ................................................................................................................ 89
Slika 18. Dvostrana selekcija .................................................................................................................... 90
Slika 19. Višestrana selekcija ................................................................................................................... 91
Slika 20. Mogući presjeci dvaju intervala ............................................................................................ 93
Slika 21. Skretnica ........................................................................................................................................ 95
Slika 22. Petlja while ................................................................................................................................... 98
Slika 23. Petlja do........................................................................................................................................100
Slika 24. Petlja for .......................................................................................................................................102
Slika 25. Slijed izvođenja naredbi kod poziva funkcije ................................................................118
Slika 26. Slijed izvođenja naredbi kod poziva funkcije iz druge funkcije .............................119
Slika 27. Stog ................................................................................................................................................120
Slika 28. Poziv funkcije i stog .................................................................................................................121
Slika 29. Rekurzija bez završetka .........................................................................................................122
Slika 30. Rekurzija bez završetka i stog .............................................................................................123
Slika 31. Brojanje rekurzijom ................................................................................................................124
Slika 32. Brojanje rekurzijom u povratku .........................................................................................125
Slika 33. Faktorijeli rekurzivno .............................................................................................................126
Slika 34. Doseg globalnih varijabli .......................................................................................................135
Slika 35. Stanje stoga kod neispravnog pokušaja zamjene funkcijom ...................................158
Slika 36. Stanje stoga kod ispravne zamjene funkcijom ..............................................................159
Slika 37. Čvrsti disk (Pixabay, CC0) .....................................................................................................217
Slika 38. Fizička organizacija čvrstog diska (Public Domain) ...................................................218
Slika 39. Logička organizacija čvrstog diska ....................................................................................219
Slika 40. Standardni ulaz i izlaz .............................................................................................................219
Slika 41. Drugi tokovi podataka ............................................................................................................220
Slika 42. Preusmjeravanje standardnog ulaza i izlaza .................................................................221
Slika 43. Preusmjeravanje uz stderr ...................................................................................................223
Slika 44. Formatirano zapisivanje u datoteku .................................................................................228
Slika 45. Zapisivanje u binarnu datoteku ..........................................................................................229
Slika 46. Argumenti naredbenog retka ..............................................................................................237

258
Uvod u programiranje Popis slika

Slika 47. Iteracija po argumentima naredbenog retka.................................................................238

259
Uvod u programiranje Popis programa

Popis programa
Program 1. CiklusBrojanja.c ..................................................................................................................... 15
Program 2. OdredjivanjeVecegBroja.c.................................................................................................. 51
Program 3. ZamjenaVarijabli.c ................................................................................................................ 73
Program 4. Sort3Vrijednosti.c ................................................................................................................. 92
Program 5. Intervali.c ................................................................................................................................. 93
Program 6. KrivoNapisaniIf.c ................................................................................................................... 94
Program 7. SkretnicaSamoglasnici.c ..................................................................................................... 95
Program 8. Kalkulator.c ............................................................................................................................. 97
Program 9. KalkulatorPetlja.c .................................................................................................................. 98
Program 10. SumaZnamenki.c................................................................................................................. 99
Program 11. Faktorijeli.c .........................................................................................................................103
Program 12. Bitovi.c ..................................................................................................................................104
Program 13. ProstBezBreak.c ................................................................................................................107
Program 14. ProstSBreak.c .....................................................................................................................107
Program 15. PovrhBezFunkcije.c .........................................................................................................110
Program 16. PovrhSFunkcijom.c ..........................................................................................................112
Program 17. PovrhSFunkcijom1.c .......................................................................................................112
Program 18. PovrhSFunkcijom2.c .......................................................................................................113
Program 19. PovrhSFunkcijom3.c .......................................................................................................114
Program 20. VarijabilniBrojArgumenata.c .......................................................................................116
Program 21. LokalnaVarijablaFunkcije.c ..........................................................................................117
Program 22. DijeljenjeStoga.c ................................................................................................................122
Program 23. RekurzijaBezZavrsetka.c ...............................................................................................122
Program 24. BrojanjeRekurzijom.c .....................................................................................................123
Program 25. BrojanjeRekurzijomUPovratku.c................................................................................124
Program 26. FaktorijeliRekurzivno.c ..................................................................................................125
Program 27. LokalneVarijableUnutarBloka.c ..................................................................................132
Program 28. SinusRedomLose.c ...........................................................................................................137
Program 29. SinusRedomDobro.c ........................................................................................................138
Program 30. RandTransformacija.c.....................................................................................................145
Program 31. RandPeriod.c ......................................................................................................................146
Program 32. HiLoGameIgrac.c ...............................................................................................................148
Program 33. HiLoGameRacunalo.c ......................................................................................................148
Program 34. PokazivacEndian.c ...........................................................................................................155
Program 35. BitoviPoBajtovima.c ........................................................................................................156
Program 36. ZamjenaFunkcijomNeispravno.c ................................................................................158
Program 37. ZamjenaFunkcijomIspravno.c .....................................................................................159
Program 38. FrekvencijeBezPolja.c .....................................................................................................160
Program 39. FrekvencijePoljem.c ........................................................................................................164
Program 40. VeciOdProsjekaFiksno.c.................................................................................................170
Program 41. JednodimenzijskoPoljeFunkcija.c ..............................................................................172
Program 42. strlen.c ..................................................................................................................................174
Program 43. UvecanjePokazivacaSDereferenciranjem.c ............................................................176
Program 44. strcpy.c .................................................................................................................................177
Program 45. strncpy.c ...............................................................................................................................177
Program 46. strcat.c...................................................................................................................................178
Program 47. strcmp.c ................................................................................................................................178

260
Uvod u programiranje Popis programa

Program 48. strcmpi.c...............................................................................................................................179


Program 49. strchr.c ..................................................................................................................................179
Program 50. strrchr.c ................................................................................................................................179
Program 51. strpbrk.c ...............................................................................................................................180
Program 52. JedinicnaMatrica.c ............................................................................................................181
Program 53. TransponiranjeMatrice.c ...............................................................................................186
Program 54. ZbrajanjeMatrica.c ...........................................................................................................187
Program 55. NeispravneDimenzije.c ..................................................................................................188
Program 56. MnozenjeMatrica.c ...........................................................................................................189
Program 57. UdaljenostTocaka.c ..........................................................................................................191
Program 58. SjecistePravaca.c ...............................................................................................................192
Program 59. StudentProsjekOcjena.c .................................................................................................194
Program 60. NajblizaTocka.c .................................................................................................................196
Program 61. NajboljiStudent.c ..............................................................................................................196
Program 62. UnionBajtovi.c....................................................................................................................199
Program 63. SkupineBitova.c .................................................................................................................200
Program 64. getchar.c ...............................................................................................................................202
Program 65. getcharDo.c .........................................................................................................................203
Program 66. getch.c ...................................................................................................................................203
Program 67. gets.c ......................................................................................................................................204
Program 68. gets1.c ...................................................................................................................................204
Program 69. getsGotovo.c .......................................................................................................................205
Program 70. scanf1.c .................................................................................................................................210
Program 71. scanf2.c .................................................................................................................................210
Program 72. sprintf.c ................................................................................................................................216
Program 73. PrimjerStderr.c ..................................................................................................................222
Program 74. fgetc.c ....................................................................................................................................225
Program 75. fgets.c ....................................................................................................................................226
Program 76. fscanf.c ..................................................................................................................................226
Program 77. PotencijeUDatoteku.c......................................................................................................228
Program 78. PotencijeUBinarnuDatoteku.c .....................................................................................229
Program 79. PotencijeIzBinarneDatoteke1.c ..................................................................................230
Program 80. PotencijeIzBinarneDatoteke2.c ..................................................................................230
Program 81. TekstUBinarnu.c ...............................................................................................................232
Program 82. TekstUBinarnuStruct.c ...................................................................................................232
Program 83. AzuriranjeBinarne.c ........................................................................................................235
Program 84. AzuriranjeBinarneSlijedno.c ........................................................................................236
Program 85. ArgumentiNaredbenogRetka.c ....................................................................................238
Program 86. ArgumentiNaredbenogRetkaCopy.c .........................................................................239
Program 87. StandardnaDevijacija.c ...................................................................................................242
Program 88. NajblizeTocke.c .................................................................................................................247
Program 89. DatotekaUMemoriju.c. ....................................................................................................249
Program 90. KorijeniGlavni.c .................................................................................................................254
Program 91. KorijeniFunkcija.c ............................................................................................................255

261
Uvod u programiranje Literatura

Literatura
1. K. N. King, C Programming: A Modern Approach, Second Edition, W. W. Norton &
Company
2. B. W. Kernighan, D. M. Ritchie: The C Programming Language, Second Edition,
Prentice Hall, 1988.
3. Programming in C, Oxford University Computing Services, 1996.
4. D. M. Jones, The New C Standard, http://www.knosof.co.uk/cbook/cbook.html,
2009.
5. https://en.cppreference.com/w/
6. https://www.wikipedia.org/

262

You might also like