PROGRAMIRANJE

C JEZIKOM


Nastavni materijal za studente FESB-a.




Split, 2005/2006

Autor: Ivo Mateljan




                                         1
Sadržaj
1 Uvod........................................................................................................................................... 5
2 Matematički i elektronički temelji računarstva ........................................................................ 13
   2.1 Izjavna i digitalna logika ................................................................................................... 13
   2.2 Brojevni sustavi i računska sposobnost računala .............................................................. 16
3 Izrada prvog C programa.......................................................................................................... 20
   3.1 Strojni, asemblerski i viši programski jezici ..................................................................... 20
   3.2 Prvi program u C jeziku .................................................................................................... 21
   3.3 Struktura i kompiliranje C programa ................................................................................ 25
   3.4 Integrirana razvojna okolina (IDE) ................................................................................... 27
   3.5 Usmjeravanje procesa kompiliranja programom nmake................................................... 32
4 Kodiranje i tipovi podataka ...................................................................................................... 33
   4.1 Kodiranje i zapis podataka ................................................................................................ 33
   4.2 Memorija ........................................................................................................................... 40
   4.3 Prosti tipovi podataka........................................................................................................ 42
   4.4 Direktiva #define............................................................................................................... 46
   4.5 Specifikatori printf funkcije .............................................................................................. 47
   4.6 Pristup podacima pomoću pokazivača .............................................................................. 49
   4.7 Unos podataka u memoriju računala ................................................................................. 52
   4.8 Inicijalizacija varijabli....................................................................................................... 54
5 Uvod u programiranje C jezikom............................................................................................. 55
   5.1 Postupak izrade programa ................................................................................................. 55
   5.2 Algoritamska struktura C programa? ............................................................................... 57
   5.3 Funkcije C jezika............................................................................................................... 63
   5.4 Zaključak........................................................................................................................... 70
6 Izrazi i sintaksa C jezika........................................................................................................... 71
   6.1 Izrazi.................................................................................................................................. 71
   6.2 Automatska i explicitna pretvorba tipova ......................................................................... 78
   6.3 Definiranje sinonima tipa pomoću typedef ....................................................................... 81
   6.4 Formalni zapis sintakse C-jezika....................................................................................... 81
7 Proste i strukturalne naredbe C jezika...................................................................................... 87
   7.1 Proste naredbe ................................................................................................................... 87
   7.2 Strukturalne naredbe ......................................................................................................... 89
8 Nizovi..................................................................................................................................... 102
   8.1 Jednodimenzionalni nizovi.............................................................................................. 102
   8.2 Prijenos nizova u funkciju............................................................................................... 107
   8.3 Višedimenzionalni nizovi................................................................................................ 110
9 Blokovi, moduli i dekompozicija programa........................................................................... 112
   9.1 Blokovska struktura programa ........................................................................................ 112
   9.2 Funkcionalna dekompozicija programa "od vrha prema dolje" ...................................... 120
   9.3 Zaključak......................................................................................................................... 128
10 Rad s pokazivačima.............................................................................................................. 129




                                                                                                                                                 2
10.1 Tip pokazivača .............................................................................................................. 129
   10.2 Operacije s pokazivačima.............................................................................................. 130
   10.3 Pokazivači kao argumenti funkcije ............................................................................... 131
   10.4 Pokazivači i nizovi ........................................................................................................ 132
   10.5 Pokazivači i argumenti funkcije tipa niza ..................................................................... 134
   10.6 Patrametri funkcije tipa void pokazivača ...................................................................... 136
   10.7 Pokazivači na funkcije .................................................................................................. 137
   10.8 Kompleksnost deklaracija ............................................................................................. 139
   10.9 Polimorfne funkcije....................................................................................................... 141
   10.10 Zaključak..................................................................................................................... 144
11 Nizovi znakova - string ........................................................................................................ 146
   11.1 Definicija stringa ........................................................................................................... 146
   11.2 Standardne funkcije za rad sa stringovima.................................................................... 148
   11.3 Ulazno-izlazne operacije sa stringovima....................................................................... 151
   11.4 Korisnički definirane ulazne operacije sa stringovima ................................................. 152
   11.5 Pretvorba stringa u numeričku vrijednost ..................................................................... 153
   11.6 Nizovi stringova ............................................................................................................ 155
   11.7 Generator slučajnih brojeva .......................................................................................... 157
   11.8 Argumenti komandne linije operativnog sustava.......................................................... 158
12 Dinamičko alociranje memorije ........................................................................................... 160
   12.1 Funkcije za dinamičko alociranje memorije ................................................................. 160
   12.2 Kako se vrši alociranje memorije.................................................................................. 163
   12.3 Alociranje višedimenzionalnih nizova .......................................................................... 165
   12.4 Standardne funkcije za brzi pristup memoriji ............................................................... 171
13 Korisnički definirane strukture podataka ............................................................................. 172
   13.1 Struktura (struct)...................................................................................................... 172
   13.2 Union – zajednički memorijski objekt za različite tipova podataka.............................. 180
   13.3 Bit-polja......................................................................................................................... 181
   13.4 Pobrojanji tip (enum).................................................................................................... 182
   13.5 Strukture i funkcije za očitanje vremena....................................................................... 183
14 Leksički pretprocesor ........................................................................................................... 188
   14.1 Direktiva #include ......................................................................................................... 188
   14.2 Direktiva #define za makro-supstitucije........................................................................ 188
   14.3 String operatori # i ##.................................................................................................. 190
   14.4 Direktiva #undef............................................................................................................ 191
   14.5 Direktive za uvjetno kompiliranje................................................................................. 192
15 Rad s datotekama i tokovima ............................................................................................... 194
   15.1 Ulazno-izlazni tokovi .................................................................................................... 194
   15.2 Binarne i tekstualne datoteke ........................................................................................ 195
   15.3 Pristup datotekama ........................................................................................................ 195
   15.4 Formatirano pisanje podataka u datoteku...................................................................... 197
   15.5 Formatirano čitanje podataka iz datoteke...................................................................... 199
   15.6 Znakovni ulaz/izlaz ....................................................................................................... 200
   15.7 Direktni ulaz/izlaz za memorijske objekte .................................................................... 203
   15.8 Sekvencijani i proizvoljni pristup datotekama .............................................................. 206
   15.9 Funkcije za održavanje datoteka ................................................................................... 208
16 Apstraktni tipovi podataka - ADT........................................................................................ 210




                                                                                                                                            3
16.1 Koncept apstraktnog dinamičkog tipa podataka ........................................................... 210
   16.2 Stog i STACK ADT ...................................................................................................... 215
   16.3 Primjena stoga za proračun izraza postfiksne notacije.................................................. 218
   16.4 Red i QUEUE ADT....................................................................................................... 221
   16.5 Zaključak....................................................................................................................... 224
17 Rekurzija i složenost algoritama .......................................................................................... 225
   17.1 Rekurzivne funkcije ...................................................................................................... 225
   17.2 Matematička indukcija .................................................................................................. 227
   17.3 Kule Hanoja .................................................................................................................. 227
   17.4 Metoda - podijeli pa vladaj (Divide and Conquer)........................................................ 230
   17.5 Pretvorba rekurzije u iteraciju ....................................................................................... 232
   17.6 Standardna bsearch() funkcija ....................................................................................... 234
   17.7 Složenost algoritama - "Veliki - O" notacija................................................................. 236
   17.8 Sortiranje ....................................................................................................................... 239
   17.9 Zaključak....................................................................................................................... 248
18 Samoreferentne strukture i liste............................................................................................ 249
   18.1 Samoreferentne strukture i lista..................................................................................... 249
   18.2 Operacije s vezanom listom .......................................................................................... 250
   18.3 Što može biti element liste ............................................................................................ 259
   18.4 Lista sa sortiranim redoslijedom elemenata .................................................................. 260
   18.5 Implementacija ADT STACK pomoću linearne liste ................................................... 265
   18.6 Implementacija ADT QUEUE pomoću vezane liste..................................................... 267
   18.7 Dvostruko vezana lista .................................................................................................. 269
   18.8 Generički dvostrani red - ADT DEQUEUE.................................................................. 271
   18.9 Zaključak....................................................................................................................... 280
19 Razgranate strukture - stabla ................................................................................................ 281
   19.1 Definicija stabla ............................................................................................................ 281
   19.2 Binarno stablo ............................................................................................................... 282
   19.3 Interpreter prefiksnih izraza .......................................................................................... 291
   19.4 Stabla s proizvoljnim brojem grana .............................................................................. 305
   19.5 Prioritetni redovi i hrpe ................................................................................................. 309
   19.6 Zaključak....................................................................................................................... 316
20 Strukture za brzo traženje podataka .................................................................................... 317
   20.1 Tablice simbola i rječnici .............................................................................................. 317
   20.2 Hash tablica ................................................................................................................... 318
   20.3 BST - binarno stablo traženja........................................................................................ 333
   20.4 Crveno-crna stabla......................................................................................................... 344
Literatura ................................................................................................................................... 354
Dodatak ..................................................................................................................................... 355
   Dodatak A - Elementi dijagrama toka................................................................................... 355
   Dodatak B - Gramatika C jezika ........................................................................................... 356
   Dodatak C - Standardna biblioteka C jezika ......................................................................... 361
Index.......................................................................................................................................... 392




                                                                                                                                                 4
1 Uvod




Naglasci:
   • Što je računalo ?
   • Što je program ?
   • Kako se rješavaju problemi pomoću računala?
   • Računarski procesi i memorijski objekti
   • Apstrakcija, algoritam, program


    Računalo ili kompjuter (eng. computer) je naziv za uređaje koji obavljaju radnje prema
programima koje izrađuje čovjek. Sastavni dijelovi računala nazivaju se hardver, a programi i
njihova dokumentacija nazivaju se softver. Prvotno su računala služila za obavljanje numeričkih
proračuna, odatle i potječe naziv računalo. Danas računala služe za obradu različitih problema.
    Korisnike računala zanima kako se koristi računalo, a one koji izučavaju računala zanima:
      •   kako se izrađuje računalo,
      •   kako se izrađuje program i
      •   kako se rješavaju problemi pomoću računala.
     Ovdje će biti pokazano kako se izrađuju programi i kako se programiranjem rješavaju
različiti problemi. Bit će opisana i unutarnja građa računala. Za pisanje programa koristit će se
programski jeziku C i asemblerski jezik.

Što je program?
     Program je zapis operacija koje računalo treba obaviti. Taj zapis može biti u obliku
izvršnog programa ili u obliku izvornog programa. Izvršni program sadrži kôd operacija koje
izvršava stroj računala, pa se naziva i strojni program. Izvorni program se zapisuje simboličkim
jezikom koji se naziva programski jezik. Prevođenje izvornog programa u strojni program vrši
se pomoću programa koji se nazivaju kompilatori (ili kompajleri).

Stroj računala
     Postoje dva tipa elektroničkih računala: analogna i digitalna. Analognim računalima se
obrađuju kontinuirani elektronički signali. Digitalnim računalom se obrađuju, prenose i pamte
diskretni elektronički signali koji u jednom trenutku mogu imati samo jedno od dva moguća
stanja. Ta stanja se označavaju znamenkama 0 i 1, odatle i naziv digitalna računala (eng. digit
znači znamenka). Programere i korisnike ne zanimaju elektronički signali u računalu, već
poruka koju oni prenose – digitalna informacija.
     Brojevni sustav, u kojem postoje samo dvije znamenke, naziva se binarni brojevni sustav.
U tom se sustavu može kodirati različite informacije koristeći više binarnih znamenki.
Znamenka binarnog brojevnog sustava se naziva bit (kratica od eng. binary digit), a može imati
samo dvije vrijednosti 0 ili 1. Niz od više bitova predstavlja kodiranu informaciju koja može




                                                                                               5
predstavljati operaciju koju računalo treba izvršiti ili neki smisleni podatak. Uobičajeno je za
nizove bitova koristiti nazive iz Tablice 1.1.
      U binarnom nizu često se označava redoslijed bitova. Kratica LSB označava bit najmanjeg
značaja (eng. least significant bit), a MSB označava bit najvećeg značaja (eng. most significant
bit). Primjer je dan na slici 1.1.


      Bit      je naziv za binarnu znamenku
      Nibl     je naziv za skupinu od četiri bita (eng. nibble) s kojom se operira kao s
                cjelinom.
      Bajt     ili oktet je naziv za skupinu od osam bita (eng. byte) s kojom se operira kao s
               cjelinom.
      Riječ     je naziv za skupinu od više bajta (eng. word) s kojom se operira kao s cjelinom.
                Kod mikro računala za riječ se uzima skupina od 2 bajta. Kod većih računala za
                riječ se uzima skupina od 4 ili 8 bajta.
                          Tablica 1.1 Nazivi temeljnih binarnih nizova


      MSB                                             LSB                  značaj bitova
      15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0                                položaj bita
       1 0 0 0 1 1 1 1 1 0 1 0 1 1 0 1                                     binarni niz
        nibl 3        nibl 2      nibl 1       nibl 0                      niz nibla
               bajt 1                    bajt 0                            niz bajta
                             Riječ                                         riječ

                              Slika 1.1 Označavanje binarnog niza

Za označavanje većih nizova koriste se prefiksi:

      k   (kilo)    ⇔       × 1024
      M   (mega)    ⇔     k × 1024
      G   (giga)    ⇔     M × 1024
      T   (tera)    ⇔     G × 1024

Primjerice, 2 kB (kilobajta) = 2048 bajta, 3 Mb (megabita) = 3145728 bita.

     Digitalno računalo može pamtiti i izvršavati programe, te dobavljati, pamtiti i prikazivati
različite informacije. Te informacije, koje su na prikladan način pohranjene u računalu, su
programski podaci.


                               broj bita – n    broj kombinacija – 2n
                                           2                        4
                                           3                        8
                                           4                       16
                                           8                      256
                                          16                    65536
                                          32               4294967296
                              Tablica 1.2 Broj kombinacija s n bita

     Često se računala klasificiraju kao 8-bitna, 16-bitna, 32-bitna ili 64-bitna. Pod time se
podrazumijeva da n-bitno računalo može operirati s nizom od n bita kao s cjelinom. Broj bita
koji se koristi za opisivanje nekog podatka ovisi o veličini skupa kojem taj podatak pripada.




                                                                                                   6
Razmotrimo skup podataka kiji se kodira s tri bita. Taj skup može imati maksimalno 8
elemenata jer se s tri bita može kodirati maksimalno osam kombinacija: 000, 001, 010, 011,
100, 101, 110, 111. Lako je pokazati da se s n-bita može kodirati podatke iz skupa od
maksimalno 2n elemenata. Tablica 1.2 pokazuje da se udvostručenjem broja bitova značajno
povećava skup vrijednosti koje se mogu kodirati u računalu.
     Operacije se u računala nikada ne izvršavaju samo s jednim bitom, već se istovremeno
prenosi i obrađuje više bita. Kod svih računala usvojeno je da najmanja jedinica digitalne
informacije, koja se kao cjelina prenosi i pamti u računalu, sadrži 8 bita, tj. jedan bajt.
     Na slici 1.2 prikazani su sastavni dijelovi digitalnog računala. Centralna procesorska
jedinica (CPU – central processing unit) - kontrolira izvršenje programa i aritmetičko-logičkih
operacija. CPU je kod mikro i mini računala izveden kao jedinstveni integrirani elektronički
sklop (čip) i naziva se mikroprocesor. Uobičajeno je koristiti naziv procesor, bilo da se radi o
mikroprocesoru ili o skupini čipova koji obavljaju funkcije CPU, a programe koji se izvršavaju
u računalu naziva se procesima.




                           Slika 1.2. Opći prikaz digitalnog računala


     Radna memorija – pamti digitalne informacije za vrijeme dok je računalo u operativnom
stanju. U memoriji se nalazi programski kôd i podaci s kojima operira procesor na temelju
naredbi sadržanih u programskom kôdu. Memorija je napravljena od poluvodičkih elemenata u
koje procesor može upisati i iz kojih može čitati digitalne informacije. Ta memorija se naziva
RAM (eng. random access memory). Sa programerskog stajališta RAM predstavlja linearno
uređen prostor u kojem se istovremeno može pristupiti grupi od 8 bita digitalne informacije (1
bajt). Položaj ove temeljne memorijske ćelije se označava prirodnim brojem i naziva se adresa.
Jedan manji dio memorije je napravljen od poluvodičkih elemenata koji mogu trajno pamtiti
digitalnu informaciju, a naziva se ROM (eng. read-only memory). U ROM-u je upisan program
koji služi pokretanju osnovnih funkcija računala. U samom procesoru ugrađeno je nekoliko
manjih memorijskih jedinica koje se nazivaju registri. Registri služe za privremeni smještaj
programskog kôda i podataka iz radne memorije, te rezultata aritmetičko-logičkih operacije koje
se izvršavaju u samom procesoru. Broj bita koji može biti pohranjen u jednom registru naziva se
riječ procesora. Kod većine današnjih PC računala riječ procesora sadrži 32 bita (4 bajta), pa se
kaže da su to 32-bitna računala.
     Vanjska memorija - služi za trajnu pohranu podataka. U tu svrhu koriste se magnetski i
optički mediji (tvrdi disk, savitljive diskete, magnetske trake, optički diskovi,..). Podaci se na
njima pohranjuju u organiziranom i imenovanom skupu podataka koji se nazivaju datoteka.
     Ulazne jedinice - služe za unos podataka (tipkovnica, miš, svjetlosna olovka, mikrofon,..).
Standardna ulazna jedinica je tipkovnica.
     Izlazne jedinice - služe za prikaz informacija korisniku računala (video-monitor, pisač,
zvučnik,...). Standardna izlazna jedinica je video-monitor.




                                                                                                7
Računalo u operativnom stanju održava poseban program koji se naziva operativni sustav.
On vrši temeljne funkcija računala: inicijalizaciju računala i priključenih vanjskih jedinica pri
uključenju električnog napajanja, kontrolu i redoslijed izvođenja programa, kontrolu korištenja
memorije, pohranjivanje i obradu podataka, vremensku raspodjelu funkcija računala, itd.
Operativni sustav nije nužno jedinstven program, već se sastoji od više programskih cjelina. On
se, jednim dijelom, trajno nalazi u ROM memoriji računala. Programi s novakvim svojstvom
nazivaju se rezidentni programi. Svi ostali programi moraju se prije izvršenja upisati u
memoriju računala.
     Može se izvršiti funkcionalna podjela softvera na sistemski i aplikativni softver. U
sistemski softver spadaju programi operativnog sustava, razni jezični procesori (interpreteri,
kompilatori, emulatori itd.), programi za testiranje programa (debugger), servisni i uslužni
programi, te razni pomoćni programi (matematički, statistički, baze podataka i uređivači teksta).
Aplikativni softver predstavljaju različiti korisnički programi.

Kako se rješavaju problemi pomoću računala?
     Kada se rješava neki problem, do ideje za rješenje dolazi se analizom problema. Čovjeku je
često dovoljno da već iz idejnog rješenja, koristeći svoju inteligenciju i predznanje, brzo dođe
do potpunog rješenja problema. Računalo, samo po sebi, ne raspolaže s inteligencijom, već
jedino može izvršavati određen broj jednostavnih operacija. Zbog toga, upute za rješenje
problema pomoću računala moraju biti zapisane u obliku preciznog algoritma.
     Računarski algoritam je precizni opis postupka za rješenje nekog problema u konačnom
broju koraka i u konačnom vremenskom intervalu. Pravila kako se piše algoritam nisu strogo
određena. Algoritam se može definirati običnim govornim jezikom, tablicama i matematičkim
formulama koje opisuju problem, te usmjerenim grafovima koji opisuju tok izvršenja programa.

Primjer: Algoritam kojim se u pet koraka opisuje postupak zamjene točka na automobilu glasi:
      1. ispitaj ispravnost rezervnog točka,
      2. podigni auto,
      3. skini točak,
      4. postavi rezervni točak,
      5. spusti auto.

     Ovaj algoritam je jasan svakome tko je bar jednom mijenjao točak, međutim, računalo je
izvršitelj kojem upute, iskazane nizom naredbi, nisu dovoljno jasne, jer ono ne zna (1) gdje se
nalazi rezervni točak, (2) kako se provjerava njegova ispravnost, (3) kako i čime podignuti auto,
te (4) kojim alatom se skida i postavlja točak. Zbog toga se algoritam dorađuje preciziranjem
pojedinog koraka algoritma. Primjerice, u prvom koraku treba predvidjeti sljedeće naredbe:
    1. ispitaj ispravnost rezervnog točka,
               1.1. otvori prtljažnik
               1.2. izvadi rezervni točak
               1.3. uzmi mjerač tlaka iz kutije s alatom
               1.4. izmjeri razinu tlaka
               1.5. dok je razina tlaka manja 1,6 ponavljaj
                             pumpaj gumu 15 sekundi
                             izmjeri razinu tlaka


     Podrazumijeva se da je naredba označena s 1. zamijenjena s nizom naredbi koje su
označene s 1.1, 1.2,..1.5. Naredbe iskazane u koracima 1.1 do 1.4 su same po sebi jasne. Korak
1.5 treba dodatno pojasniti. Njime je opisan postupak pumpanja gume do neke razine tlaka.
Pošto nitko ne može unaprijed znati koliko vremena treba pumpati gumu, da bi se postigla
željena razina tlaka, predviđeno je da se dvije naredbe: "pumpaj gumu 15 sekundi" i "izmjeri
razinu tlaka", višekratno ponavljaju, sve dok je razina tlaka manja od 1,6. Obje ove naredbe su




                                                                                               8
zapisane uvlačenjem reda kako bi se točno znalo koje naredbe treba ponavljati. Ovaj se tip
naredbe naziva iteracija ili petlja. Uobičajeno se kaže da petlja ima zaglavlje, u kojem se
ispituje uvjet ponavljanja petlje (dok je razina tlaka manja od 1,6 ponavljaj), i tijelo petlje, koje
obuhvaća jednu ili više naredbi koje treba ponavljati. Naziv petlja podsjeća na činjenicu da se
uvijek nakon izvršenja posljednje naredbe tijela petlje proces vraća na izvršenje prve naredbe,
ali samo u slučaju ako je zadovoljen uvjet iskazan u zaglavlju petlje. Naredbe petlje nisu
posebno numerirane jer su one povezane uz zaglavlje petlje, a izvršavaju se u kao jedinstvena
složena naredba. Uobičajeno se niz naredbi koji predstavljaju jedinstvenu složenu naredbu
naziva i blok naredbi ili samo blok.
      Uvjet ponavljanja petlje je izjava: "razina tlaka manja od 1,6". Odgovor na ovu izjavu može
biti "Da" ili "Ne", ovisno o trenutno izmjerenoj razini tlaka. Ako je odgovor "Da", kažemo da je
ispunjen uvjet ponavljanja petlje. Računarska se znanost koristi znanjima matematičke logike. U
tom kontekstu ova izjava predstavlja tzv. predikatni izraz koji može imati samo dvije logičke
vrijednosti: "istina" ili "laž", pa se kaže da je uvjet održanja petlje ispunjen ako je predikatni
izraz istinit. Matematička logika je zapravo znanstveni temelj cijele računarske znanosti i o njoj
će biti više govora u sljedećem poglavlju.
      Pokušajte dalje sami precizirati korake 2, 3 , 4 i 5. Ali pazite, kad pomislite da je problem
ispravno riješen, moguće je da se opet potkrade neka greška. To se obično događa kada se ne
predvide sve moguće situacije, odnosno stanja u koja se može doći. Primjerice, gornji algoritam
nije predvidio slučaj da je guma probušena. Kakav bi razvoj događaja tada bio, ako bi se
dosljedno poštovao postupak iz koraka 1.5? Pošto je kod probušene gume razina tlaka uvijek
manja od 1,6, ispada da bi tada izvršitelj naredbi ponavljao postupak pumpanja gume
beskonačan broj puta.
Algoritam se može popraviti tako da korak 1.5 glasi:
1.5.   ako je tlak manji od 0.1 tada
                     ako je guma probušena onda
                            odnesi točak na popravak
                     inače dok je tlak manji od 1.6 ponavljaj
                            pumpaj gumu 15 sekundi
                            izmjeri razinu tlaka

U ovom se zapisu koriste tzv. naredbe selekcije, prema sljedećoj logici izvršenja:
              ako je ispunjen uvjet tada
                      izvrši prvi niz naredbi
              inače
                      izvrši alternativni niz naredbi

Ovaj se tip naredbe zove uvjetna selekcija ili grananje, jer se nakon ispitivanja logičkog uvjeta
vrši selekcija jednog od dva moguća niza naredbi, odnosno program se grana u dva smjera.

Specijalni oblik selekcije je uvjetna naredba tipa:
              ako je ispunjen uvjet tada
                      izvrši naredbu

Njome se određuje izvršenje neke naredbe samo ako je ispunjen neki uvjet.

Koristeći naredbe selekcije, algoritam se može zapisati u obliku:
       1. ispitaj ispravnost rezervnog točka,
               1.1 otvori prtljažnik
                       1.1.1. uzmi najmanji od tri ključa
                       1.1.2. gurni ključ u bravu i lagano ga okreni na desno
                       1.1.3. podigni vrata prtljažnika




                                                                                                   9
1.2. izvadi rezervni točak
                     1.2.1. podigni tapetu
                     1.2.2.ako je točak pričvršćen vijkom onda odvij vijak
                     1.2 .2. izvadi točak
             1.3. uzmi kutiju s alatom
             1.4. ispitaj razinu tlaka
                     1.4.1. izvadi mjerač tlaka iz kutije alata
                     1.4.2. postavi ga na zračnicu točka
                     1.4.3. očitaj razinu tlaka

             1.5. ako je tlak manji od 0,1 onda
                    1.5.1. provjeri da li je guma probušena
                    1.5.2. ako je guma probušena onda
                                    odnesi točak na popravak
                            inače, ako je tlak manji od 1,6 onda
                    1.5.3. otvori prednji poklopac motora
                    1.5.4. uzmi zračnu pumpu
                    1.5.5. dok je tlak < 1,6 ponavljaj
                                    postavi crijevo pumpe na zračnicu
                                    dvadeset puta pritisni pumpu
                                    na zračnicu postavi mjerač tlaka
                                    ispitaj razinu tlaka

Očito da je potrebno dosta raditi i dosta razmišljati da bi se napisao kvalitetan algoritam.
    Nakon što je napisan precizan algoritam rješenja problema, pristupa se pisanju izvornog
programa. Kako se to radi bit će objašnjeno u sljedećim poglavljima. Važno je uočiti da su u
zapisu algoritma korištena četiri tipa iskaza:
    1. proste ili primitivne naredbe – iskazi koji označavaju jednu operaciju
    2. blok naredbi – iskazi koji opisuju niz naredbi koje se sekvencijalno izvršavaju jedna za
       drugom, a tretiramo ih kao jedinstvenu složenu operaciju.
    3. naredbe selekcije – iskazi kojima se logički uvjetuje izvršenje bloka naredbi.
    4. iterativne naredbe ili petlje – iskazi kojima se logički kontrolira ponovljeno izvršenje
       bloka naredbi.
Računarski procesi i memorijski objekti
Svaki proces rezultira promjenom stanja ili atributa objekata na koje procesi djeluju. Uobičajeno
se stanje nekog promjenljivog objekta označava kao varijabla koja ima neko ime. U računalu se
stanje objekta pamti u memoriji računala pa se algoritamske varijable mora tretirati kao
memorijske objekte.
Kada se u C jeziku napiše iskaz

      x = 5;

on predstavlja naredbu da se memorijskom objektu, imena x, pridijeli vrijednost 5. Ako se pak
napiše iskaz:

      x = 2*x +5;

on predstavlja proces u kojem se najprije iz memorije očitava vrijednost memorijskog objekta x
zapisanog na desnoj strani znaka =. Zatim se ta vrijednost množi s 2 i pribraja joj se numerička
vrijednost konstante 5. Time je dobivena numerička vrijednost izraza s desne strane znaka =. Ta
se vrijednost zatim pridjeljuje memorijskom objektu s lijeve strane znaka =. Konačni je rezultat
ovog procesa da je varijabli x pridijeljena vrijednost 15. Ako bi prethodni iskaz tretirali kao
matematički iskaz, on bi predstavljao jednadžbu s jednom varijablom, koja uvjetuje da je
vrijednost varijable x jednaka 5.




                                                                                               10
Znak = u C jeziku ne predstavlja znak jednakosti, kao u matematici već operator pridjele
vrijednosti. Njegova upotreba označava naredbu da se vrijednost memorijskog objekta s lijeve
strane znaka = postavi na vrijednost izraza koji je zapisan s desne strane znaka =. Takove
naredbe se zovu naredbe pridjele vrijednosti. Zbog ove nekonzistentnosti upotrebe znaka = u
matematici u odnosu na upotrebu u nekim programskim jezicima (C, Basic, Fortan, Java) često
se u općim algoritamskim zapisima operator pridjele vrijednosti zapisuje znakom ←,
primjerice:
      x ← 5
      x ← 2*x +5

     Operacija pridjele vrijednosti posljedica je načina kako procesor obrađuje podatke u
računalu. Naime, procesor može vršiti operacije samo nad podacima koji se nalaze u registrima
procesora, pa je prije svake operacije s memorijskim objektima prethodno potrebno njihov
sadržaj (vrijednost) prenijeti u registre procesora, a nakon obavljene operacije se sadržaj iz
registra, koji sadrži rezultat operacije, prebacuje u memorijski objekt označen s lijeve strane
operatora pridjele vrijednosti. Kaže se da procesor funkcionira po principu: dobavi-izvrši-spremi
(eng. fetch-execute-store).

Što je to apstrakcija?
     Netko može primijetiti da je opisani proces zamjene točka loš primjer primjene računala.
To je točno, jer ako bi se napravio robot, koji bi obavljao navedenu funkciju, onda bi to bila vrlo
neefikasna i skupa upotreba računala. Međutim, malo iskusniji programer bi prema gornjem
algoritmu mogao lako napraviti program kojim se animirano simulira proces zamjene točka. To
je moguće jer, iako je prethodni algoritam apstraktan, on specificira procese u obliku koji se
može ostvariti računarskim programom.
     Apstrakcija je temeljna mentalna aktivnost programiranja. U računarskoj se terminologiji
pod pojmom apstrakcije podrazumijeva prikladan način zapisa o objektima i procesima koje se
obrađuje pomoću računala, a da se pri tome ne vodi računa o tome kako je izvršena stvarna
računarska implementacija, niti objekta niti procesa. Važna je samo ona pojavnost koja je
određena apstrakcijom. Algoritam zapisan programskim jezikom predstavlja apstrakciju
strojnog koda, a algoritam zapisan prirodnim jezikom predstavlja apstrakciju programskog
jezika.
     Programski jezik služi da se formalnim jezikom zapiše procese i stanje memorijskih
objekata u računalu, pa on predstavlja apstrakciju računarskih procesa i stanja memorije.
Pomoću programskih jezika se piše program koji ponovo predstavlja neku novu apstrakciju, a u
toku izvršenja programa moguće je daljnje usložnjavanje apstrakcije. Primjerice, korisnik CAD
programa pokretima miša zadaje program za crtanje nekog geometrijskog oblika.
     S obzirom na način kako je izvršena apstrakcija računarskog procesa, može se izvršiti
sljedeća klasifikacija programskih jezika:

      1. Imperativni (proceduralni) programski jezici (C, Pascal, Modula-2, Basic, Fortran,..)
      2. Objektno orijentirani programski jezici (C++, Java, C#, Eiffel, Objective C, Smaltalk,
         Modula-3, ..)
      3. Funkcionalni programski jezici (Lisp, Sheme, ML, Haskel..)
      4. Logički programski jezici (Prolog)
      5. Jezici specijalne namjene: pretraživanje baza podataka (SQL), vizuelno programiranje
         (Delphi, Visual Basic), uređivanje teksta (Perl, TeX, HTML), matematički proračuni
         (Matlab).

    Imperativni programski jezici koriste iskaze koji su bliski naredbama procesora (to su
naredbe pridjele vrijednosti, aritmetičko-logičke operacije, uvjetni i bezuvjetni skokovi te poziv




                                                                                                11
potprograma). Kod objektno orijentiranih jezika naglasak je na tome da varijable predstavljaju
atribute nekog objekta, a funkcije predstavljaju metode pomoću kojih objekt komunicira s
drugim objektima. Specifikacije atributa i metoda određuju klase objekata. Kod funkcionalnih
se jezika ne koristi        temeljna imperativna naredba pridjele vrijednosti, već se sva
međudjelovanja u programu opisuju funkcijama. Teorijska podloga ovih jezika je u tzv. λ-
računu. Kod logičkih programskih jezika međudjelovanja se u programu opisuju predikatnim
logičkim izrazima i funkcijama. Naglasak je na zapisu onoga “što program treba izvršiti”, za
razliku od imperativnih jezika pomoću kojih se zapisuje “kako nešto izvršiti”.
     Apstrakcija je dakle, temeljna mentalna aktivnost programera. Ona je moguća samo ako se
dobro poznaje programski jezik i programske algoritme za efikasno korištenje računarskih
resursa.
O tome će biti riječi u sljedećim poglavljima.




                                                                                           12
2 Matematički i elektronički temelji
računarstva


Naglasci:
   • Izjavna logika
   • Logičke funkcije i predikati
   • Booleova logika
   • Temeljni digitalni sklopovi
   • Brojevni sustavi


2.1 Izjavna i digitalna logika
     Bit će navedeni osnovni pojmovi potrebni za razumijevanje izjavne logike (ili propozicijske
logike), koji se intenzivno koristi u programiranju, i digitalne logike koja je temelj izgradnje
digitalnog računala.
     Osnovni objekt kojeg proučava izjavna logika je elementarna izjava. Ona može imati samo
jedno svojstvo - njome se izriče

     "laž" ili "istina".

      Primjerice, izjava "osam je veće od sedam" je istinita, a izjava "broj sto je djeljiv sa
sedam" je laž. Pri označavanju izjava koristit će se slovo T (true) za istinitu izjavu i F (false) za
lažnu izjavu.
     Rečenica "broj x je veći od broja y" ne predstavlja izjavu jer njena istinitost ovisi o veličini
brojeva x i y. Ako se umjesto x i y uvrste brojevi dobije se izjava. Ovakve rečenice se nazivaju
izjavne funkcije, a za x i y se kaže da su (predmetne) varijable. Odnos među varijablama, kojeg
izjavna funkcija izriče, naziva se predikat. Označi li se u prethodnom primjeru predikat " ... je
veći od.... " sa P, navedena izjavna funkcija se može zapisati u obliku P(x,y).
     Izjavne funkcije se prevode u izjave kada se uvrsti vrijednost predmetnih varijabli ili ako se
uz izjavne funkcije primijene neodređene zamjenice svaki (oznaka ∀ koja se naziva univerzalni
kvantifikator) ili neki (oznaka ∃ koja se naziva egzistencijalni kvantifikator). ∃x se čita i "postoji
x". Primjerice, prethodna izjavna funkcija primjenom kvantifikatora u predikatnom izrazu (∀
y)(∃x)P(x,y) postaje izjava koja znači: "za svaki broj y postoji broj x takav da je x veći od y".
     Rezultat izjavne funkcije je logička vrijednost T ili F. Varijable koje sadrže logičku
vrijednost nazivaju se logičke varijable.
     U programiranju se često koriste izjavne funkcije iskazane tzv. relacijskim izrazima
primjerice
      a ← (x<z)

označava da se logičkoj varijabli a pridijeli logička vrijednost određena izjavnom funkcijom
(x<z). Kada je x manje od z logička varijabla poprima logičku vrijednost T inače je F.
Standardno se koriste relacijski operatori: < (veće), > (manje), ≠ (različito ili nije jednako), ≥
(veće ili jednako), ≤ (manje ili jednako).




                                                                                                   13
Složene logičke izjave nastaju korištenjem sljedećih logičkih operacija:

Konjunkcija, a & b, (ili a ∧ b) dviju izjava a i b je je složena izjava, nastala povezivanjem
               izjava a i b veznikom i za kojeg se upotrebljava simbol ∧ ili &. Složena izjava
               je istinita samo ako su obje izjave istinite. Izjava a & b čita se "a i b".
Disjunkcija, a ∨ b, je složena izjava, koja je lažna onda i samo onda kada su obje izjave
               lažne; a ∨ b čita se "a ili b".
Implikacija, a ⇒ b, je složena izjava koja je lažna onda i samo onda ako je a istinito i b
               lažno; čita se " a povlači b" ili " a implicira b". Za izjavu b ⇒ a kaže se da je
               obrat izjave a ⇒ b. Vrijedi i sljedeće tumačenje implikacije: ako je izjava a ⇒
               b istinita onda je a dovoljan uvjet za b, ili b je nuždan uvjet za a.
Ekvivalencija, a ⇔ b, je složena izjava koja je istinita onda i samo onda kada su obje izjave
               istinite, ili kada su obje lažne: čita se " a je ekvivalentno sa b".
Negacija,      ¬a, je izjava koja je istinita onda i samo onda kada je izjava a lažna.

     Simboli: ¬, &, ∨, ⇔ i ⇒ su logički operatori. Njihovo djelovanje na logičke varijable a i b
je prikazano tzv. tablicom istinitosti (tablica 2.1).

                 A           b          ¬a         a & b       a ∨ b       a ⇒ b      a ⇔ b
                 T           T           F           T            T           T           T
                 T           F           F           F            T           F           F
                 F           T           T           F            T           T           F
                 F           F           T           F            F           T           T

                        Tablica 2.1. Tablica istinitosti logičkih operacija

     Upotrebom logičkih operatora i uvođenjem zagrada mogu se, kao i u algebri, graditi razni
logički izrazi, primjerice

      ¬a ∨ (b & d) ⇒ c.

Redoslijed izvršavanja operacija je sljedeći: (1) izraz u zagradi, (2) negacija, (3) disjunkcija, (4)
konjunkcija, (5) implikacija i ekvivalencija. Logički izrazi koji sadrže samo operacije negacije,
konjunkcije i disjunkcije, te zagrade, određuju Booleovu algebru. Svi se logički izrazi mogu
iskazati Booleovom algebrom jer se djelovanje operatora implikacije i ekvivalencije može
izraziti pomoću Booleovih izraza. Vrijedi:

      x ⇒ y      = ¬x ∨ y
      x ⇔ y      = ¬((¬x & y) ∨ (¬y & x))

Zadatak: Provjerite prethodne izraze tablicom istinitosti.

U Booleovoj algebri vrijede slijedeće zakonitosti:

1. Zakon komutacije

      x ∨ y ≡ y ∨ x
      x & y ≡ y & x

2. Zakon asocijacije




                                                                                                  14
x ∨ (y ∨ z) ≡ (x ∨ y) ∨ z
      x & (y & z) ≡ (x & y) & z

3. Zakon idempotentnosti

      x ∨ x ≡ x
      x & x ≡ x

4. Zakon distribucije

      x ∨ (y & z) ≡ (x ∨ y) & (x ∨ z)
      x & (y ∨ z) ≡ (x & y) ∨ (x & z)

5. De Morganov teorem

      ¬(x ∨ y) ≡ ¬x & ¬y
      ¬(x & y) ≡ ¬x ∨ ¬z

6. Zakon dvostruke negacije

      ¬¬x ≡ x

     Booleova logika ima veliku primjenu u programiranju i posebno pri projektiranju sklopova
digitalnog računala, jer se gotovo svi potrebni sklopovi digitalnog računala mogu realizirati
pomoću tri temeljna elektronička sklopa: invertor, sklop-I (eng. AND gate) i sklop-ILI (eng.
OR gate).




                              Slika 2.1. Temeljni digitalni sklopovi

     Ovi se sklopovi upravljaju naponom (ili strujom) tako da reagiraju na stanje pod naponom
i stanje bez napona, dakle oni raspoznaju samo dvije naponske razine: nisku i visoku.
Uobičajeno se ta dva stanja označavaju s "1" i "0" umjesto s true i false. To su sklopovi kojima
izlaz odgovara operacijama negacije, disjunkcije i konjunkcije ulaznih logičkih stanja "0" i "1".
Funkcija ovih sklopova se može prikazati pomoću preklopki. Primjerice, rad sklopa I se može
opisati strujnim krugom u kojem su serijski spojene žarulja, sklopka A i sklopka B. Žarulja će
zasvijetliti kada proteče struja, a to je moguće samo ako ako su obje sklopke uključene, odnosno
izlaz je 1 samo ako su varijable A i B jednake 1. Kod sklopa ILI dovoljno je uključiti jednu
sklopku da bi zasvijetlila žarulja. Očito sklop I obavlja logičku funkciju konjunkcije, a sklop ILI
obavlja logičku funkciju disjunkcije. U digitalnom se računalu pomoću navedenih sklopova
obrađuje i prenosi mnoštvo digitalnih signala.
     Pošto je uvedeno označavanje stanja digitalnog signala znamenkama 0 i 1, može se reći da
se digitalnim signalom prenosi poruka o vrijednosti binarne znamenke koja u jednom trenutku




                                                                                                15
može imati iznos nula ili jedinica. Iz tog se razloga umjesto pojma Booleova algebra ili
matematička logika često koristi pojam digitalna logika.
     U digitalnoj je tehnici uobičajena primjena logičkih operatora na nizove bitova. Tada se
podrazumijeva da se logičke operacije provode nad bitovima jednake značajnosti. Takve
logičke operacije se nazivaju bit-značajne operacije.
Primjer: bit značajnom konjunkcijom dva binarna niza A i B dobije se niz C:

              7 6 5 4 3 2 1 0    bit
              -----------------------
              1 1 0 0 1 1 1 1 = A
              0 0 0 0 0 1 0 0 = B
                ----------------------
      A & B = 0 0 0 0 0 1 0 0 = C

U nizu C jedino bit 2 može biti jednak 1 i to samo ako je i u nizu A taj bit jednak 1. Ovo je
često korišten postupak da se ispita da li je neki bit u nizu jednak 1 ili 0. Obično se niz B naziva
"maska" za ispitivanje bitova u nizu A.
    Pored prije navedenih Booleovih logičkih operacija u digitalnoj se tehnici često koristi bit-
značajna operacija koja se naziva ekskluzivna disjunkcija ili ekskluzivno ILI. Označava se
znakom ⊕ ili XOR. Ima značaj zbrajanja po modulu 2, a njeno korištenje u programiranju bit će
pojašnjeno kasnije.

      A XOR B = A ⊕ B = (¬A & B) ∨ (A & ¬B)




             A        B      A ⊕ B
             0        0        0
             0        1        1               A ⊕ B   = (¬A & B) ∨ (A & ¬B)
             1        0        1
             1        1        0


      Slika 2.2 Definicijska tablica ekskluzivne disjunkcije i simbol digitalnog XOR-sklopa


2.2 Brojevni sustavi i računska sposobnost računala
      U programskim jezicima operacije s brojevima se najčešće zapisuju u decimalnom
brojevnom sustavu, jer je čovjek naviknut na rad s decimalnim brojevima. U računalu se pak
računske operacije vrše u binarnom brojevnom sustavu.

2.2.1 Binarni brojevni sustav
Sasvim općenito, numerička vrijednost broja Z, koji je u pozicionoj notaciji zapisan
znamenkama: zn-1....z1z0, u brojevnom sustavu baze x, računa se prema izrazu:
                                        n −1
             Z = ( zn −1 .... z1z0 ) x = ∑ zi ⋅ x i
                                        i =0
Decimalni brojevni sustav je definiran bazom x=10 i znamenkama zi ε{0,1,2,3,4,5,6,7,8,9},
primjerice iznos broja 765 je jednak 7⋅102 + 6⋅101 + 5⋅100 .




                                                                                                 16
Binarni brojevni sustav    je definiran bazom x=2 i binarnim znamenkama zi ∈
{0,1}.Primjerice, iznos binarnog broja 1011 odgovara iznosu broja 11 u decimalnom sustavu,
jer je
             (1011)2 = 1⋅23 + 0⋅22 + 1⋅21 +1⋅20 = 8 + 0 + 2 + 1 = (11)10.
     Općenito vrijedi da se s binarnim nizom od n bita može kodirati pozitivni cijeli broj
maksimalnog iznosa 2n -1, što odgovara broju različitih kombinacija binarnog niza duljine n
umanjenom za jedan (i nula je broj!). Za pozitivne cijele brojeve koristi se i nazivi kardinalni
brojevi i nepredznačeni cijeli brojevi.
     U binarnom brojevnom sustavu se mogu izvoditi osnovne računske operacije kao i u
decimalnom brojevnom sustavu. Binarno zbrajanje se obavlja kao i decimalno zbrajanje, osim
što se prijenos na slijedeće značajnije mjesto ne obavlja nakon zbroja 10, već nakon 2 (1+1).
Primjer:

                                                        1 1      ← prijenos
           1 0 1   = 510                               1 1 1   = 710
         + 0 1 0   = 210                             + 1 0 1   = 510
         -----------------                             ----------------------
           1 1 1   = 710                             1 1 0 0   = 1210


     Ukoliko se zbrajanje izvodi bez prijenosa ta operacija se naziva zbrajanje po modulu 2. U
logičkom smislu ta operacija je ekvivalentna ekskluzivnoj disjunkciji (XOR). Operaciju
zbrajanja LSB bitova može se prikazati tablicom istinitosti 2.2:


                     A        B       zbroj = A ⊕ B          prijenos = A & B
                     0        0             0                        0
                     0        1             1                        0
                     1        0             1                        0
                     1        1             0                        1

                           Tablica 2.2. Tablica istinitosti za polu-zbrajalo

                      A           B        Donos          Zbroj        prijenos
                      0           0          0              0              0
                      0           0          1              1              0
                      0           1          0              1              0
                      0           1          1              0              1
                      1           0          0              1              0
                      1           0          1              0              1
                      1           1          0              0              1
                      1           1          1              1              1

                          Tablica 2.3. Tablica istinitosti za potpuno zbrajalo

     Digitalni sklop koji realizira ovu funciju naziva se poluzbrajalo (half-adder) i prikazan je na
slici 2.3(a). Pri zbrajanju ostalih bitove treba pribrojiti i bit donosa kao u tablici 2.3. Digitalni
sklop koji realizira ovu funkciju naziva se potpuno zbrajalo (full-adder). Prikazan je na slici
2.3(b).
     Očito je da se upotrebom više ovakvih sklopova može "izračunati" zbroj dva binarna niza,
na način da se "prijenos" s zbrajala bitova manjeg značaja prenosi kao "donos" u zbrajalo
bitova većeg značaja.




                                                                                                  17
Slika 2.3 Sklopovska izvedba 1-bitnog zbrajala

      Operacija ekskluzivne disjunkcije (XOR) se često koristi u bit-značajnim operacijama pri
šifriranju i u programima s bit-mapiranim grafičkim algoritmima. Interesantno svojstvo ove
operacije je da ako se na neki binarni niz A dva puta uzastopno primjeni bit-značajna
ekskluzivna disjunkcija s nizom B rezultatni niz je jednak nizu A. Primjerice, neka je niz A=
1010, a niz B=0110. Tada je:

       A ⊕ B = 1100
      (A ⊕ B) ⊕ B = 1010 = A

Dakle, prvo djelovanje je šifriranje, a drugo djelovanje je dešifriranje originalnog niza.
     Oduzimanje broja se može izvesti kao zbrajanje negativne vrijednosti broja. Kako se
kodiraju negativni brojevi bit će pokazano kasnije.
     Binarno množenja se vrši tako da se djelomičan umnožak pomiče za jedno mjesto ulijevo
pri svakom uzimanju idućeg množitelja. Ako je množitelj 0, djelomični umnožak je 0, a ako je
množitelj 1, djelomični umnožak jednak je množeniku. Primjer:

      5 x 5 = 25                                   5 x 10 = 50

          101   (5)                                101     (5)
          101   (5)                               1010    (10)
      ------------                             ---------------
          101                                      000
         000                                      101
        101                                      000
      ------------                              101
        11001 (25)                             ----------------
                                                110010    (50)

    Binarno dijeljenje se u računalu izvodi primjenom binarnog množenja i oduzimanja, na isti
način kao i kod decimalnih brojeva. Navedene operacije su ugrađene u skup naredbi većine
današnjih procesora.




                                                                                             18
Još dvije operacije su specifične za rad s nizovima bitova. To su operacije logičkog
posmaka bitova u lijevo ili u desno (podrazumijeva se LSB na desnoj strani niza), a označavaju
se sa SHL (eng. shift left - posmak u lijevo) i SHR (shift right - posmak u desno).
     Posmak od jednog mjesta u lijevo odgovara množnju kardinalnih brojeva s 2, a posmak
bitova jedno mjesto udesno odgovara dijeljenju kardinalnih brojeva s 2. Na prazna mjesta se
postavljaju nule.
Primjer:

      0011 SHL 1 ≡ 0110               odgovara        3 * 2 = 6
      0011 SHL 2 ≡ 1100               odgovara        3 * 4 = 12
      1110 SHR 1 ≡ 0111               odgovara       14 / 2 = 7

2.2.2 Oktalni i heksadecimalni brojevni sustavi
     U višim programskim se jezicima rijetko koristi zapis broja u binarnom obliku jer čovjek
teško pamti veće nizove "nula i jedinica". Radije se koristi oktalni ili heksadecimalni brojevni
sustav.
     U oktalnom brojevnom sustavu koristi se 8 znamenki: 01234567, a baza brojevnog sustava
je x=23=8. Oktalnim brojem jednostavno se označava niz od 3 bita, jer je s binarnim nizom od 3
bita moguće kodirati 8 znamenki oktalnog brojevnog sustava:

      bit 0   0 1 0 1 0 1 0 1
      bit 1   0 0 1 1 0 0 1 1
      bit 2   0 0 0 0 1 1 1 1
      -----------------------
              0 1 2 3 4 5 6 7             znamenke oktalnog brojevnog sustava

To omogućuje pregledniji zapis većih binarnih nizova, primjerice

      1001000101112 = 44278,

a koristi se pravilo grupiranja po 3 bita: 100=4, 100=4, 010=2, 111=7.
    U heksadecimalnom brojevnom sustavu koristi se 16 znamenki: 0123456789ABCDEF, a
baza brojevnog sustava iznosi x=16. Za kombinacije od 10 do 15 upotrebljena su prva slova
abecede, kojima numerička vrijednost u decimalnom brojevom sustavu iznosi:

      A=10, B=11, C=12, D=13, E=14 i F=15.

     Heksadecimalnim se brojem jednostavno označava niz od 4 bita, jer se binarnim nizom od
4 bita može kodirati 16 znamenki heksadecimalnog brojevnog sustava:

      bit 0   0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1
      bit 1   0 0 1 1 0 0 1 1 0 0 1 1 0 0 1 1
      bit 2   0 0 0 0 1 1 1 1 0 0 0 0 1 1 1 1
      bit 3   0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1
      ---------------------------------------
              0 1 2 3 4 5 6 7 8 9 A B C D E F                heksadecimalne znamenke

To omogućava pregledniji zapis većih binarnih nizova, primjerice:

      10010001011111102 = 917E16,

a koristi se pravilo grupiranja po 4 bita: 1001=9, 0001=1, 0111=7, 1110=E.
    U programskim jezicima se uvode posebna leksička pravila za zapis konstanti u pojedinom
brojevnom sustavu. Ta pravila će biti opisana u četvrtom poglavlju.




                                                                                             19
3 Izrada prvog C programa


Naglasci:
   • Izvorni program i izvršni program
   • Prvi program u C jeziku – hello.c
   • Kompiliranje programa koji je napisan u više datoteka
   • Integrirana programska okolina
   • Makefile

U ovom poglavlju opisani su programi koji se koriste u razvoju programa. Njihova upotreba se
demonstrira s nekoliko jednostavnih programa u C jeziku.


3.1 Strojni, asemblerski i viši programski jezici
     Procesor izvršava radnje u računalu na temelju binarno kodiranih strojnih naredbi, koje
dobavlja iz memorije računala. Skup svih strojnih naredbi procesora naziva se strojni jezik, a
skup naredbi kojima se neposredno izvršava neka zadana operacija u računalu naziva se strojni
program.
     Strojne naredbe je veoma teško pamtiti. Zbog toga se koristi simboličko zapisivanje naredbi
asemblerskim jezikom, u kojem se naredbe zapisuju s kraticama riječi iz engleskog jezika.
Primjerice, naredba da se broj, koji se nalazi u registru ax procesora Intel 8086, uveća za jedan,
glasi:

      strojni jezik                   01000000
      asemblerski jezik                 inc ax        (inc je kratica od increment)

Dakle, binarni strojni kôd je zamijenjen simboličkim zapisom koje procesor ne razumije. Za
prevođenje asemblerskog zapisa u strojni program koristi se program koji se naziva asembler.
    Viši programski jezici omogućuju jednostavnije programiranje uvođenjem simboličkih
naredbi koje zamjenjuju više naredbi strojnog jezika. Primjerice, iskaz C jezika

      x = sin(3.14) + 7;

znači naredbu da se varijabli x pridijeli vrijednost koja se dobije zbrojem vrijednosti funkcije
sin(3.14) i konstante 7.
     Zapis programa asemblerskim ili višim programskim jezikom naziva se izvorni program
(eng. source program). Za pisanje izvornog programa koriste se programi za uređivanje teksta
koji se nazivaju editori. Izvorni program u C jeziku se obično pohranjuje kao datoteka s
imenom koje završava znakovima “.c”
     Programi za prevođenje izvornog programa u izvršni program mogu biti realizirani kao
interpreteri ili kao kompilatori (eng. compiler), a razlika među njima je u načinu kako se izvorni
program prevodi u izvršni program.
     Kompilator analizira izvorni program i stvara strojni kôd, koji pohranjuje u tzv. objektnu
datoteku (eng. object file). Kod MS-DOS računala ime objektne datoteke završava s “.obj”, a
kod Unix računala s “.o”. Iako objektna datoteka sadrži strojni kôd, on još nije pogodan za
izvođenje u računala, naime za izvođenje programa potrebno je odrediti veze s operativnim




                                                                                               20
sustavom i memorijske lokacije programskih objekata. To se vrši programom koji se zove
povezivač ili linker. Nakon obrade s linkerom dobije izvršni ili izvedivi program (eng.
executive program), a ako se pohrani kao datoteka onda se ta datoteka naziva izvršna datoteka
(eng. executive file). Kod MS-DOS računala ime izvršne datoteke završava slovima “.exe”.
Postoje programi u kojima je integrirana funkcija kompilatora i linkera. Primjerice, Microsoft
Visual C sadrži program "cl.exe", a na Unix-u se koriste "cc" ili "gcc" programi.
     Učitavanje izvršne datoteke u memoriju računala vrši program koji se naziva punjač (eng.
loader). On je sastavni dio operativnog sustava računala. Korisnik pokreće izvršenje programa
tako da otkuca ime programa u komandnoj liniji ili se služi programima s grafičkim sučeljem
(primjerice, program Explorer kod Windows-a). Ostale radnje punjača obavlja operativni
sustav.
     Programi se mogu izvršavati i pomoću specijalnih programa koji služe za testiranje
izvršenja procesa. Tu spadaju programi koji se nazivaju dibageri (eng. debugger) i monitori.
Pomoću njih je moguće izvršavati program u konačno zadanom broju koraka, naredbu po
naredbu, i nakon svakog koraka moguće je pratiti stanje varijabli (memorije) i registara
procesora. Kod naprednijih dibagera moguće je pratiti izvršenje programa na razini izvornog
koda, dok se kod jednostavnijih dibagera i monitora izvršenje programa može pratiti na razini
strojnog koda.
     Interpreter prevodi izvorni kod u niz izvršnih naredbi koje se obično izvršavaju unutar
samog programa interpretera. Skup izvršnih naredbi interpretera obično se naziva "virtuelni
stroj" (primjerice Java VM ili Microsoft CLR). Program za "interpretiranje", pomoću virtuelnog
stroja, može biti i odvojeni program. U tom slučaju interpreter ima funkciju kompilatora koji
generira datoteke s nizom naredbi virtuelnog stroja.
     U razvoju programa koristi se veliki broj programa, tzv. softverskih alata, koji olakšavaju
razvoj programa i pisanja dokumentacije. Primjerice, na Unix-u se koriste: make - program za
kontrolu procesa kompiliranja, grep - program za pretraživanje datoteka, profiler - program za
analizu izvršenja programa, diff – program za utvrđivanje razlika među izvornim datotekama,
patch – program za automatsko unošenje izmjena u izvorne datoteke. Kod Windows
operativnog sustava više su u upotrebi programi koji se nazivaju integrirana razvojna okolina
(eng. IDE – integrated developement environment) kod kojih se pod jedinstvenim grafičkim
sučeljem koordinira rad svih faza razvoja programa – editiranje izvornog koda, kompiliranje,
dibagiranje i profiliranje koda. U okviru IDE-a integriran je i pristup kompletnoj dokumentaciji
kompilatora i programskih biblioteka. Bez sumnje, najpopularniji program ovog tipa je
Microsoft Visual Studio.


3.2 Prvi program u C jeziku
Gotovo sve knjige o C jeziku kao prvi primjer C-programa uzimaju program:

      /* Datoteka: hello.c */
      /* Prvi C program. */

      #include <stdio.h>

      int main()
      {
        printf("Hello world!n");
        return 0;
      }


Ovaj program vrši samo jednu radnju; na standardnom izlaznom uređaju ispisuje poruku:

      Hello World!.




                                                                                             21
Na slici 3.1 prikazan je Windows komandni prozor. On se još naziva MS-DOS prozor ili
još jednostavnije komandna konzola. Karakteristika ispisa u konzoli je da se može jedino vršiti
ispis teksta, i to u po principu da se ispisuje redak po redak, od vrha konzole na dolje. U konzoli
se mogu zadavati komande operativnom sustavu u retku koji uobičajeno započinje s oznakom
tekućeg direktorija. Taj redak se naziva komandna linija operativnog sustava.
     Najprije će biti pokazano kako se može editirati, kompilirati i izvršiti ovaj program,
koristeći komandnu liniju operativnog sustava u MSDOS-prozoru (sl. 3.1). To se vrši u tri
koraka:
    1. Pomoću editora stvara se tekstualna datoteka, imena "hello1.c", koja sadrži prikazani
       izvorni kôd programa.
    2. Pomoću kompilatora se izvorni kôd programa prevodi u objektni, a zatim i u izvršni
       kôd. Ako kompilator dojavi da u izvornom kôdu postoje leksičke ili sintaktičke
       pogreške, ponavlja se korak 1 i ispravljaju pogreške.
    3. Izvršenje programa se zadaje u komandnoj liniji.

Primjer za OS Windows:
c:> edit hello.c ↵               - poziv editora edit (↵ je tipka enter)
                                   i unos izvornog programa u datoteku hello.c
c:> cl hello.c ↵                 - poziv kompilatora (Microsoft-program cl.exe)
                                   koji stvara izvršnu datoteku hello.exe
c:> hello ↵                      - komanda za izvršenje programa hello.exe
Hello world!                     - rezultat izvršenja programa




                Slika 3.1 Izgled komandnog prozora u Windows operativnom sustavu

Primjer za OS Linux:
$ vi hello.c ↵                          - poziv editora vi
                                          i unos datoteke hello.c
$ gcc hello.c –o hello ↵                - poziv kompilatora gcc,
                                          koji stvara izvršnu datoteku hello
$ hello ↵                               - komanda za izvršenje programa hello
Hello world!                            - rezultat izvršenja programa




                                                                                                22
Analiza programa "hello1.c":
     C programi se sastoje od niza potprograma koji se zovu funkcije C-jezika. U programu
"hello1.c" definirana je samo jedna funkcija, nazvana main(). Ona mora biti definirana u
svakom C programu, jer predstavlja mjesto početka izvršenja programa. Programer može
definirati nove funkcije, svaku s jedinstvenim imenom, a mogu se koristiti i prethodno
definirane funkcije iz standardne biblioteke funkcija C jezika.
     Radnje koje obavlja neka funkcija zapisuju se unutar tijela funkcije. Tijelo funkcije je
omeđeno vitičastim zagradama. U ovom je slučaju u tijelu funkcije je iskaz koji predstavlja
naredbu da se pomoću standardne C funkcije printf(), na standardnoj izlaznoj jedinici,
ispiše poruka "Hello World!".

    Pojedini dijelovi programa "hello1.c" imaju sljedeći značaj:

    /* Prvi C program. */                 Tekst omeđen znakovima /* i */ predstavlja
                                          komentar. Kompilator ne analizira komentare,
                                          već ih tretira kao umetnuto "prazno" mjesto.
    #include <stdio.h>                    #include         predstavlja      pretprocesorsku
                                          direktivu. Ona označava da u proces
                                          kompiliranja treba uključiti sadržaj datoteke
                                          imena "stdio.h". Ta datoteka sadrži deklaracije
                                          funkcija iz standardne biblioteke C-jezika.
    int main()                            Ovo je zaglavlje funkcije imena main.
                                          int označava tip vrijednosti (cijeli broj) koji
                                          vraća funkcija na kraju svog izvršenja (u ovom
                                          programu to nema nikakvi značaj).
    {                                     { označava početak tijela funkcije main.
    printf("Hello world!n");             Ovo je naredba za poziv standardne funkcije
                                          printf(), kojom se ispisuje niz znakova
                                          (string) koji je argument ove funkcije.
                                          n predstavlja oznaku za prijelaz u novi red
                                          ispisa.
                                          Znak točka-zarez označava kraj naredbe.
    Return 0;                             main() "vraća" vrijednost 0, što se uobičajeno
                                          koristi kao oznaka      uspješnog završetka
                                          programa.
    }                                     } označava kraja tijela funkcije main.


     U objašnjenju programskih iskaza korišteni su neki novi pojmovi (deklaracija, standardna
biblioteka, pretprocesorska direktiva). Oni će biti objašnjeni u sljedećim poglavljima.

     Ako program nije napisan u skladu s pravilima jezika, tada kažemo da je program
sintaktički pogrešan. Primjerice, ukoliko u prethodnom programu nije otkucana točka-zarez iza
naredbe printf("Hello world!n"), kao u programu "hello2.c",




                                                                                              23
/* Datoteka: hello2.c */
      /* Hello s greškom   */

      #include <stdio. h>

      int main()
      {
        printf("Hello world!n")              /* greška: nema ; */
        return 0;
      }


tada kompilator, u ovom slučaju program cl.exe, ispisuje poruku da postoji sintaktička pogreška
u sljedećem obliku:


      C:>cl hello.c

Microsoft (R) 32-bit C/C Optimizing Compiler Ver.12.00.8168 for 80x86
Copyright (C) Microsoft Corp 1984-1998. All rights reserved.

hello.c
hello.c(5) : error C2143: syntax error : missing ';' before 'return'

Poruka o greški ima sljedeće elemente:

      hello.c(5) – obavijest da je greška u retku 5 datoteke "hello.c",
      error C2143: syntax error - kôd i tip greške,
      missing ';' before 'return' – kratki opis mogućeg uzroka greške.

Na temelju dojave greške često je lako izvršiti potrebne ispravke u programu.
     Važno je uočiti da je kompilator pronašao grešku u petom retku, iako je pogrešno napisana
naredba u četvrtom retku. Razlog tome je pravilo C jezika po kojem se naredba može pisati u
više redaka, a stvarni kraj naredbe predstavlja znak točka-zarez. Pošto kompilator nije pronašao
točku-zarez u četvrtom retku, kompiliranje je nastavljeno s petim retkom i tek tada je utvrđeno
da postoji pogreška.
Zadatak: Provjerite da li je sintaktički ispravno napisan sljedeći program:

      /* Datoteka: hello3.c
       * Zapis naredbe u više redaka
       */
      #include <stdio. h>

      int
      main()
      {
        printf
        (
        "Hello world!n"
        );
        return
        0;
      }




                                                                                             24
3.3 Struktura i kompiliranje C programa
     Na sličan način, kao u funkciji main(), može se neka druga grupa naredbi definirati kao
posebna funkcija s prikladnim imenom. Primjerice, prethodni program se može napisati pomoću
dvije funkcije Hello() i main() na sljedeći način:

      /* Datoteka: hello4.c
       * Program s korisnički definiranom funkcijom Hello()
       */

      #include <stdio.h>

      void Hello()
      {
         printf("Hello worldn");
      }

      int main()
      {
         Hello();
         return 0;
      }



    Za razumijevanje ovog programa potrebno je upoznati pravila za definiranje i pozivanje
funkcija.

    Funkcija se definira zaglavljem i tijelom funkcije.

     Zaglavlje funkcije se zapisuje na sljedeći način: ime funkcije se zapisuje nizom znakova
koji sadrži slova, znamenke i znak '_', ali uz uvjet da je prvi znak niza slovo ili '_'. Ispred imena
funkcije se navodi vrijednost koju funkcija vraća. Ako funkcija ne vraća nikakovu vrijednost
tada se ispred imena piše riječ void, koja znači “ništa ili nevažno je”. Ovakve funkcije se
nazivaju procedure. U njima se ne mora koristiti naredba return, već one završavaju kada se
izvrši posljednje definirana naredba. Iza imena funkcije se, u zagradama, navode formalni
argumenti funkcije, ako ih ima. Kasnije će biti objašnjeno kako se definiraju i koriste argumenti
funkcije.

    Tijelo funkcije se zapisuje unutar vitičastih zagrada, a sadrži niz naredbi i deklaracija.

     Poziv funkcije je naredba za izvršenje funkcije, (tj. za izvršenje naredbi koje su definirane
unutar funkcije). Zapisuje se na način da se prvo napiše ime funkcije, a zatim obvezno zagrade i
argumenti funkcije, ako su prethodno definirani. Primjerice, u funkciji main() iskaz
Hello(); predstavlja poziv funkcije Hello(). Poziv funkcije pokreće izvršenje naredbi koje
su definirane u tijelu funkcije Hello(), tj. poziva se funkcija printf() s argumentom
"Hello Worldn". Nakon izvršenja te naredbe program se vraća, u funkciju main() na
izvršenje prve naredbe koja je napisana iza poziva funkcije Hello(). Funkcija iz koje se
pokreće izvršenje pozvane funkcije naziva se pozivna funkcija.
     U prethodnom primjeru funkcija Hello() je definirana prije funkcije main(). Taj
redoslijed je određen pravilom da se funkcija može pozivati samo ako je prethodno definirana.
Iznimka od ovog pravila je ako se koriste tzv. prototipovi funkcija (ili unaprijedne deklaracije
funkcija).




                                                                                                  25
Prototip ili deklaracija funkcije je zapis u koji sadrži zaglavlje funkcije i znak točka-zarez.
On služi kao najava da je funkcija definirana negdje drugdje; u standardnoj biblioteci ili u
programu iza mjesta njenog poziva ili u drugoj datoteci. U skladu s ovim pravilom dozvoljeno
je prethodni program pisati u obliku:

      /* Datoteka: hello5.c:
       * C program s korisnički definiranom funkcijom Hello()
       * i prototipom funkcije Hello()
       */
      #include <stdio.h>

      void Hello();        /* prototip ili deklaracija funkcije Hello()*/

      int main()                           /* definicija funkcije main()                 */
      {
         Hello();
         return 0;
      }

      void Hello()                /* definicija funkcije Hello()                           */
      {
          printf("Hello worldn");
      }


U C jeziku se programi mogu zapisati u više odvojenih datoteka. Primjerice, prethodni program
se može zapisati u dvije datoteke "hellomain.c" i "hellosub.c". U datoteci "hellomain.c"
definirana je funkcija main() i deklarirana je funkcija Hello(). Definicija funkcije Hello()
zapisana je u datoteci "hellosub.c".


      /* Datoteka: hellomain.c */                  /* Datoteka: hellosub.c */

      void Hello();                                #include <stdio.h>

      int main()                                     void Hello()
      {                                              {
         Hello();                                       printf("Hello worldn");
         return 0;                                   }
      }


Izvršni program se može dobiti komandom:

      c:> cl     hellomain.c hellosub.c /Fe"hello.exe"

U komandnoj liniji su zapisana imena datoteka koje treba kompilirati. Zatim je komandnom
preklopkom /Fe zapisano da izvršnu datoteku treba formirati pod imenom "hello.exe".




                                                                                                 26
Slika 3.2 Proces formiranja izvršnog programa

     Proces formiranja izvršnog programa je prikazan na slici 3.2. Najprije leksički pretprocesor
kompilatora unosi sve deklaracije iz datoteke "stdio.h" u datoteku "hellosub.c". Zatim
kompilator prevodi izvorne datoteke "hellomain.c" i "hellosub.c" u objektne datoteke
"hellomain.obj" i "hellosub.obj". U ovim datotekama se uz prijevod izvornog koda u strojni
jezik nalaze i podaci o tome kako izvršiti poziv funkcija koje su definirane u drugoj datoteci.
Povezivanje strojnog kôda, iz obje datoteke, u zajednički izvršni program obavlja program
link.exe, kojeg skriveno poziva program cl.exe.
     Dobra strana odvajanja programa u više datoteka je da se ne mora uvijek kompilirati sve
datoteke, već samo one koje su mijenjane. To se može ostvariti na sljedeći način:
Prvo se pomoću preklopke /c kompilatorskom pogonskom programu cl.exe zadaje da izvrši
prijevod u objektne datoteke, tj.

      c:> cl /c hellomain.c
      c:> cl /c hellosub.c

Time se dobiju dvije objektne datoteke: "hellomain.obj" i "hellosub.obj". Povezivanje ovih
datoteka u izvršnu datoteku vrši se komandom:

      c:> cl hellomain.obj hellosub.obj /Fe"hello.exe"

Ako se kasnije promijeni izvorni kôd u datoteci "hellomain.c", proces kompiliranja se može
ubrzati komandnom:

      c:> cl hellomain.c hellosub.obj /Fe"hello.exe"

jer se na ovaj način prevodi u strojni kôd samo datoteka "hellomain.c", a u procesu formiranja
izvršne datoteke koristi se prethodno formirana objektna datoteka "hellosub.obj".


3.4 Integrirana razvojna okolina (IDE)
     Integrirana razvojna okolina Visual Studio omogućuje editiranje izvornog koda,
kompiliranje, linkanje, izvršenje i dibagiranje programa. Sadrži sustav "on-line" dokumentacije
o programskom jeziku, standardnim bibliotekama i programskom sučelju prema operativnom
sustavu (Win32 API). Pomoću njega se mogu izrađivati programi s grafičkim korisničkim
sučeljem i programi za konzolni rad.
Nakon poziva programa dobije se IDE Visual Studio prikazan na slici 3.3.




                                                                                              27
Slika 3.3 Izgled Visual Studio pri pokretanju programa


    Najprije će biti opisano kako se formira projekt za konzolni tip programa. Prije pokretanja
programa, neka se u direktoriju c:My DocumentsC2002pog2 nalaze izvorne datoteke
hellomain.c i hellosub.c.
    Pokretanjem komande menija: File-New-Project, dobije dijalog za postavljanje novog
projekta. U dijalogu prikazanom na slici 3.4 označeno je
    1. da će projekt biti klase: Win32 Console Application,
    2. upisano je ime direktorija d:src
    3. gdje će biti zapisana dokumentacija projekta imena hello.
     U ovom slučaju Visual Studio zapisuje dokumentaciju projekta u datotekama hello.vsproj i
hello.sln (datoteka ekstenzije .sln sadrži opis radne okoline, a datoteka ekstenzije .vcproj sadrži
opis projekta). Visual Studio također automatski formira dva poddirektorija: .Release i
.Debug, u kojima će se nalaziti objektne i izvršne datoteke. (Debug direktorij je predviđen za
rad kompilatora u obliku koji je prikladan za pronalaženje grešaka u programu)
     Pokretanjem komande: Project - Add to project – File, u dijalogu prikazanom na slici 3.9
odabiremo datoteke "hellomain.c" i "hellosub.c", iz direktorija c:My Documentscpp-
2001pog3. Dobije se izgled radne okoline kao na slici 3.10.

     Pokretanjem komande: Build – Build hello.exe vrši se proces kompiliranja i linkanja. Ako
se izvrši bez greške, nastaje izvršni program imena hello.exe. Program hello.exe se može
pokrenuti pozivom komande Build – Execute hello.exe (ili pritiskom tipki Ctrl+F5).




                                                                                                28
Slika 3.4 Dijalog za postavljanje novog projekta




Slika 3.5 Dijalog za postavljanje tipa Win32-Console projekta




                                                                29
Slika 3.6 Dijalog koji izvještava o postavkama novog projekta




Slika 3.7 Izgled radne okoline nakon formiranja novog projekta “hello”




                                                                         30
Slika 3.9 Dijalog za umetanja datoteka u projekt




  Slika 3.10 Izgled IDE s aktivnim editorom




                                                   31
3.5 Usmjeravanje procesa kompiliranja programom nmake
    Kada se program sastoji od velikog broja datoteka, procesom kompiliranja se može
upravljati pomoću program imena nmake (make na UNIX-u) i specifikacije koja se zapisuje u
datoteci koji se obično naziva makefile. Za prethodni primjer datoteka makefile može biti
napisana na sljedeći način:

       # datoteka: makefile
       #Simbolička definicija za spisak objektnih datoteka
       OBJS = hellomain.obj hellosub.obj

       #progam se formira povezivanjem objektnih datoteka:
       hello.exe : $(OBJS)
            cl $(OBJS) /Fe"hello.exe"

       # $(...) znači umetanje prethodnih makro definicija

       # ovisnost objektnih datoteka o izvornim datotekama
       # i komanda za stvaranje objektne datoteke
       hellomain.obj : hellomain.c
            cl -c hellomain.c
       hellosub.obj : hellosub.c
            cl -c hellosub.c

Ako se ovu datoteku spremi pod imenom makefile, dovoljno je u komandnoj liniji otkucati:

       c:> nmake

i biti će izvršen cijeli postupak kompiliranja i linkanja izvršnog programa. Ako se pak ova
datoteka zapiše pod nekim drugim imenom, primjerice "hello.mak", u komandnoj liniji, iza
preklopke –f treba zadati i ime datoteke, tj.

       c:>nmake –fhello.mak

Kako se formira makefile.
Temeljna pravila formiranja makefile datoteke su:
   • Komentari se u makefile zapisuju tako da redak započne znakom #.
   • Mogu se navesti različite simboličke definicije oblika: IME = text, što omogućuje da
       na mjestu gdje se piše $(IME) bude supstituiran text.
   • U makefile se zatim navodi niz definicija koje se sastoje od dva dijela: prvi dio opisuje
       ovisnost datoteka, a drugi opisuje kao se ta ovisnost realizira. Primjerice, u zapisu

       hellosub.obj : hellosub.c
           cl –c hellosub.c

   •    U prvom retku je označena ovisnost sadržaja "hellosub.obj" o sadržaju "hellosub.c".
   •    U drugom retku je specificirana komanda koja se primjenjuje na datoteku "hellosub.c"
   •    Redak u kojem se specificira komanda mora započeti znakom tabulatora
     Program nmake uspoređuje vrijeme kada su nastale međuovisne datoteke. Čim se promijeni
sadržaj "hellosub.c", dolazi do razlike u vremenu nastanka međuovisnih datoteka, pa program
nmake pokreće program za kompiliranje koji je specificiran u drugom retku.
     Korištenje makefile je vrlo popularno, posebno na Unix sustavima i kod profesionalnih
programera, jer se pomoću simboličkih definicija lako može definirati proces kompiliranja na
različitim operativnim sustavima.




                                                                                           32
4 Kodiranje i tipovi podataka


Naglasci:
   • kodiranje brojeva
   • kodiranje znakova
   • kodiranje logičkih vrijednosti
   • pojam tipa podataka
   • tipovi konstanti i varijabli u C jeziku
   • adrese i pokazivači
   • ispis i unos podataka

U ovom se poglavlju se opisuje kako se u računalu kodiraju brojevi i znakovi, objašnjava se
koncept tipa podataka i pokazuje karakteristike tipova u C jeziku.


4.1 Kodiranje i zapis podataka
    Kodiranje je postupak kojim se znakovima, numeričkim vrijednostima i drugim tipovima
podataka pridjeljuje dogovorom utvrđena kombinacija binarnih znamenki. Ovdje će biti opisano
kodiranje koje se koristi u C jeziku.
    S programerskog stajališta, važnije od samog načina kodiranja je veličina zauzeća
memorije i interval vrijednosti koji se postiže kodiranjem. Također, važno je upoznati leksička
pravila po kojima se zapisuju znakovne i numeričke konstante u "literalnom" obliku.

4.1.1 Kodiranje pozitivnih cijelih brojeva (eng. unsigned integers)
     Pozitivni cijeli brojevi (eng. unsigned integers), ili kardinalni brojevi, su brojevi iz skupa
kojeg čine prirodni brojevi i nula. Način njihovog kodiranja je opisan u poglavlju 2. U C jeziku
se literalne konstante, koje predstavljaju pozitivne cijele brojeve, mogu zapisati u decimalnom,
heksadecimalnom i oktalnom brojevnom sustavu, prema slijedećem leksičkom pravilu:
    •    niz decimalnih znamenki označava decimalnu konstantu ukoliko prva znamenka nije
         nula.
    •    niz oktalnih znamenki označava oktalnu konstantu ako je prva znamenka jednaka nuli.
    •    niz heksadecimalnih znamenki, kojem prethodi prefix 0x ili 0X, označava
         heksadecimalnu konstantu.

Primjer: tri ekvivalentna literalna zapisa vrijednosti binarnog niza 011011 u C jeziku su:

        decimalna konstanta                 27
        oktalna konstanta                  033
        heksadecimalna konstanta          0x1B

4.1.2 Kodiranje cijelih brojeva (eng. integers)
     Cijeli brojevi (eng. integers) su brojevi iz skupa kojeg čine prirodni brojevi, negativni
prirodni brojevi i nula, ili drukčije kazano, to su brojevi s predznakom (eng. signed integers).
Većina današnjih procesora za kodiranje cijelih brojeva s predznakom koristi tzv. komplementni




                                                                                                33
brojevni sustav. Puni komplement n-znamenkastog broja Nx, u brojevnom sustavu baze x
matematički se definira izrazom:
              N x = xn − N x
                ′
primjerice, u decimalnom sustavu komplement troznamenkastog broja 733 je 103-733= 277.
Ako se n-znamenkasti broj i njegov komplement zbroje vrijedi da će n znamenki biti jednako
nuli. U prijašnjem primjeru 733+277=1000, dakle tri znamenke su jednake nuli.
    U binarnom se sustavu puni komplement naziva komplement dvojke i vrijedi:
              N 2 = 2n − N 2
                ′
Komplement dvojke se koristi za označavanje negativnih brojeva. Primjerice, komplement
dvojke broja +1 za n=4 iznosi:
                ′
              N 2 = 2 4 − 1 = 10000 − 0001 = 1111 .
     Ako se broj i njegov komplement zbroje, rezultat treba biti nula. To vrijedi, u prethodnom
primjeru, jer su prva četiri bita zbroja jednaka nuli. Peti bit je jednak jedinici, ali on se u 4-
bitnom sustavu odbacuje.
     U sustavu komplementa dvojke pozitivni brojevi uvijek imaju MSB=0, a negativni brojevi
imaju MSB=1.
     Što se događa ako se zbroje dva pozitivna broja koji imaju bitove ispod MSB jednake
jedinici. Primjerice, ako zbrojimo 4+5 ( u 4-bitnom sustavu)

       0100
      +0101
      -----
       1001

dobije se rezultat koji predstavlja negativan broj u sustavu komplementa dvojke. Do prijelaza u
područje komplementa ne bi došlo da je rezultat zbrajanja bio manji od 7, odnosno 24-1-1.
    Poopći li se prethodno zapažanje na brojeve od n-bita, može se zaključiti da operacija
zbrajanja ima smisla samo ako je zbroj operanada manji od 2n-1-1. Zbog toga, najveći
pozitivni broj koji se može predstaviti u sustavu komplementa dvojke iznosi:

      max_int = (0111...111) = 2n-1-1,

a najveći iznos negativnog broja iznosi:

      min_int = (1000...000) = -2n-1.

Uočite da postoji za jedan više negativnih brojeva od pozitivnih brojeva.
     Obični komplement binarnog broja (naziva se i komplement jedinice) dobije se zamjenom
svih jedinica s nulom i obratno. Iznos broja. koji se dobije na ovaj način, računa se prema
izrazu:
              N 2 = 2n − N 2 − 1
    Obični komplement nije pogodan za izražavanje prirodnih brojeva jer nije jednoznačno
određena vrijednost nule, naime obični komplement od 0000 iznosi 1111. On služi za
jednostavno izračunavanje punog komplementa, jer vrijedi:
              N2 = N2 + 1
               ′




                                                                                               34
Puni komplement se izračunava tako da se običnom komplementu pribroji jedinica, primjerice,
komplement dvojke broja 6 u 8-bitnoj notaciji iznosi:

      00000110       (+6)
      --------
      11111001       (obični komplement od +6)
             1       (dodaj 1)
      --------
      11111010       (-6 u komplementu dvojke)

Izračunajmo puni komplement od 0:

       00000000        (0)
       --------
       11111111        (komplement od 0)
              1        (dodaj 1)
       --------
      100000000        (-0 u komplementu dvojke)

Jedinica predstavlja deveti bit. Ona se u 8-bitnom sustavu odbacuje pa rezultat opet predstavlja
nulu. Komplement dvojke omogućuje jednoznačno određivanje nule, pa je podesan za kodiranje
cijelih brojeva.

4.1.3. Kodiranje realnih brojeva
    Realni brojevi se u matematici zapisuju na način da se cijeli i decimalni dio odvoje
decimalnim zarezom (pr. 67,098), a koristi se i ekponentni format (eng. scientific format).
Primjerice, prethodni se broj može zapisati u obliku 0,67089⋅102. U programskom jeziku C
koristi se sličan zapis kao u matematici, s razlikom da se umjesto decimalnog zareza koristi
"decimalna točka", a potencija broja 10 se označava velikim ili malim slovom E.


              matematički zapis           ekvivalentni zapis u C-jeziku
              1,789                       1.789
              0,789                       0.789              ili
                                           .789
              -178,9⋅10-2                 -178.9e-2          ili
                                          -178.9E-2
              -0,01789⋅102                -0.01789e2         ili
                                          -0.01789E2         ili
                                          -0.01789e+2
                  Tablica 4.1. Matematički i programski zapis realnih brojeva

Eksponentni format se sastoji od dva dijela: mantise i eksponenta

      eksponentni decimalni format = mantisa 10eksponent
     Mantisa se zapisuje kao obični decimalni broj s predznakom, a eksponent se zapisuje kao
cijeli broj. Prednost korištenja eksponentnog formata je u lakšem zapisu vrlo velikih i vrlo malih
brojeva. Uočite da se promjenom vrijednosti eksponenta pomiče položaj decimalnog zareza.
Kodiranje s fiksnim položajem binarne točke (eng. fixed point numbers)
     Umjesto pojma decimalnog zareza uvodi se pojam binarne točke. Opći oblik zapisivanja
realnog broja s fiksnim položajem binarne točke, u slučaju da se N znamenki koristi za




                                                                                               35
označavanje cijelih vrijednosti, a n znamenki za označavanje razlomljenih vrijednosti (po bazi
2: 1/2, 1/4, 1/8 itd.) glasi:


             bN −1bN − 2 ...b0 • b−1b− 2 ...b− n ,

a iznos mu se računa prema izrazu:


             N .n2 = bN −1 2 N −1 + ... + b0 2 0 + b−1 2 −1 + b− 2 2 −2 + ... + b− n 2 − n ,   bi ∈ (0,1)


     Ako se ovaj broj pomnoži s 2n može ga se u operacijama smatrati cijelim brojem, a nakon
izvršenih aritmetičkih operacija rezultat se skalira za iznos 2-n. Ovaj oblik kodiranja ima brojne
nedostatke, i koristi se samo u izuzetnim slučajevima.
Kodiranje s pomičnim položajem binarne točke (eng. floating point numbers)
    Ideja eksponentnog formata uzeta je kao temelj za kodiranje realnih brojeva i u binarnom
brojevnom sustavu. Kodiranje se vrši prema izrazu

             F = ( − 1) m 2 e
                          s



gdje m predstavlja mantisu, e je eksponent dvojke, a s∈(0,1) određuje predznak broja.
Eksponent i mantisa se kodiraju u binarnom brojevnom sustavu. Eksponent se kodira kao cijeli
broj, a mantisa kao binarni broj s fiksnim položajem binarne točke. Ideja je jednostavna:
promjenom eksponenta pomiče se i položaj binarne točke iako se mantisa zapisuje s fiksnim
položajem binarne točke. Primjerice, neka je broj kodiran u obliku:

      0.00001xxxxxxxxx 2e

gdje x može biti 0 ili 1. Ovaj oblik zapisa realnog broja naziva se nenormalizirani zapis.
Pomakne li se položaj binarne točke za 5 mjesta ulijevo dobije se ekvivalentni zapis

      1.xxxxxxxxx00000 2e-5,

     Posmak bitova ulijevo ekvivalentan je dijeljenju s dva, stoga se vrijednost eksponenta
smanjuje za 5. Ovakav kodni zapis, u kojem je uvijek jedinica na prvom mjestu, naziva se
normalizirani zapis. Značaj normaliziranog zapisa je u činjenici što se njime iskorištavaju svi
bitovi mantise za kodiranje vrijednosti, dakle osigurava se veća točnost zapisa. Normaliziranim
se oblikom ipak ne može kodirati veoma male vrijednosti, pa je tada pogodniji nenormalizirani
zapis broja. Treba pojasniti i kako je kodirana vrijednost nula. Pogodno bi bilo da sva bitna
polja pri kodiranju vrijednosti nula budu jednaka nuli (zbog logičkih operacija), ali pošto se za
kodiranje eksponenta također koristi binarni zapis, vrijednost eksponenta nula se matematički
koristi za označavanje brojeva većih od jedinice. Da bi se zadovoljilo zahtjevu kodiranja nule s
nultim zapisom eksponenta uobičajeno je da se umjesto stvarne vrijednosti eksponenta kodira
vrijednost:

      E = e + pomak,

gdje je pomak neka konstantna vrijednost, a odabire se na način da je jednak najnižoj vrijednosti
eksponenta e, koji je negativna vrijednost Ovako zapisani eksponent naziva se pomaknuti
eksponent. Značaj pomaka pokazuje sljedeći primjer. Neka je eksponent opisan s 8 bita. Tada se
E kreće u rasponu od 0 do 255. Ako se uzme da je pomak=127, i da je E=0 rezervirano za
kodiranje nule, onda se vrijednost binarnog eksponenta kreće u rasponu od -126 do +127.




                                                                                                            36
Postoji više različitih formata zapisa mantise i eksponenta u binarnom kodu. Danas se
gotovo isključivo koristi format koji je određen ANSI/IEEE standardom br.745 iz 1985. godine.
Prema tom standardu koriste se dva tipa kodiranja: jednostruki format (32 bita) i dvostruki
format (64 bita).




            Slika 4.1 Format kodiranja realnih brojeva prema IEEE/ANSI standardu

                     STANDARDNI IEEE/ANSI FORMAT REALNIH                  BROJEVA
  parametar                   Jednostruki (SINGLE)                        dvostruki (DOUBLE)
  ukupan broj bita            32 (+1)                                     64 (+1)
  broj bita eksponenta        8                                           11
  broj bita za predznak       1                                           1
  broj bita mantise           23 (+1)                                     52 (+1)
  pomak                       +127                                        +1023
  Emax                        255                                         2047
  Emin                        0                                           0
  minreal (za nenorm.)        (-1)s⋅1.4⋅10-45                             (-1)s⋅2.225⋅10-324
  minreal (za norm.)          (-1)s⋅1.175⋅10-38                           (-1)s⋅2.225⋅10-308
  maxreal                     (-1)s⋅3.4028⋅10+38                          (-1)s⋅1.797⋅10+308

                    Tablica 4.2. Standardni IEEE/ANSI format realnih brojeva

     Bitna karakteristika ovog standarda je da je u format za kodiranje realnog broja moguće
upisati i šifru o ispravno obavljenoj matematičkoj operaciji (pr. dijeljenje s nulom dalo bi
beskonačnu vrijednost, koju je nemoguće kodirati, pa se ta operacija izvještava kao greška).
Binarno kodirani signal greške koristi format binarno kodiranih realnih brojeva, ali kako to nije
broj, u standardu se opisuje pod nazivom NaN (Not a Number). Kodiranje s normaliziranim
zapisom mantise je izvršeno na način da se ne upisuje prva jedinica, čime se podrazumjeva da je
mantisa kodirana s jednim bitom više nego je to predviđeno u binarnom zapisu. Vrijednost
pomaka i raspona eksponenta dana je tablicom 3.2. Vrijednost eksponenta Emin je iskorištena za
kodiranje nule, a vrijednost Emax za kodiranje NaN-a i beskonačnosti.
Zapis formata se interpretira na sljedeći način:
    1.   Ako je E=Emax i m≠0 kodna riječ predstavlja NaN, bez obzira na vrijedost predznaka s.
    2.   Ako je E=Emax i m=0 kodna riječ predstavlja (-1)s (∝).
    3.   Ako je Emin<E<Emax kodna riječ predstavlja broj (-1)s2e-127(1.m), tj. predstavlja
         normalizirani realni broj.
    4.   Ako je E=0 i m=0 kodna riječ predstavlja broj (-1)s(0).
    5.   Ako je E=0 i m≠0 kodna riječ predstavlja broj (-1)s2ee-127(0.m) tj. predstavlja nenormalizirani
         realni broj (vodeća nula se ne zapisuje). Opis nenormaliziranim brojevima ne osigurava točnost
         za sve brojeve pa se ovo kodiranje u nekim implementacija ne koristi. Vrijednosti za minimalnu
         i maksimalnu vrijednost realnog broja u tablici 3.2. dani su za normalizirani i nenormalizirani
         format realnog broja.




                                                                                                     37
Programer, koji programira u višem programskom jeziku, ne mora znati kako se neki broj
kodira u procesoru ili memoriji računala. Njega zanimaju pravila za zapis literalnih konstanti,
veličina zauzeća memorije, maksimalna i minimalna vrijednost broja, te broj točnih decimala.
Normaliziranim zapisom postiže se točnost na 7 decimala za jednostruki format, odnosno 15
decimala za prošireni format.

4.1.4. Kodiranje znakova
     Znak (eng. character) je element dogovorno usvojenog skupa različitih simbola koji su
namijenjeni obradi podataka (slova abecede, numeričke znamenke, znakovi interpunkcije i sl.).
Niz znakova se često tretira kao cjelina i naziva se string. Primjerice, u C jeziku se string
literalno zapisuje kao niz znakova omeđen znakom navodnika ("ovo je literalni zapis C
stringa").
     Za kodiranje slova, znamenki i ostalih tzv. kontrolnih znakova koristi se američki standard
za izmjenu informacija ASCII (American Standard Code for Information Interchange). Njemu
je ekvivalentan međunarodni standard ISO 7. ASCII kôd se najviše koristi za komuniciranje
između računala i priključenih vanjskih jedinica: pisača, crtača, modema, terminala itd.
     To je sedam bitni kôd (ukupno 128 različitih znakova), od čega se prva 32 znaka koriste
kao kontrolni znakovi za različite namjene, a ostali znakovi predstavljaju slova abecede,
pravopisne i matematičke simbole.

000:   (nul)   016:   (dle)    032:   (sp)   048:   0     064:   @   080:   P     096:   ž   112:   p
001:   (soh)   017:   (dc1)    033:   !      049:   1     065:   A   081:   Q     097:   a   113:   q
002:   (stx)   018:   (dc2)    034:   "      050:   2     066:   B   082:   R     098:   b   114:   r
003:   (etx)   019:   (dc3)    035:   #      051:   3     067:   C   083:   S     099:   c   115:   s
004:   (eot)   020:   (dc4)    036:   $      052:   4     068:   D   084:   T     100:   d   116:   t
005:   (enq)   021:   (nak)    037:   %      053:   5     069:   E   085:   U     101:   e   117:   u
006:   (ack)   022:   (syn)    038:   &      054:   6     070:   F   086:   V     102:   f   118:   v
007:   (bel)   023:   (etb)    039:   '      055:   7     071:   G   087:   W     103:   g   119:   w
008:   (bs)    024:   (can)    040:   (      056:   8     072:   H   088:   X     104:   h   120:   x
009:   (tab)   025:   (em)     041:   )      057:   9     073:   I   089:   Y     105:   i   121:   y
010:   (lf)    026:   (eof)    042:   *      058:   :     074;   J   090:   Z     106:   j   122:   z
011:   (vt)    027:   (esc)    043:   +      059:   ;     075:   K   091:   [     107:   k   123:   {
012:   (np)    028:   (fs)     044:   ,      060:   <     076:   L   092:        108:   l   124:   |
013:   (cr)    029:   (gs)     045:   -      061:   =     077:   M   093:   ]     109:   m   125:   }
014:   (so)    030:   (rs)     046:   .      062:   >     078:   N   094:   ^     110:   n   126:   ~
015:   (si)    031:   (us)     047:   /      063:   ?     079:   O   095:   _     111:   o   127:   del

                          Tablica 4.3. ACSII standard za kodiranje znakova

U Hrvatskoj se za latinična slova Č,Š,Ž,Ć i Đ koristi modifikacija ASCII standarda. (tablica
4.4).
          HR-ASCII                                      IBM EBCDIC-852 standard
          dec   hex ASCII HR-ASCII                      Dec     hex EBCDIC-852
           64    40    @    Ž                            166     A6      Ž
           91    5B    [    Š                            230     E6      Š
           92    5C        Đ                            209     D1      Đ
           93    5D    ]    Ć                            172     AC      Ć
           94    5E    ^    Č                            143     8F      Č
           96    60    `    ž                            167     A7      ž
          123    7B    {    š                            231     E7      š
          124    7C    |    đ                            208     D0      đ
          125    7D    }    ć                            134     86      ć
          126    7E    ~    č                            159     9F      č

                              Tablica 4.4. HR_ASCII i EBCDIC-852 standard




                                                                                                38
Na IBM PC računalima se koristi 8-bitno kodiranje znakova, čime je omogućeno kodiranje
256 znakova. Ovaj kod se naziva EBCDIC (Extended Binary Coded Decimal Interchange
Code). Prvih 128 znakova ovog koda jednaki su ASCII standardu, a ostali znakovi predstavljaju
različite grafičke i matematičke simbole te slova koja se koriste u alfabetu većine zapadno-
evropskih zemalja, a nisu obuhvaćena ASCII standardom. U sklopu tog standarda predviđeno je
da se znakovi Č,Š,Ž,Ć,Đ,č,ć,ž,š,đ kodiraju vrijednostima većim od 127. Taj standard ima
oznaku EBCDIC-852 i prikazan je u tablici 4.4. U Windows operativnom sustavu, koji koristi
grafičko sučelje, koristi se poseban tip kodiranja, gdje se uz kôd znakova zapisuje i oblik znaka
(font). Sve se više koristi prošireni skup znakova koji je određen 16-bitnim Unicode
standardom. Njime su obuhvaćeni znakovi iz svih svjetskih jezika.

    U C jeziku se imena varijabli i funkcija označavaju nizom znakova, stoga se za
označavanje znakovne konstante koriste jednostruki navodnici, primjerice u C iskazu:

      a = 'a';

slovo a se koristi za označavanje varijable s lijeve strane naredbe pridjele vrijednosti, a s desne
strane je napisana znakovna konstanta 'a'. Ovom se naredbom se varijabli a pridjeljuje ASCII
vrijednost znakovne konstante 'a', odnosno numerička vrijednost 97.
    Potrebno je razlikovati sljedeće iskaze:

      1)    a = 8;
      2)    a = '8';

U prvom iskazu varijabla a ima numeričku vrijednost 8, a u drugom iskazu varijabla a ima
numeričku vrijednost 56, jer se tada varijabli pridjeljuje numerička ASCII vrijednost znaka '8'.
U samom procesu programiranja uglavnom nije nužno znati numeričku vrijednost znakovne
konstante, već se tada apstraktno uzima da je varijabli a pridijeljen znak '8'.
    Neki znakovi iz ASCII skupa se ne mogu zapisati u izvornom kodu, jer za njih ne postoji
oznaka na standardnoj tipkovnici. Za slučaj da se pak želi raditi i s takvim znakovima , za
njihov unos u C jeziku se koriste specijalne kontrolne sekvence (tzv. escape sequence), koje se
zapisuju na način da se napiše obrnuta kosa crta  i jedan od sljedećih znakova.

      b           oznaka za povrat unatrag - backspace BS
      f           oznaka za stranicu unaprijed - form feed FF
      n           oznaka nove linije - new line NL
      r           oznaka za povrat na početak reda CR
      t           oznaka za horizontalni tab HT
      v           oznaka za vertikalni tab
      ”           oznaka za dvostruki navodnik
      '           oznaka za jednostruki navodnik
                 oznaka za znak  - backslash
      ooo         ASCII kôd znaka je zapisan oktalnim znamenkama ooo.
      0xhh        ASCII kôd znak je zapisan heksadecimalnim znamenkama hh.


Primjerice, ekvivalentna su tri zapisa znaka A:

         'A',    '0x41',      '101'

      Znakovne konstante su u C jeziku zapisuju unutar jednostrukih zagrada znakovima
      iz ASCII skupa (ili kontrolnom sekvencom). Može ih se tretirati i kao numeričke




                                                                                                39
vrijednosti cjelobrojnog tipa na način da je vrijednost znakovne konstante jednaka
      ASCII kodu znaka.


     Kada se u C jeziku koriste literalne konstante, koje predstavljaju string (niz znakova), one
se zapisuju unutar dvostrukih navodnika, primjerice

      "Hello World!n".

Unutar stringa se kontrolni znakovi zapisuju bez navodnika. U ovom primjeru, na kraju stringa
je zapisan znak za prijelaz u novu liniju.

4.1.5. Kodiranje logičkih vrijednosti
      Moguće su samo su dvije logičke vrijednosti: istina i laž. U većini programskih jezika
(C++, Pascal) ove vrijednosti se zapisuju s “true” i “false”. U C jeziku se ne koristi posebno
označavanje logičkih vrijednosti, već vrijedi pravilo da se svaka numerička vrijednost može
tretirati kao logička “istina” ako je različita od nule, odnosno “laž” ako je jednaka nuli.
      Logičke vrijednosti nastaju i kao rezultat relacijskih izraza u kojima se koriste operatori: >
(veće), < (manje), >= (veće ili jednako) ,<= (manje ili jednako), == (jednako) i != (nije
jednako). U C jeziku, ako je vrijednost relacijskog izraza logička vrijednost “istina” ona se
kodira kao numerička vrijednost 1, a logička vrijednost “laž” se kodira kao numerička
vrijednost 0. Primjerice u izrazu

               a = x > y;

varijabli a se pridjeljuje vrijednost 1 ako je izraz na desnoj strani istinit (tj. ako je x veće od y),
u suprotnome, pridjeljuje se vrijednost 0. Relacijski se odnos dvije vrijednosti pri izvršavanju
programa zapravo određuje pomoću operacije oduzimanja, a definiran je u tablici 4.5.

         Ako   je         x == y      x != y      x > y       x < y      x <= y        x >= y
         x-y   > 0           0           1          1           0           0             1
         x-y   = 0           1           0          0           0           1             1
         x-y   < 0           0           1          0           1           1             0

                     Tablica 4.5 Rezultat izvršenja relacijskih operacija u C jeziku


4.2 Memorija
     Prije nego se pokaže kako se na apstraktnoj razini manipulira s podacima, koji se nalaze u
radnoj memoriji računala, potrebno je upoznati neke hardverske karakteristike radne memorije
računala.
     Elektronička memorijska jedinica može pamtiti 1 bit informacije. Više memorijskih
jedinica se pakira u integrirani elektronički sklop koji se popularno naziva čip. U računalu se
memorijski čipovi spajaju na način da se istovremeno može pristupiti grupi od 8 ili više
temeljnih memorijskih jedinica, pa se sa softverskog stajališta može uzeti da jedna memorijska
ćelija sadrži 1 bajt digitalne informacije.
     Informacije se između procesora i memorije ne prenose serijski, bit po bit, već se vrši
istovremeni prijenos više digitalnih znamenki (8, 16, 32 ili 64 bita) pomoću višestrukih
električnih vodova, koje se obično naziva sabirnica (eng. bus). Postoje tri tipa sabirnice:
adresna sabirnica služi za aktiviranje memorijske ćelije, podatkovna sabirnica služi za prijenos
podatka koji će biti spremljen (zapisan) ili dobavljen (pročitan) iz te memorijske ćelije i
kontrolna sabirnica kojom se određuje da li i kako se vrši čitanje ili pisanje.
     Na slici 3.2. prikazana je pojednostavljena shema spajanja memorijskih čipova. Stanje
upisivanja ili čitanja sadržaja memorijske grupe (8,16,32 ili 64 bita) kontrolira se signalom mem




                                                                                                    40
r/w (memory read/write). Usklađenost rada s ostalim sklopovima računala osigurava se na način
da se vrijeme upisivanja ili čitanja iz memorije kontrolira posebnim signalima generatora takta.
Aktiviranje jedne memorijske grupe određeno je binarnom kombinacijom naponskih stanje
adresne sabirnice. Memorija je, stoga, linearno uređen prostor, jer je položaj u memoriji, tj.
adresa, proporcionalan numeričkoj vrijednosti binarne kombinacije adresne sabirnice.




             Slika 4.2. Pojednostavljeni prikaz sheme spajanja memorijskih čipova

    Potrebno je uočiti da svakom podatku u memoriji su pridjeljene dvije vrijednosti: adresa i
sadržaj memorije na toj adresi. Veličina se adresne sabirnice i sabirnice podataka iskazuje
brojem bita koje oni u jednom trenutku prenose i određena je mogućnostima procesora i
hardverskom strukturom računala. Maksimalni memorijski kapacitet je određen izrazom:
      Maksimalni memorijski kapacitet = 2veličina adresne sabirnice (bajta)
     Tzv. 8-bitni mikroprocesori (Z-80, Motorola 6800, Intel 8080) imaju 16-bitnu adresnu
sabirnicu i 8-bitnu sabirnicu podataka, pa im maksimalni memorijski kapacitet iznosi 64
kilobajta. Prva personalna računala klase IBM PC XT imala su adresnu sabirnica od 20 linija i
8-bitnu sabirnica podataka, pa je maksimalni kapacitet memorije tih računala iznosio 1M. Kod
današnjih personalnih računala koriste se 32-bitne sabirnice (kod većih računala koriste se 64-
bitne sabirnice podataka) pa maksimalni kapacitet memorije može biti 4GB. Realno se u
računala ugrađuje znatno manje memorije (64MB-1GB).
     Na temelju onoga što je do sada kazano o kodiranju sadržaja i hardverskom ustrojstvu
memorije, sa programerskog stajališta, važne su sljedeće činjenice i definicije:
    1. Podaci, koji se nalaze na nekoj adresi u memoriji, nazivaju se memorijski objekti.
    2. Adresa se opisuje kardinalnim brojem koji opisuje položaj temeljne memorijske ćelije,
       veličine 8 bita, u linearno uređenom prostoru memorije.
    3. Podaci su najčešće kodirani tako da pripadaju skupu numeričkih vrijednosti ili skupu
       znakova koji su kodirani prema ASCII standardu, ili im se pridodaje logička vrijednost
       {true, false}.
    4. Memorijskim objektima procesor može mijenjati sadržaj i s njima obavljati aritmetičko-
       logičke operacije.
    5. Memorijski objekti, s kojima procesor operira kao s cjelinom, nazivaju se varijable.




                                                                                             41
4.3 Prosti tipovi podataka
     U uvodu je naglašeno da ime varijable simbolički označava položaj u memoriji (adresu) na
kojem je upisana vrijednost varijable. Sada ćemo uz pojam varijable uvesti i pojam tipa
varijable.
     U matematici je uobičajeno uz matematičke izraze navesti i skup vrijednosti varijabli za
koje ti izrazi vrijede. Primjerice, neka varijabla x pripada skupu realnih brojeva (x ∈ R), a
varijabla z skupu kompleksnih brojeva (z ∈ Z). Tada vrijedi:
              z Re( z)     Im( z)
                =      + j
              x   x          x
Vrijednost ovog izraza također pripada skupu kompleksnih brojeva. Bitno je uočiti da oznaka
pripadnosti skupu vrijednosti određuje način kako se računa matematički izraz i kojem skupu
vrijednosti pripada rezultat izraza. U programskim se jezicima pripadnost skupu koji ima
zajedničke karakteristike naziva tipom. Tip varijable određuje način kodiranja, veličinu
zauzimanja memorije i operacije koje su sa tom varijablom dozvoljene. Tipovi varijabli (ili
konstanti) koji se koriste u izrazima određuju tip kojim rezultira taj izraz.

U većini programskih jezika se koriste sljedeći tipovi podataka:

    • numerički (cjelobrojni ili realni),
    • logički,
    • znakovni,

a nazivaju se i primitivni ili prosti tipovi jer predstavljaju nedjeljive objekte koji imaju izravan
prikaz u memoriji računala.

   Tip             Oznaka u                 Interval                                zauzeće
                   C jeziku                 vrijednosti                             Memorije
   znakovni        [signed]    char         -127 .. 128                             1 bajt
   tip             Unsigned    char            0 .. 255                             1 bajt
   cjelobrojni     [signed]    int          -2147483648.. 2147483647                4 bajta
   tip             [signed]    short        -32768 .. 32767                         2 bajta
                   [signed]    long         -2147483648.. 2147483647                4 bajta
   kardinalni      unsigned    [int]        0 .. 4294967295                         4 bajta
   tip             unsigned    short        0 .. 65535                              2 bajta
                   unsigned    long         0 .. 4294967295                         4 bajta
   realni tip                               min   ± 1.175494351e-38
   (jednostruk     float                    maks ± 3.402823466e+38                  4 bajta
   i format)
   realni tip      double                   min ± 2.2250738585072014e-308           8 bajta
   (dvostruki                               maks ± 1.7976931348623158e+308
   format)
   logički tip     -                        0 .. različito od nule                  -

Tablica 4.6. Označavanje standardnih tipova podataka u C jeziku (uglate zagrade označavaju
                                     opcioni element)

    Tablica 4.6 prikazuje karakteristike standardnih tipove podataka koji se koriste u C jeziku;
ime kojim se označavaju, interval vrijednosti i veličina zauzeća memorije u bajtima. Uglate
zagrade označavaju opciona imena tipova, primjerice može se pisati

      int      ili         signed int
      unsigned ili         unsigned int




                                                                                                 42
Oznake tipova su uzete iz leksike engleskog jezika: int je kratica leksema integer (cijeli
broj), char je kratica leksema character (znak), signed je pridjev koji označava skup cijelih
brojeva u kojem se koristi predznak (+ ili -), a unsigned je pridjev koji označava da se iz
skupa cijelih brojeva koriste samo brojevi bez predznaka (kardinalni brojevi), float je kratica
od floating point (realni broj kodiran prema IEEE standardu jednostrukog formata), a double
znači dvostruko u smislu dvostruko preciznog IEEE formata zapisa realnih brojeva.


      Uz pojam varijable uvijek su vezani pojmovi: tip i ime. Ime predstavlja oznaku
      adrese varijable u memoriji, pa se zove i referenca varijable, ili referenca
      memorijskog objekta. Tip označava skup vrijednosti varijable, veličinu zauzeća
      memorije, način kodiranja i operacije koje se mogu primijeniti.

      Pridjeljivanje oznake tipa nekoj varijabli naziva se deklaracija varijable, a ako se
      pod tom deklaracijom podrazumijeva i rezerviranje memorijskog prostora u kojem
      će biti smještena vrijednost varijable, onda se kaže da je deklaracijom izvršena i
      definicija varijable. U C jeziku uvijek mora biti deklariran tip varijable prije nego
      sa ona upotrijebi u programskim iskazima.


Primjer: Program, imena povrsina.c, služi za proračun površine kruga. U njemu se koriste
se dvije varijable tipa double: r označava radijus, a P površinu kruga. Pretpostavlja se da je
radijus poznat i neka iznosi 2.1 m. Rezultat proračuna će biti ispisan pomoću printf()
funkcije. Evo kako je napisan taj program.

      /* Datoteka: povrsina.c                             */
      /* Program koji računa površinu kruga radijusa 2.1m */

      #include <stdio.h>

      int main()
      {
          double r, P;              /* deklaracija varijabli r i P */
          r = 2.1;                  /* zadana vrijednost radijusa 2.1m */

            P = r*r*3.14;           /* proračun površine kruga                      */

            printf( "n Povrsina kruga = %f m", P);
            return 0;
      }


Kada se program kompilira i izvrši dobije se ispis:

      Povrsina kruga = 13.847400 m

     Komentarima je opisan značaj pojedinog programskog iskaza. Najprije je izvršena
deklaracija varijabli r i P, prema pravilu da se oznaka tipa napiše ispred imena varijabli (ako
se istovremeno deklarira više varijabli istoga tipa, njihova se imena odvajaju zarezom). Zapis
deklaracije završava znakom točka-zarez. Nadalje, naredbom pridjele vrijednosti postavljena je
vrijednost varijable r na zadanu vrijednost 2.1. Proračun površine je izvršen prema izrazu
3.14*r*r, gdje zvjezdica označava operator množenja. U matematici formula za površinu
glasi r2π. Ovu formulu se ne može izravno primijeniti jer u C jeziku ne postoji operator
potenciranja.




                                                                                              43
Rezultat proračuna je pridijeljen varijabli P. Za ispis vrijednosti te varijable koristi se
standardna funkcija printf(). Potrebno je uočiti razliku od zapisa printf() funkcije koji je
korišten u programu Hello.c. U ovom slučaju funkcija printf() koristi dva argumenta. Prvi
argument je literalni string, a drugi argument je varijabla P čiju vrijednost treba ispisati. Položaj
na kojem će se ispisati ta vrijednost označen je unutar literalnog stringa oznakom %d. Ova
oznaka se naziva specifikator ispisa. Argumenti su odvojeni zarezom.
Opći oblik korištenja printf funkcije je:

      printf(format_ispisa, lista_argumenata)

gdje lista_argumenata sadrži nula ili više argumenata funkcije odvojenih zarezom, a položaj
gdje će biti izvršen ispis vrijednosti tih argumenta označava se u stringu format_ispisa
specifikatorom ispisa. Broj specifikatora ispisa mora biti jednak broju argumenata funkcije koji
su navedeni u lista_argumenata. Specifikatori ispisa moraju odgovarati tipu argumenta,
primjerice za cjelobrojne argumente ( tip: int, short, long) oznaka je %d, za znakovne
argumente (tip char) oznaka je %c, za realne argumente (tip: float, double) oznaka je %f
ili %g, a za ispis stringa oznaka je %s. Argumenti funkcije mogu biti imena varijabli,
konstante i izrazi koji rezultiraju nekom vrijednošću.
    Sada će sa nekoliko programa biti pokazane karakteristike standardnih tipova C jezika. Prvi
program sizeof.c ispisuje koliko pojedini tip zauzima memorije.

/* Datoteka: sizeof.c */
/* Program ispisuje zauzeće memorije za sve proste tipove C jezika */

#include <stdio.h>

int main()
{
printf( "nSizeof(char)               = %d",      sizeof( char ));
printf( "nSizeof(int)                = %d",      sizeof( int ));
printf( "nSizeof(short)              = %d",      sizeof( short ));
printf( "nSizeof(long)               = %d",      sizeof( long ));
printf( "nSizeof(float)              = %d",      sizeof( float ));
printf( "nSizeof(double)             = %d",      sizeof( double ));
printf( "nSizeof(unsigned            char)       = %d", sizeof( unsigned char ));
printf( "nSizeof(unsigned            int)        = %d", sizeof( unsigned int ));
printf( "nSizeof(unsigned            short)      = %d", sizeof( unsigned short ));
printf( "nSizeof(unsigned            long)       = %dn", sizeof( unsigned long ));
return 0;
}


Kada se kompilira i izvrši, program daje ispis:
      Sizeof(char)           = 1
      Sizeof(int)            = 4
      Sizeof(short)          = 2
      Sizeof(long)           = 4
      Sizeof(float)          = 4
      Sizeof(double)         = 8
      Sizeof(unsigned        char)     =    1
      Sizeof(unsigned        int)      =    4
      Sizeof(unsigned        short)    =    2
      Sizeof(unsigned        long)     =    4




                                                                                                  44
U ovom se programu koristi standardni C operator sizeof koji daje vrijednost u bajtima,
koju neki tip ili prethodno deklarirana varijabla zauzima u memoriji. Primjerice,
sizeof(short int) daje vrijednost 2.
     Konstante C jezika indirektno u svom zapisu sadrže oznaku tipa. Primjeri su dani u tablici
4.7. Uočite da sufiks U ili u označava konstante kardinalnog tipa, sufiks f ili F označava realne
brojeve jednostrukog formata, sufiks L ili l označava brojeve dvostrukog formata.


    char           'A'         znakovna konstanata A
                   'a'         znakovna konstanata a
    ili            '035'      znakovna konstanata 35 u oktalnoj notaciji
                   'x29'      znakovna konstanata 29 u heksadecimalnoj notaciji
    int            'n'        znak za novu liniju
    Int            156         decimalna notacija
                   0234        oktalna notacija          cjelobrojne konstante
                   0x9c        heksadecimalna notacija
    unsigned       156U        Decimalno
                   0234U       oktalno      (prefiks U određuje kardinalni broj)
                   0x9cU       heksadecimalno
    float          15.6F       realni broj – jednostruki format
                   1.56e1F     određen primjenom sufiksa F ili f
    double         15.6        konstante su tipa "double" ukoliko se ne koristi
                   1.56E1L     prefiks F. Nije nužno pisati sufiks L.

                              Tablica 4.7 Literalni zapis konstanti
     U tablici 7.7 važno je uočiti da su znakovne konstante kompatibilne i sa znakovnim i s
cjelobrojnim tipom. To demonstrira program charsize.c.

       /* Datoteka: charsize.c */
       /* Program kojim se ispituje tip znakovne konstante */

       #include <stdio.h>

       int main()
       {
           char c;           /* deklaracija varijable c tipa char*/
           int x;            /* deklaracija varijable x tipa int*/

             c = 'A';        /* objema varijablama može se pridijeliti */
             x = 'A';        /* vrijednost znakovne konstante 'A' */

             printf( "n c = %c", c);
             printf( "n Sizeof c = %d", sizeof (c) );

             printf( "n x = %d", x);
             printf( "n Sizeof x = %d", sizeof(x));
             printf( "n Sizeof 'A' = %d", sizeof('A'));
             return 0;
       }


koji daje ispis:

       c = A
       Sizeof c = 1
       x = 65
       Sizeof x = 4
       Sizeof 'A' = 4




                                                                                              45
U programu su prvo deklarirane dvije varijable; c je tipa char, a x je tipa int. Objema
varijablama je zatim pridijeljena vrijednost znakovne konstante 'A'. Prilikom ispisa varijable c
koristi se specifikator ispisa za znakove - %c, pa se ispisuje slovo A. Ispis vrijednosti varijable x
daje cjelobrojnu vrijednost, koja je jednaka vrijednosti ASCII koda znaka A. Očito je da
kompilator tretira znakovnu konstantu ovisno o kontekstu u kojem se koristi, a za nju rezervira
u memoriji mjesto od 4 bajta, što pokazuje posljednji ispis. Ova dvostrukost primjene znakovnih
konstanti zapravo je dobra strana C jezika. Time je omogućeno da se znakovne konstante mogu
koristiti i kao simboli i kao numeričke vrijednosti. Simbolički značaj konstante važan je pri
unosu i ispisu znakova, numerički značaj omogućuje da se znakovne konstante mogu koristiti u
aritmetičkim i relacijskim izrazima na isti način kao i cjelobrojne konstante.


4.4 Direktiva #define
     Leksička se vrijednost numeričkih, znakovnih i literalnih konstanti, pa i samog
programskog teksta, može pridijeliti nekom simboličkom imenu pomoću pretprocesorske
direktive #define. Primjerice, ako se zapiše:

      #define PI 3.141592653589793

to ima efekt da kada se u tekstu programa, koji slijedi iza ove direktive, zapiše PI, vrijedi kao da
je zapisano 3.141592653589793.
    Do sada smo upoznali direktivu #include. Sve pretprocesorske direktive počinju znakom
#. Njih se ne smatra programskim naredbama pa se iza njih ne zapisuje znak točka-zarez.
Pomoću njih se vrši leksička supstitucija teksta iz drugih datoteka ( pomoću #include) ili
prema supstitucijskom pravilu koji je opisan direktivu #define .
Općenito direktivu #define ima oblik:

      #define IME        supstitucijski_tekst

gdje IME predstavlja proizvoljan naziv zapisan neprekinutim nizom znakova (obično se zapisuje
velikim slovima i podvlakom), a supstitucijski_tekst može sadržavati i isprekidani niz
znakova. Primjerice,

      #include <stdio. h>
      #define MESSAGE "Vrijednost broja pi = "
      #define PI 3.141592653589793
      #define PR_NL printf("n")

      int main( void)
      {
        printf(MESSAGE);
        printf("%f" ,PI);
        PR_NL;
        return 0;
      }


Nakon izvršenja dobije se ispis

      Vrijednost broja pi = 3.141592653589793

Kasnije će biti detaljnije pojašnjen rad s pretprocesorskim direktivama.




                                                                                                  46
Na kraju ovog poglavlja pogledajmo program Limits.c. Njime se ispisuje minimalna i
maksimalna vrijednost za sve proste tipove podataka C jezika. U programu se koriste
simboličke konstante koje su pomoću direktive #define zapisane u standardnim datotekama
limits.h i float.h.

    /* Datoteka: Limits.c */
    /* Ispisuje interval vrijednost numeričkih tipova */

    #include       <stdio.h>
    #include       <limits.h>
    #include       <float.h>

    int main( void )
    {
        printf("%12s%12s%15s%15sn",
                 "Tip", "Sizeof", "Minimum", "Maksimum");
        printf("%12s%15d%15dn","char", CHAR_MIN, CHAR_MAX) ;
        printf("%12s%15d%15dn","short int", SHRT_MIN, SHRT_MAX) ;
        printf("%12s%15d%15dn","int", INT_MIN, INT_MAX) ;
        printf("%12s%15ld%15ldn","long int", LONG_MIN, LONG_MAX) ;
        printf("%12s%15g%15gn", "float", FLT_MIN, FLT_MAX) ;
        printf("%12s%15g%15gn", "double", DBL_MIN, DBL_MAX) ;
        printf("%12s%15Lg%15Lgn","long double",LDBL_MIN, LDBL_MAX) ;
        return 0 ;
    }


Nakon izvršenja dobije se ispis:

           Tip          Sizeof            Minimum            Maksimum
              char                   1            -128                  127
         short int                   2          -32768                32767
               int                   4     -2147483648           2147483647
          long int                   4     -2147483648           2147483647
             float                   4    1.17549e-038         3.40282e+038
            double                   8    2.22507e-308         1.79769e+308
       long double                   8    2.22507e-308         1.79769e+308

Ovaj ispis izgleda uredno. To je postignuto korištenjem specifikatora ispisa printf() funkcije
u proširenom obliku, tako da je između znaka % i oznake tipa ispisa upisan broj kojim se
određuje točan broj mjesta za ispis neke vrijednosti.


4.5 Specifikatori printf funkcije
     Kada se ispis vrši po unaprijed određenom obliku i rasporedu kažemo da se vrši formatirani
ispis. Sada će biti pokazano kako se zadaje format ispisa printf() funkcije. Općenito format
se zadaje pomoću šest polja:
      %[prefiks][širina_ispisa][. preciznost][veličina_tipa]tip_argumenta

Format mora započeti znakom % i završiti s oznakom tipa argumenta. Sva ostala polja su
opciona (zbog toga su napisana unutar uglatih zagrada).
     U polje širina_ispisa zadaje se minimalni broj kolona predviđenih za ispis vrijednosti. Ako
ispis sadrži manji broj znakova od zadane širine ispisa, na prazna mjesta se ispisuje razmak.
Ako ispis sadrži veći broj znakova od zadane širine, ispis se proširuje. Ako se u ovo polje




                                                                                             47
upiše znak * to znači da će se broj kolona indirektno očitati iz slijedećeg argumenta funkcije,
koji mora biti tipa int.

       Polje prefiks može sadržavati jedan znak koji ima sljedeće značenje:
 -     Ispis se poravnava prema lijevoj granici ispisa određenog poljem širina_ispisa. (inače se
       poravnava s desne strane) U prazna mjesta se upisuje razmak
+      Pozitivnim se vrijednostima ispisuje i '+' predznak.
razmak Ako je vrijednost pozitivna, dodaje se razmak prije ispisa (tako se može poravnati kolone s
       pozitivnim i negativnim brojevima).
0      Mjesta razmaka ispunjaju se znakom 0.
#      Alternativni stil formatiranja


     Polje .preciznost određuje broj decimalnih znamenki iza decimalne točke kod ispisa realnog
broja ili minimalni broj znamenki ispisa cijelog broja ili maksimalni broj znakova koji se
ispisuje iz stringa. Ovo polje mora započeti znakom točke, a iza nje se navodi broj ili znak *,
koji znači da će se preciznost očitati iz slijedećeg argumenta tipa int. Ukoliko se ovo polje ne
koristi, tada se podrazumijeva da će realni brojevi biti ispisano s maksimalno šest decimalnih
znamenki iza decimalne točke.

       Polje tip_argumenta može sadržavati samo jedan od sljedećih znakova:

c         Argument se tretira kao int koji se ispisuje kao znak iz ASCII skupa.
d, i      Argument se tretira kao int, a ispisuje se decimalnim znamenkama.
e, E      Argument je float ili double, a ispis je u eksponentom formatu.
F         Argument je float ili double, a ispis je prostom decimalnom formatu. Ako je prefiks # i
          preciznost .0, tada se ne ispisuje decimalna točka.
g, G      Argument je float ili double, a ispis je prostom decimalnom formatu ili u
          eksponencijalnom formatu, ovisno o tome koji daje precizniji ispis u istoj širini ispisa.
o         Argument je unsigned int, a ispisuje se oktalnim znamenkama.
p         Argument se tretira kao pokazivač tipa void *, pa se na ovaj način može ispisati adresa bilo
          koje varijable. Adresa se obično ispisuje kao heksadecimalni broj.
s         Argument mora biti literalni string odnosno pokazivač tipa char *.
u         Argument je unsigned int, a ispisuje se decimalnim znamenkama.
x, X      Argument je unsigned int, a ispisuje se heksadecimalnim znamenkama. Ako se zada
          prefiks # , ispred heksadecimalnih znamenki se ispisuje 0x ili 0X.


    Polje veličina_tipa može sadržavati samo jedan znak koji se upisuje neposredno ispred
oznake tipa.

h         Pripadni argument tipa int tretira se kao short int ili unsigned short int.
l         Pripadni argument je long int ili unsigned long int.
L         Pripadni argument realnog tipa je long double.

Primjeri korištenja printf() funkcije dani su u programu printf.c.

        /* Datoteka: printf.c                 */
        /* Primjer korištenja printf funkcije */

        #include<stdio.h>

        int main()
        {




                                                                                                   48
printf("%d, %5d, %-5d, %05d, %5.5dn", 1, 2, 3, 4, 5);
            printf("%o %x %X %#o %#xn", 171, 171, 171, 171, 171);
            printf("%f %e %gn", 3.14, 3.14, 3.14);
            printf("%s, %.5s!n", "Hello", "worldly");
            printf("%0*d, %.*f, %*.*sn", 2, 3, 4, 5.6, 7, 3, "abcdef");
            return 0;
      }


Ovaj pogram daje ispis:

      1,     2, 3    , 00004, 00005
      253 ab AB 0253 0xab
      3.140000 3.140000e+000 3.14
      Hello, world!
      03, 5.6000,     abc

    U prethodnim tablicama za oznake tipa argumenata 'p' i 's' naglašeno je da su argumenti
funkcije pokazivači. Što je to pokazivač, bit će objašnjeno u slijedećem odjeljku.


4.6 Pristup podacima pomoću pokazivača
    Jedan od glavnih razloga zašto se C jezik smatra jezikom niske razine je taj što omogućuje
korištenje numeričke vrijednosti adresa varijabli i funkcija za indirektno manipuliranje s
podacima i izvršenjem programa. U tu svrhu koriste se posebni operatori - adresni operator & i
operator indirekcije *, te specijalni tip varijabli koje se nazivaju pokazivačke varijable ili
pokazivači (eng. pointer).
Adresni operator &
     Prije je naglašeno da ime varijable ujedno označava i adresu varijable. Koja je to adresa?
Brigu o tome vodi kompilator. Adresa varijable se može odrediti pomoću posebnog operatora &
koji se naziva adresni operator. Koristi se kao unarni operator koji se zapisuje ispred imena
varijable. Pogledajmo primjer programa adresa.c.

      /* Datoteka: adresa.c              */
      /* Primjer ispisa adrese varijable */

      #include<stdio.h>

      int main()
      {
          int y;
          y = 777;

           printf("n Vrijednost varijable je %d", y);
           printf("n Adresa varijable je %#p", &y);

           return 0;
      }


Ispis programa može izgledati ovako:

      Vrijednost varijable y je 777
      Adresa varijable y je 0x0063FDF4




                                                                                            49
Za ispis adrese varijable korištena je naredba

      printf("n Adresa varijable je               %#p", &y);

    Uočimo da je ispred imena varijable zapisan adresni operator &. Ispis adrese je izvršen u
heksadecimalnim obliku s osam znamenki, jer je korišteno 32-bitno računalo na kojem je adresa
određena 32-bitnim kardinalnim brojem.
Napomena: pri ponovljenom izvršenju programa ispis adrese ne mora biti isti jer operativni
sustav ne učitava program uvijek na isto mjesto u memoriji (time se mijenja i adresa na kojoj se
nalazi varijabla x).
Operator indirekcije *
    Komplementarno adresnom operatoru koristi se unarni operator indirekcije koji se
označava znakom *. Zapisuje se ispred izraza čija vrijednost predstavlja neku adresu. Značaj
operatora indirekcije je u tome da se pomoću njega dobije vrijednost koja je upisana na toj
adresi. To znači da
      ako je y jednako 777, onda je *(&y) također jednako 777
jer operator indirekcije daje vrijednost koja se nalazi na adresi varijable y. Ovo pravilo se može
provjeriti tako da se u prethodnom programu napiše naredba:

      printf("n Vrijednost dobivena indirekcijom je %d", *(&y) );

Tada se dobije ispis:

       Vrijednost varijable je 777
       Adresa varijable je 0X0063FDF4
       Vrijednost dobivena indirekcijom je 777

Napomena: izraz *(&y) se može pisati i bez zagrada *&y, međutim , obično se pišu zagrade
zbog lakšeg uočavanja redoslijeda djelovanja operatora.

Pokazivači
     Varijable kojima je vrijednost adresa neke druge varijable ili funkcije nazivaju se
pokazivači ili pointeri. Pokazivači moraju biti deklarirani, jer C kompilator mora znati kakav će
tip podatka biti na adresi koju oni sadrže, ili kako se češće kaže, mora biti poznat tip objekta na
kojeg oni pokazuju.
     Deklaracija pokazivača vrši se slično deklaraciji varijable, s razlikom što se između oznake
tipa i imena pokazivača obvezno zapisuje znak indirekcije '*'. Primjerice,

      int *p;               /* p je pokazivač na objekt tipa int */
      unsigned *q;          /* q je pokazivač na objekt tipa unsigned */

    Ovim deklaracijama definirane su dvije pokazivačke varijable. Njihova vrijednost je
neodređena, jer im nije pridijeljena adrese nekog realnog memorijskog objekta. Važno je znati
da pokazivače prije upotrebe treba inicijalizirati, odnosno mora im se pridijeliti vrijednost
adrese postojećeg memorijskog objekta. Pri tome, tip pokazivača mora biti jednak tipu
memorijskog objekta. To se ostvaruje adresnim operatorom '&'. Primjerice, u programu

      int    suma;          /*   deklaracija varijable suma */
      int    *p;            /*   deklaracija pokazivača na objekt tipa int */
      sum    = 777;         /*   inicijalizacija varijable suma */
      p =    &suma;         /*   p inicijaliziran na adresu varijable suma */




                                                                                                50
najprije je izvršena deklaracija varijable suma i pokazivača p. Zatim je varijabli suma
pridijeljena vrijednost 777, a pokazivač p je inicijaliziran da pokazuje na tu varijablu. Ako bi
dalje koristili naredbe

      printf ("%d", suma);
      printf ("%d", *p);

dobili bi isti ispis, jer se indirekcijom pokazivača dobiva vrijednost na koju on pokazuje, a to je
vrijednost varijable suma (*p ⇔*&suma).

                       memorijska              sadržaj                 ime
                         adresa               memorije             varijable
                         0x09A8                 ......               ......
                         0x09AC                   777                 suma
                         0x09B0                 ......               ......
                         ......                 ......               ......
                         ......                 ......               ......
                         ......                 ......               ......
                         0x1000                 0x09AC                  p
                         0x1004                 ......               ......
         Slika 4.3. Prikaz memorijskog sadržaja i adresa varijable suma i pokazivača p

Stanje u memoriji prikazano je na slici 4.3. Strelicom je označeno da je vrijednost pokazivača p
jednaka adresi varijable suma. Vrijednost adresa je napisana proizvoljno, jer se ne može
unaprijed znati na kojoj će se adresi nalaziti podaci. Zbog toga se češće za opis operacija s
pokazivačima koristi sljedeća simbolika. Varijabla se označava kao pravokutni objekt. Unutar
pravokutnika upisuje se vrijednost varijable (ako je vrijednost neodređena upisuje se znak ?), a
ime varijable se upisuje uz pravokutnik. Sadržaj pokazivačke varijable se označava strelicom
koja pokazuje na neki drugi objekt. U skladu s ovakvom simbolikom prije opisane operacije se
mogu predstaviti slikom 4.4.




                     Slika 4.4. Prikaz operacija s pokazivačem na varijablu

Važna osobina indirekcije pokazivača je da se može koristiti u svim izrazima u kojima se
koriste i obične varijable. Primjerice, naredbom

      *p = 678;




                                                                                                51
je iskazano da se vrijednost 678 pridijeli memorijskom objektu na kojeg pokazuje pokazivač p.
Pošto je prije određeno da je taj objekt varijabla suma, onda ovaj izraz ima isti efekt kao da je
korištena naredba:

      suma = 678;

    Ovime je demonstriran glavni razlog korištenja pokazivača, a to je da se pomoću
pokazivača može manipulirati sa svim memorijskim objektima.
    U slučaju kada se pokazivač koristi samo za bilježenje adresa i kad nije predviđeno da se
koristi u izrazima on se može deklarirati posebnom oznakom tipa

      void *p;

što znači da je deklariran pokazivač koji pokazuje na “bilo što”, ili da je deklariran “netipski”
pokazivač. Riječ void se u C jeziku koristi s značenjem kojeg je teško prevesti na hrvatski, jer
sama riječ void ima višestruko značenje – prazan , slobodan i nevažeći.
    Često se koristi naziv nul pokazivač za pokazivače kojima je vrijednost jednak nuli.
Njihova upotreba je vrlo opasna jer pokazuju na početni dio memorije, odnosno na područje
memorije gdje nije smješten korisnički program, već programi ili podaci operativnog sustava.
Stoga nije čudo da programi u kojima se greškom koristi nul pokazivači mogu potpuno
“uništiti” operativno stanje računala.

    Iz prethodnog izlaganja očito je da su pokazivači varijable, jer im se sadržaj može mijenjati.
To je razlog da se u C jeziku češće koristi pojam memorijski objekt nego varijabla za objekte
kojima se može mijenjati sadržaj. Memorijskom se objektu može pristupiti pomoću imena ili
posredno pomoću pokazivača.


    Imena označavaju fiksni položaj u memorija i često se kaže da predstavljaju referencu
    memorijskog objekta ili referencu. U skladu s ovim nazivljem, kada se ispred
    pokazivača primijeni operator indirekcije, kaže se da je to dereferencirani pokazivač.


    Rad s pokazivačima predstavlja važan element programiranja C jezikom. Slučajevi kada se
koriste, i zašto se koriste, biti će objašnjeni tek kada se upoznaju temeljni elementi
programiranja C jezikom.


4.7 Unos podataka u memoriju računala
    Do sada smo razmatrali kako su podaci smješteni u računalu i kako se njihova vrijednost
može predočiti korisniku programa pomoću printf() funkcije. Sada će biti pokazano kako se
podaci mogu unijeti u memoriju računala, u slučaju kada se unos podataka vrši pomoću
tipkovnice. U tu svrhu se može koristiti funkcija scanf().
Funkciju scanf() ćemo koristiti u obliku:

      scanf(format_unosa, lista_adresnih_izraza);

format_unosa je literalni string u kojem se zadaju specifikatori tipa objekta čija se vrijednost
                unosi. Oni su gotovo identični specifikatorima ispisa kod printf() funkcije.
lista_adresnih_izraza je niz izraza odvojenih zarezom, čija vrijednost predstavlja adresu
                postojećeg memorijskog objekta. Tip objekta mora odgovarati specifikaciji tipa
                prethodno zapisanog u formatu_unosa.




                                                                                               52
Jednostavni programi u kojima ćemo koristiti funkciju scanf() imat će sljedeći oblik:

      /* Datoteka: scanf1.c */
      /* Obrazac unosa podataka pomoću scanf() funkcije*/
      #include <stdio.h>

      int main(void)
      {
      /* 1. definiraj varijablu čiju vrijednost će unositi korisnik */
      int unos;

      /* 2. ispiši poruku korisniku da se program očekuje unos : */
      printf("Molim otipkajte jedan cijeli broj >");

      /* 3. Pozovi funkciju scan s agumentom koji je adresa varijabe*/
      scanf("%d", &unos);

      /* 4. obavi radnje s tom varijablom ......*/

      /* 5. ispiši rezultat obrada*/
         printf("nOtkucali ste broj %d.n", unos);

          return 0;
      }

Kada se pokrene, ovaj program ispisuje poruku:
      c:> Molim otipkajte jedan cijeli broj >_

i čeka da korisnik otkuca jedan broj. Unos završava kada korisnik pritisne tipku <Enter>.
Primjerice, ako korisnik otipka 12345<Enter>, program će završiti s porukom:
      Otkucali ste broj 12345.

Važno je zapamtiti da argumenti funkcije scanf() moraju biti izrazi čija je vrijednost adresa.
U prethodnom primjeru to je adresa varijable unos. Adresa se dobije primjenom adresnog
operatora & na varijablu unos.
    Obično se u format_unosa ne upisuje nikakvi dodatni tekst, kao što je bio slučaj kod
printf() funkcije, iako je to dozvoljeno. Razlog tome je činjenica da se tada od korisnika
očekuje da otipka i taj dodatni tekst. Primjerice, ako bi se koristila naredba

      scanf("Broj=%d", &unos);

i ako se želi unijeti vrijednost 25, onda korisnik mora otipkati "Broj=25<Enter>". Ako bi
otkucao samo broj 25, funkcija scanf() ne bi dala ispravan rezultat.
    Pomoću scanf() funkcije može se odjednom unijeti više vrijednosti, primjerice unos
jednog cijelog broja, jednog realnog broja i jednog znaka, može se ostvariti samo s jednim
pozivom funkcije scanf();

      int i;
      double x;
      char c;
      ....
      scanf("%d%f%c", &i, &x, &c);

Pri unosu cijelih i realnih brojeva, funkcijom scanf(), podrazumijeva se da unos broja
završava tzv. “bijelim” znakovima (razmak, tab, nova linija). Svi bijeli znakovi uneseni ispred




                                                                                            53
broja se odbacuju. To nije slučaj kada se unosi znak, jer i bijeli znakovi predstavljaju znak.
Stoga, pri unosu znaka potrebno je, u formatu zapisa, eksplicitno zadati razmak od prethodnog
unosa.
     Razmotrimo prijašnji primjer i pretpostavimo da korisnik želi unijet cijeli broj 67, realni
broj 3.14 i znak 'Z'. Prema zadanom formatu on bi morao otkucati:

      86 3.14Z

dakle, znak bi trebalo otipkati bez razmaka od prethodnog broja. Problem se može riješiti tako
da se u format upisa unese razmak:

      scanf("%d%f% %c", &i, &x, &c);

iako i ovo može stvarati probleme u komuniciranju s korisnikom, jer se smije koristiti samo
jedan razmak. Primjerice ako bi korisnik otipkao:

        86 3.14      Z

ne bi bio unesen znak 'Z' već znak razmaka, jer su ispred znaka 'Z' dva mjesta razmaka.
    Navedeni problemi su razlog da programeri rijetko koriste scanf() funkciju za
komuniciranje s korisnikom. Kasnije će biti pokazano da je za unos znakova pogodnije koristiti
neke druge funkcije. Također, bit će pokazano kako dijagnosticirati da li je izvršen unos koji
odgovara zadanom tipu varijable.


4.8 Inicijalizacija varijabli
     Temeljni uvjet korištenja neke varijable je da ona prethodno mora biti deklarirana. Samom
deklaracijom nije određeno i početna (inicijalna) vrijednost varijable, pa prije korištenja
varijable u izrazima, treba joj pridijeliti neku početnu vrijednost. Primjerice, u dijelu programa

      int main()
      {
          int y, x;              /* deklaracija varijabli x i y    */
          x = 77;                /* početna vrijednost varijable x */
          y = x + 7;             /* početna vrijednost varijable y */
          ...

koriste se dvije varijable: x i y. Početno je varijabli x pridijeljena vrijednost 77, i pomoću nje je
određena početna vrijednost varijable y. Kada ne bi bila određena vrijednost od x, program bi se
kompilirao bez dojave pogreške, ali tada bi pri izvršenju programa bila neodređena vrijednost
varijable y.
     Određivanje početnih vrijednosti varijabli važan je element programiranja. Prilikom izrade
većih programa, ukoliko se koriste neinicijalizirane varijable, mogu nastati greške koje je teško
otkriti. U C jeziku se početna vrijednost varijable može odrediti i u samoj deklaraciji.
Primjerice, prethodni program se može napisati u obliku:

      int main()
      {
          int y, x = 77;            /* deklaracija varijabli x i y          */
                                    /* i inicijalizacija x na vrijednost 77 */
            y = x + 7;              /* početna vrijednost varijable y       */

    Inicijalizacija varijable je deklaracija u kojoj se određuje početna vrijednost varijable.




                                                                                                  54
5 Uvod u programiranje C jezikom


Naglasci:
    •    postupak izrade programa
    •    algoritamska struktura programa u C jeziku
    •    složena naredba, if-else selekcija i while-petlja
    •    standardne i korisničke funkcije
    •    definiranje prototipa funkcije
    •    zaglavlje i tijelo funkcije
    •    formalni i stvarni argumenti funkcije
    •    razvoj jednostavnih algoritama


       U prethodnom je poglavlju opisano nekoliko jednostavnih C programa. Cilj je bio
upoznati standardne tipove podataka i jednostavne postupke komuniciranja s korisnikom u
dobavi i ispisu podataka. Sada će biti opisana algoritamska i funkcionalna struktura C programa
te postupci izrade jednostavnih programa.


5.1 Postupak izrade programa
Izrada se programa može opisati kao aktivnost koja se odvija u četiri temeljna koraka:

        1.   Definiranje zadatka i analiza problema.
        2.   Izrada detaljne specifikacije i uputa za rješenje problema.
        3.   Pisanje programa, dokumentacije i formiranje izvršnog programa.
        4.   Testiranje programa.
Programer treba znati:
   • mogućnosti programskog jezika,
   • kako obraditi problem:
           o definiranje objekata obrade (podaci),
           o definiranje postupaka obrade (apstraktni i programski algoritmi),
           o definiranje korisničkog sučelja za unos podataka i prezentiranje rezultata
               obrade.
   • kako metodološki pristupiti razradi programa (strukturalno programiranje, modularno
       programiranje, objektno orijentirano programiranje),
   • kako optimalno iskoristiti računarske resurse i mogućnosti operativnog sustava
       računala,
   • koje softverske alate koristiti za razvoj programa.

Većina od ovih pitanja bit će obrađena u narednim poglavljima.

     Postupak izrade manjih programa se može prikazati i dijagramom toka na slici 5.1.
(korišteni su standardnim elementi za opis dijagrama toka, a opisani su u Dodatku 1).




                                                                                            55
Slika 5.1. Postupak izrade manjih programa
Bitno je uočiti:
    o Izradi programa prethodi analiza problema i izrada algoritama za rješenja problema
    o Tijekom pisanja programa često je potrebno ispravljati sintaktičke pogreške.
    o Ukoliko se program ne izvršava u potpunosti, moguće je postojanje pogreške u
        korištenju računarskih resursa (pr. u korištenju memorije). Postojanje takovih pogrešaka
        se ispituje posebnim programima – dibagerima (eng. debugger).
    o Postupak programiranja ne može biti završen ako program pri izvršavanju iskazuje
        nelogične rezultate. Tada ne preostaje ništa drugo nego da se krene od početka i da se
        ponovo kritički sagleda zadatak programiranja.

Postupci izrade velikih programa, koji obrađuju kompleksne sustave, ovdje neći biti razmatrani.




                                                                                             56
5.2 Algoritamska struktura C programa?
     U uvodnom su poglavlju opisani temeljni oblici zapisa programskih algoritma. Oni se
sastoje od naredbi koje se izvršavaju jedna za drugom (sekvence), od naredbi selekcije i
iterativnih naredbi (petlji). Kroz niz primjera bit će pokazano kako se ove naredbe zapisuju u C
jeziku.
     Posebnu pažnju posvetit će se problemu koji se obrađuje. Prvo će se definirati zadatak, a
zatim će se vršiti analiza problema. Moguće rješenje iskazat će se podesnim algoritmom. Zatim
će biti pokazano kako se izvršenje tog algoritam može ostvariti programom napisanim u C
jeziku. Na kraju će se analizirati napisani program i rezultati koje on iskazuje tijekom svog
izvršenja.
Zadatak: Napisati program kojim se računa vrijednost od 5! (čitaj: pet faktorijela).

Analiza problema: Vrijednost n! u matematici naziva n-faktorijela, a definirana je formulom:

           ⎧1     za n = 0
           ⎪ n
      n! = ⎨
           ⎪∏ k za n > 0
           ⎩ k =1

Ovu se formulu može opisati i sljedećim zapisom:

      n! je jednako 1 ako je n=0, a za vrijednosti n>0, n! je jednako 1*2*3*..*n

Rješenje: Trivijalno rješenje problema dano je u programu fact0.c. Najprije je deklarirana
cjelobrojna varijabla nfac. Zatim je toj varijabli pridijeljena vrijednost umnoška konstanti
1*2*3*4*5, što odgovara vrijednosti 5!. Za ispis te vrijednosti korištena je standardna funkcija
printf().

      /* Datoteka fact0.c - Proračun 5! */

      #include <stdio.h>

      int main()
      {
        int nfact;
        nfact = 1 * 2 * 3 * 4 * 5;
        printf("Vrijednost 5! iznosi: %dn", nfact);
        return 0;
      }

Nakon izvršenja programa dobije se ispis:

      Vrijednost 5! iznosi: 120

Pošto argument funkcije printf() može biti bilo koji izraz koji rezultira nekom vrijednošću,
prethodni se program može napisati i u obliku:

      /* Datoteka fact01.c */
      /* Proračun 5! unutar argumenta funkcije printf() */
      #include <stdio.h>
      int main()
      {
         printf("Vrijednost 5! iznosi: %dn", 2 * 3 * 4 * 5);
         return 0;




                                                                                             57
}


    Oba, prethodno napisana programa nisu od neke koristi, jer se njima računa nešto što
čovjek može napamet puno brže riješiti.

5.2.1 Naredba iteracije – while petlja
    Cilj pisanja programa je poopćenje procesa obrade nekog problema na način da se dobije
rezultat za različite vrijednosti ulaznih podatka. U tu svrhu definiran je sljedeći zadatak:
Zadatak: Napisati program kojim se računa vrijednost od n!. Vrijednost n zadaje korisnik.
Program mora obaviti sljedeće operacije:

    1. Dobaviti vrijednost od n.
    2. Izračunati vrijednost n!.
    3. Ispisati vrijednost od n i n!.
    Postavlja se pitanje kako realizirati korak 2 ovog algoritma. Problem je u tome što se
unaprijed ne zna vrijednost od n, jer tu vrijednost unosi korisnik programa.
Analiza problema: Polazi se od definicije n-faktorijela

      n! = 1                             za n = 0
      n! = 1*2 * ..(n-2)*(n-1)*n         za n > 0

Lako je uočiti da vrijedi i sljedeće pravilo:

      n! = 1 za n=0
      n! = n * (n-1)! za n>0

koje kazuje da se vrijednost od n! može izračunati iz prethodno poznate vrijednosti od (n-1)!.
Koristeći ovu formulu, prijašnji problem proračuna 5! bi se mogao programski riješiti
uvođenjem pomoćne cjelobrojne varijable k i sljedećim nizom naredbi:

                                                /*   stanje nakon izvršenja naredbi */
      k   =   0; nfact = 1;                     /*   k jednak nuli, nfact jednak 1 */
      k   =   k+1; nfact = k      *   nfact;    /*   k jednak 2, nfact jednak 2   */
      k   =   k+1; nfact = k      *   nfact;    /*   k jednak 3, nfact jednak 6*/
      k   =   k+1; nfact = k      *   nfact;    /*   k jednak 4, nfact jednak 24*/
      k   =   k+1; nfact = k      *   nfact;    /*   k jednak 5, nfact jednak 120*/

    Ovaj primjer pokazuje vrlo neefikasan način proračuna 5!, međutim, značajan je jer ukazuje
da se do rezultata dolazi ponavljanjem istih naredbi. U ovom se slučaju naredba

      k=k+1; nfact=k*nfact;

ponavlja sve dok je vrijednost varijable k manja od 5, pa se može napisati algoritamsko rješenje
u obliku iterativne petlje:

      1. k = 0; nfact = 1;
      2. dok je k<5 ponavljaj
             k = k+1;
             nfact = k * nfact;

U C jeziku se ovaj tip petlje zapisuje iskazom koji se zove while-petlja:




                                                                                             58
k = 0; nfact = 1;
      while (k < 5)                  /* zaglavlje petlje */
      {
         k = k+1;                    /* tijelo petlje */
         nfact = k * nfact;
      }

Općenito while-petlja ima oblik:
      while (izraz)
      {
       niz_naredbi                     ili         while (izraz) naredba

      }

a ima značenje: dok je (eng. while) izraz u zaglavlju petlje različit od nule izvršava se
niz_naredbi tijela petlje koje su napisane unutar vitičastih zagrada. Ako je izraz jednak nuli
izvršenje programa se nastavlja operacijom koja je definirana naredbom koja slijedi iza tijela
petlje. U slučaju kada se u tijelu petlje navodi samo jedna naredba, tada nije nužno pisati
vitičaste zagrade.
     Izraz može biti bilo koji numerički ili relacijski izraz, a tretira se kao logički uvjet za
izvršenje naredbi koje su obuhvaćene tijelom petlje. U prethodnom primjeru izraz ima oblik
relacijskog izraza k < 5. Taj izraz u C jeziku može imati samo dvije vrijednosti: 1 ili 0, što je
ekvivalentno logičkim vrijednostima istina ili laž. Naredbe tijela petlje će se ponavljati za
vrijednosti k=1,2,3,4, jer je za te vrijednosti relacijski izraz k<5 istinit, odnosno njegova
numerička vrijednost je različita od nule.

     Pravu korist korištenja while-petlje spoznaje se tek kada ona primijeni za računanje
vrijednosti n!, gdje je n vrijednost koju zadaje korisnik programa. Rješenje je jednostavno: u
zaglavlju while-petlje, umjeto izraza k<5, dovoljno je uvrstiti izraz k<n, pa za realizaciju
koraka 2 vrijedi algoritam:
      2. Izračunati vrijednost n!.
            2.1. Postavi nfact = 1;
            2.2. Postavi k=0;
            2.3. Dok je k < n ponavljaj
                   Uvećaj vrijednost varijable k za jedan
                   nfact = k * nfact;

Dorada koraka 2:
Analiziram prethodnog algoritama može se uočiti da je vrijednost nfact jednaka jedinici ne
samo kada je n=0, već i u slučaju kada je n=1. Zbog toga se kao početna vrijednost varijable k
može uzeti jedinica. Vrijedi algoritam:

      2. Izračunati vrijednost n!.
            2.1 Postavi nfact = 1;
            2.3 Postavi k=1;
            2.3 Dok je k < n ponavljaj
                  Uvećaj vrijednost varijable k za jedan
                  nfact = k * nfact;

Sada se može napisati program "fact1.c", kojim se implementira prethodno opisani algoritam.




                                                                                              59
/* Datoteka: fact1.c */
      /* Proračun n!. Vrijednost od n unosi korisnik. */
      #include <stdio.h>

      int main()
      {
         int n, k, nfact;                 /* deklaracija potrebnih varijabli*/

          /* korak 1*/
          scanf("%d", &n);

          /* korak 2 */
          nfact = 1;                   /* korak                2.1     */
          k = 1;                       /* korak                2.2     */
          while ( k < n) {             /* korak                2.3     */
              k = k + 1;
              nfact = k * nfact;
          }
          /* korak 3 */
          printf("Vrijednost %d! iznosi: %dn",                n, nfact);
          return 0;
      }


Unutar programa komentarima je označen pojedini korak algoritma.

Testiranje programa fact1:
Nakon izvršenja ovog programa, na ekranu se dobije prikaz

      c:>_

Program čeka da korisnik unese neku vrijednost za n. Ako unese vrijednost 5, dobije se ispis

      Vrijednost 5! iznosi: 120

Ako korisnik unese vrijednost 13 dobije se rezultat:

      Vrijednost 13! iznosi: 1932053504

Ako korisnik unese vrijednost 18 dobije se rezultat:

      Vrijednost 18! iznosi: -898433024

Ovaj posljednji rezultat je pogrešan, jer vrijednost od 18! nadmašuje maksimalnu vrijednost
koja se može kodirati kao cijeli broj u memoriji veličine 4 bajta (tj. 2147483647). Može se
zaključiti da je maksimalna vrijednost koja se može izračunati jednaka 13!.

Ako korisnik otkuca negativni broj, primjerice broj -3, program će ispisati:

      Vrijednost -3! iznosi: 1

Ovaj rezultat nema nikakvog smisla jer funkcija n-faktorijela nije definirana za negativne
brojeve.




                                                                                               60
5.2.2 Uvjetna naredba – if naredba
     Nakon provedenog testiranja, pokazala se potreba za doradom prvog koraka algoritma
sljedećim operacijama:
Dorada koraka 1:
     1. Dobaviti vrijednost od n.
     1.1. Upozoriti korisnika da se očekuje unos broja unutar intervala [0,13]
     1.2. Dobaviti otipkanu vrijednost u varijablu n
     1.3 Ako je n < 0 ili n > 13 tada izvršiti sljedeće:
                   izvijestiti korisnika da je otkucao nedozvoljeni broj
                   prekinuti izvršenje programa

Kako implementirati ove korake u C jeziku? Korake 1.1 i 1.2 može se zapisati naredbama

      printf("Unesite broj unutar intervala [0,13]n");
      scanf("%d", &n);

    Za implementaciju koraka 1.3 potrebno je upoznati kako se u C jeziku zapisuje uvjetna
naredba tzv. if-naredba. Njen opći oblik glasi:

      if (izraz)
      {
         niz_naredbi                   ili          if (izraz) naredba;
      }

a značenje ove naredbe je: ako je (eng. if) izraz različit od nule izvršava se niz_naredbi koji je
omeđen vitičastim zagradama, u protivnom izvršit će se naredba koja slijedi iza if-naredbe.

Predikatni izraz, na temelju kojeg se u algoritamskom zapisu vrši selekcija, glasi:

      n < 0 ili n > 13.

U C jeziku se logički operator “ili” zapisuje s dvije okomite crte ||, pa prethodni izraz u C
jeziku ima oblik

      n < 0 || n > 13

(Napomena: logički operator “i” se zapisuje s &&, a logička negacija znakom ! ispred logičkog
izraza).

Sada se korak 1.3 može napisati u obliku:

      if((n < 0) || (n > 13))
      {
          printf("Otipkali ste nedozvoljenu vrijednost");
          return 1; /* forsirani izlaz iz funkcije main */
      }

pa kompletni program izgleda ovako:

      /* Datoteka fact2.c */
      /* Proračun n!. Vrijednost od n unosi korisnik. */
      /* Vrijednost od n mora biti unutar intervala [0,13]*/

      #include <stdio.h>

      int main()




                                                                                               61
{
          int n, k, nfact;

          printf("Unesite broj unutar intervala [0,13]n");
          scanf("%d", &n);

          if((n < 0) || (n > 13)) {
             printf("Otipkali ste nedozvoljenu vrijednost");
             return 1;         /* forsirani izlaz iz funkcije main */
          }

          nfact = 1;
          k = 1;

          while ( k < n) {
              k = k + 1;
              nfact = k * nfact;
          }

          printf("Vrijednost %d! iznosi: %dn", n, nfact);
          return 0;
      }


    Konačno je ostvaren kvalitetan i robustan program. On za bilo koju ulaznu vrijednost daje
rezultat nakon konačnog broja operacija. Ovo svojstvo se smatra temeljnim uvjetom koji mora
zadovoljiti svaki programski algoritam.

5.2.3 Naredba selekcije: if-else naredba
    Radi vježbe i upoznavanja još jednog programskog iskaza – if-else naredbe, prethodni
algoritam se može zapisati u ekvivalentnom obliku:
      Dobavi vrijednost od n.
      Ako je n >= 0 i n<=13 tada
          Izračunaj vrijednost n!.
            Ispiši vrijednost od n i n!.
      inače
            Izvijesti o pogrešnom unosu
      Kraj!

Tijek programa se sada kontrolira naredbom selekcije, koja ima značenje:
      ako je logički uvjet istinit tada
             izvrši prvi niz naredbi
      inače
             izvrši alternativni niz naredbi

U C jeziku se ovaj tip naredbe zove if-else naredba ili if-else iskaz, a zapisuje se prema obrascu:

      if(izraz)
      {
         niz_naredbi1                     ili        if(izraz)
      }                                                 naredba1;
      else                                          else
      {                                                  naredba2;
         niz_naredbi2
      }




                                                                                                62
Značenje ove naredbe u je: ako je (eng. if) izraz različit od nule izvršava se niz_naredbi1,
inače (eng. else) izvršava se niz_naredbi2. Ako niz_naredbi sadrži samo jednu naredbu ne
moraju se pisati vitičaste zagrade. Izraz se tretira kao logička vrijednost.
     U ovom primjeru proračun n! će se izvršiti samo ako su istovremeno zadovoljena dva
uvjeta: n>=0 i n<=13. Ovaj se uvjet u C jeziku zapisuje s dva relacijska izraza povezana
logičkim operatorom “i”, koji se označava s &&.
Program sada izgleda ovako:

      /* Datoteka fact3.c */
      /* Proračun n!. Vrijednost od n unosi korisnik. */
      /* Vrijednost od n mora biti unutar intervala [0,13]*/

      #include <stdio.h>

      int main()
      {
         int n, k, nfact;

          printf("Unesite broj unutar intervala [0,13]n");
          scanf("%d", &n);

          if((n >= 0) && (n <= 13))   {
                nfact = 1;
            k = 1;
              while ( k < n) {
                  k = k + 1;
                  nfact = k * nfact;
              }
              printf("Vrijednost %d! iznosi: %dn", n, nfact);
          }
          else
              printf("Otipkali ste nedozvoljenu vrijednost");
          return 0;
      }



5.3 Funkcije C jezika
    U prethodnoj su sekciji opisani temeljni iskazi kontrole izvršenja C programa, te kako se
oni koriste u implementaciji programskih algoritama. Čitav se program izvršavao unutar jedne
funkcije – main(). Unutar te funkcije korištene su standardne funkcije print() i scanf(),
iako nije poznato kako su te funkcije implementirane. Korištene su zbog toga jer su poznata
pravila njihove upotrebe i efekti koje one uzrokuju.

5.3.1 Korištenje funkcija iz standardne biblioteke C jezika
     Funkcije se u programiranju koriste slično načinu kako se koriste funkcije u matematici.
Kada se u matematici napiše y=sin(x), x predstavlja argument funkcije, a ime funkcije sin
označava pravilo po kojem se skup vrijednosti, kojem pripada argument x, pretvara u skup
vrijednosti koje može poprimiti y.
     Funkcija sin() se može koristiti i u izrazima C-jezika jer je implementirana u standardnoj
biblioteci funkcija. Primjerice, dio programa, u kojem se ona koristi, može biti sljedećeg
oblika:

      #include <math.h>
      int main()




                                                                                              63
{
            double x,y;
            .........
            x = sin(5.6);
            y = sin(x)+4.6;
            .....
      }

    Prvo je napisana leksička direktiva da se u proces kompiliranja uključi datoteka "math.h" u
kojoj je specificiran prototip (ili deklaracija) funkcije sin(). Pregledom te datoteke može se
pronaći specifikacija prototipa funkcije sin() oblika:

      double sin(double);

Prototip iskazuje da argument funkcije mora biti vrijednost tipa double i da funkcija u izraze
vraća vrijednost tipa double.
     Općenito, funkcija može imati više argumenata. Oni se navode u zagradama iza imena
funkcije i odvajaju zarezom. Tip vrijednosti kojim rezultira izvršenje funkcije uvijek se navodi
ispred imena funkcije. Deklaracija prototipa završava znakom točka-zarez.
     Ime argumenta funkcije nije navedeno već samo tip argumenta. Ime argumenta može biti i
napisano (primjerice, double sin(double x) ), međutim, u prototipu ono nema nikakvi
značaj jer deklaracija prototipa služi kompilatoru jedino kao pokazatelj s kojim tipovima
vrijednosti će se koristiti funkcija.
     Važno je zapamtiti da C funkcije “uzimaju” vrijednost svojih argumenata za proračun
novih vrijednosti ili za ostvarenje nekog drugog procesa. Argument funkcije može biti bilo koji
izraz koji rezultira tipom vrijednosti koji je deklariran u prototipu funkcije. Primjerice,
vrijednost argumenta u naredbi x=sin(5.6) je vrijednost konstante 5.6, a u naredbi
y=sin(x)+4.6 stvarni argument funkcije je vrijednost varijable x.




                              Slika 5.2. Redoslijed poziva funkcije

       Uobičajeno se kaže da je u prethodnim naredbama izvršen poziv funkcije sin(), čime se
želi naglasiti da se za izvršenje te funkcije aktivira dio izvršnog koda u kojem se nalaze naredbe
koje realiziraju tu funkciju. Funkcija iz koje se poziva funkcija, naziva se pozivna funkcija, (u
ovom slučaju to je funkcija main()), a sama funkcija sin() se naziva pozvana funkcija.
       Simbolički, pozvanu funkciju možemo shvatiti kao “crnu kutiju” koja prima i vraća
vrijednost u pozivnu funkciju. Ta je simbolika ilustrirana na slici 5.2.
       Prethodni segment programa se može napisati u ekvivalentnom obliku:

      #include <math.h>
      .......
      y = sin(sin(5.6))+ 4.6;
      .......




                                                                                                64
prema pravilu da argumenti funkcije mogu biti i izrazi. Postavlja se pitanje: kojim redoslijedom
se izvršavaju operacije u navedenoj naredbi pridjele vrijednosti. U C jeziku vrijedi pravilo da se
pri proračunu izraza najprije računa vrijednost izraza koji se nalaze unutar zagrada. Stoga,
najprije će biti izračunata vrijednost funkcije sin(5.6), zatim će ta vrijednost biti upotrebljena
kao argument za ponovni poziv funkcije sin(). Konačno, dobivenoj će vrijednosti biti
pribrojena vrijednost konstante 4.6.
     Korisnik ne mora znati kako je napisan dio programa koji računa vrijednost funkcije sin()
jer se taj dio programa uključuje u izvršni kod direktno iz biblioteke kompiliranih funkcija.
Kako se to može napraviti i s funkcijama koje kreira korisnik bit će pokazano u sljedećoj
sekciji.

5.3.2 Korisnički definirane funkcije
    Sada će biti pokazano kako korisnik može definiranja neku funkciju i kako se ona uključuje
u korisnički program.

Pravilo je:
Definicija funkcije se sastoji od "zaglavlja" i "tijela" funkcije.

Zaglavlje funkcije je deklaracija u kojoj se redom navodi
      1. oznaka tipa koji funkcija vraća u izraze,
      2. ime funkcije,
      3. deklaracija liste parametara (formalnih argumenata) funkcije napisanih unutar
           zagrada.

Tijelo funkcije je složeni iskaz naredbi i deklaracija varijabli, koji definiraju implementaciju.
Piše se unutar vitičastih zagrada. Unutar tijela funkcije se pomoću ključne riječi return
označava izraz, čiju vrijednost funkcija vraća u pozivnu funkciju.

Primjerice, definicija funkcije kojom se računa kvadrat cjelobrojnog argumenta glasi

      int kvadrat(int y)
      {
           return y * y;
      }

Ključna riječ return označava mjesto na kojem se prekida izvršenje funkcije, na način da se
prethodno izračuna vrijednost izraza koji je napisan iza riječi return. Vrijednost tog izraza je
vrijednost koju funkcija vraća u izraz iz kojeg je pozvana.


   U definiciji funkcije mora se navesti ime argumenta s kojim će se izvršiti operacije unutar
   funkcije. To ime se naziva formalni argument ili parametar funkcije jer on ima značaj
   samo pri definiranju funkcije (pri pozivu funkcije kao stvarni argument koristi se
   vrijednost nekog izraza).


U sljedećem programu ilustrirana je definicija i upotreba funkcije kvadrat().




                                                                                                 65
Slika 5.3 Definiranje funkcije


   Složeni iskaz C jezika, koji je napisan unutar vitičastih zagrada, naziva se blok. Tijelo
   funkcije je blok C jezika. Unutar bloka se mogu koristiti svi tipovi iskaza C jezika
   uključujući deklaraciju varijabli i prototipova funkcija, jedino se ne smije vršiti
   definiranje neke druge funkcije. Deklaracije se moraju pisati neposredno na početku
   bloka (iza vitičastih zagrada).

Alternativno se funkcija kvadrat() može napisati u obliku:

      int kvadrat(int y)
      {
      int tmp;
      tmp = y*y;
           return tmp;
      }

     U ovom je slučaju najprije deklarirana varijabla tmp koja služi za privremeni smještaju
rezultata izraza y*y. Funkcija vraća vrijednost te varijable. Iako ova verzija funkcije
kvadrat() izgleda bitno drugačije od prve verzije, ne mora biti nikakve razlike u načinu kako
se stvarno izvršavaju ove funkcije Razlog tome je činjenica da kompilator sam generira tzv.
privremene varijable za smještaj rezultata aritmetičkih operacija.
     Optimizirajući kompilatori često za smještaj privremenih varijabli koriste registre
procesora, jer se njima najbrže pristupa. Iz ovog razloga u C jeziku je omogućeno da se pomoću
ključne riječi register, napisane ispred deklaracije cjelobrojne varijable,             sugerira
kompilatoru da za smještaj varijable koristi registre procesora. Primjerice, sljedeći oblik
funkcije

      int kvadrat(int y)
      {
      register int tmp; /* sugeriraj korištenje registra*/
      tmp = y*y;
           return tmp;
      }




                                                                                               66
je najbliži načinu kako optimizirajući kompilatori prevode prvi oblik funkcije kvadrat().
       Danas mnogi programeri smatraju da uopće ne treba koristiti ključnu riječ register, jer
moderni optimizirajući kompilatori mnogo efikasnije koriste procesorske registre, nego što to
može učiniti programer tijekom procesa programiranja.

5.3.2 “void” funkcije
     U programskim se jezicima često koristi dva tipa potprograma: funkcije i procedure.
Procedura je potprogram koji vrši neki proces, ali ne vraća nikakvu vrijednost. Pošto se u C-
jeziku svi potprogrami nazivaju funkcije, onda se kaže da je procedura funkcija koja vraća ništa
(eng. void). Primjerice, u trećem poglavlju korištena je funkcija void hello() za ispis
poruke "Hello World!".


   Pomoću ključne riječi void označava se da je tip vrijednosti koji funkcija vraća "ništa",
   odnosno da je nevažan. Poziv procedure se vrši njezinim imenom. Pošto procedure ne
   vraćaju nikakvu vrijednost, ne mogu se koristiti u izrazima. U proceduri se ne navodi
   ključna riječ return, iako se može koristiti (bez argumenta) ako se želi prekinuti
   izvršenje procedure prije izvršenja svih naredbi koje se pozivaju u proceduri.


5.3.3 Primjer: funkcija za proračun n!
     Za proračuna n-faktorijela zgodno je definirati funkciju koja obavlja taj proračun. Prototip
te funkcije može biti oblika:

      int factorial(int n);

Funkcija factorial() će kao argument koristiti vrijednost tipa int. Primjena ove funkcije u
izrazima rezultirat će vrijednošću tipa int koji predstavlja vrijednost n-faktorijela. Definicija i
primjena funkcije zapisani su u programu fact4.c

      /* Datoteka fact4.c */
      /* Proračun n! pomoću funkcije factorial(n) */
      /* Vrijednost od n mora biti unutar intervala [0,13]*/

      #include <stdio.h>

      /* definicija funkcije za proračun n faktorijela */

      int factorial(int n)
      {
         int k = 1, nfact = 1;

          while (k < n)
          {
             k = k + 1;
             nfact = k * nfact;
          }
          return nfact;
      }

      int main()
      {
         int n;

          printf("Unesite broj unutar intervala [0,13]n");




                                                                                                67
scanf("%d", &n);

          if((n < 0) || (n > 13))
                printf("Otipkali ste nedozvoljenu vrijednost");
          else
            printf("Vrijednost %d! iznosi: %dn", n, factorial(n));

          return 0;
      }



     Bitno je uočiti da se u glavnom programu više ne koriste varijable k i nfact. Te varijable
su deklarirane unutar funkcije factorial(), jer su one potrebne samo za vrijeme dok se
izvršava ta funkcija.
     U C jeziku vrijedi opće pravilo da sve varijable, koje se definiraju unutar bloka ili tijela
funkcije, zauzimaju memoriju samo dok se izvršava taj blok ili funkcija. Kada započne
izvršenje funkcije, skriveno od korisnika rezervira se dio memorije za te varijable, i to u dijelu
memorije koja se uobičajeno naziva stog (eng. stack). Nakon izvršenja funkcije, a prije nego se
nastavi izvršenje programa iz pozivne funkcije, ta se memorija ponovo smatra slobodnom za
korištenje. Ovo ujedno znači da se varijable, koje se deklariraju u nekoj funkciji, mogu koristiti
samo u toj funkciji. One se stoga po dosegu imena ili vidljivosti (eng. scope) nazivaju lokalne
varijable, a pošto im je vrijeme postojanja ograničeno na vrijeme u kojem se izvršavaju naredbe
funkcije, nazivaju se i automatske varijable.

5.3.4 Funkcija za proračun ex
Zadatak je napisati funkciju kojom se približno određuje vrijednost funkcije ex (e = 2. 718282) i
rezultat usporediti s vrijednošću koja se dobije pomoću standardne funkcije exp(), kojoj je
prototip - double exp(double x) - deklariran u datoteci "math.h".

Metod: Koristeći razvoj u red:        ex = 1 + x/1! + x2 /2! + x3 /3! + ..
     zbrajati članovi reda, za dati x, sve dok razlika od prethodnog rezultata ne bude manja od
     zadane preciznosti eps. Primjerice, za x = 1.0, i eps = 0.0001 trebat će zbrojiti 10
     članova reda.

Specifikacija funkcije: double my_exp(double x, double eps);

      Parametri:          x - vrijednost za koju se računa ex , tipa double
                          eps - zadana preciznost proračuna, tipa double

      Rezultat:           vrijednost tipa double, jednaka vrijednosti ex

Algoritam: Razvoj u red funkcije ex ima karakteristiku da se i-ti pribrojnik reda dobije tako da
se prethodni pribrojnik pomnoži s x/i. Koristeći tu činjenicu, može se primijeniti sljedeći
iterativni algoritam:

             unesi x i eps
             i =1, pribrojnik = 1;
             ex = pribrojnik, preth_ex = 0;
             dok je apsolutna vrijednost od (ex – preth_ex) manja od eps ponavljaj
                    preth_ex = ex;
                    pribrojnik = pribrojnik * x / i;
                    ex = ex + pribrojnik;
                    uvećaj i;




                                                                                               68
Napomena: apsolutna se vrijednost realnog broja x u C jeziku dobije primjenom funkcije
double fabs(double x) koja je deklarirana u <math.h>.

Realizacija programa:

      /*Datoteka: ex.c*/
      #include <stdio.h>
      #include <math.h>

      double my_exp(double x, double epsilon)
      {
         int i = 1;
         double pribroj = 1.0;
         double ex = 1.0, preth_ex = 0.0;

          while (fabs( ex - preth_ex) > epsilon) {
                preth_ex = ex;
                pribroj = pribroj * x / i;
                ex = ex + pribroj;
                i = i + 1;
          }
          return ex;
      }

      int main( void)
      {
          double eps, x, ex;
          printf(" Unesi x i preciznost eps:n");
          scanf("%lf%lf", &x, &eps);
          ex = my_exp(x, eps);
          printf(" e^%f = %f; (tocno: %f)n", x, ex,                       exp(x));
          return 0;
      }


Izvršenjem programa dobiju su rezultati:

      c:>ex
      Unesi x i preciznost eps: 1 .00001
      e^1.000000 = 2.718282; (tocno: 2.718282)

      c:>ex.exe
      Enter x and the eps: 2 .0001
      e^2.000000 = 7.389047; (tocno: 7.389056)

     U prethodnom programu istim imenom (ex) su deklarirane varijable u funkciji main() i u
funkciji my_exp(). Postavlja se pitanje: da li je to ista varijabla ili se radi o dvije različite
varijable? Na to pitanje daju odgovor pravila dosega ili postojanosti identifikatora. Pravilo je da
se u različitim blokovima mogu deklarirati varijable s istim imenom. To su onda različite
varijable koje postoje samo u bloku u kojem su definirane. O tome će biti više govora u
poglavlju 8.

   Na sličan način su definirane mnoge matematičke funkcije iz standardne biblioteke (vidi
Dodatak C).




                                                                                                69
5.4 Zaključak
Do sada su korišteni sljedeći elementi C jezika:
1. Varijable i funkcije su zapisivane simboličkim imenima. U iskazima deklaracije svim
   varijablama je uvijek označen tip. To omogućuje kompilatoru da rezervira memoriju
   potrebnu za smještaj vrijednosti varijable.
2. Numeričke konstante i stringovi su zapisivani u literalnom obliku – upravo onako kako se
   zapisuju i u govornom jeziku..
3. Korišteni su različiti operatori pomoću kojih se formiraju aritmetički, relacijski i logički
   izrazi.
4. Korištene su naredbe kojima se određuje izvršenje procesa u računalu. Najprije su korištene
   tzv. proste naredbe: pridjela vrijednosti i poziv izvršenja standardnih funkcija printf() i
   scanf(). Zatim su korištene tzv. strukturalne naredbe: sekvenca naredbi koja se omeđuje
   vitičastim zagradama, while-petlja kojom se kontrolira tijek iterativnih procesa, if-
   naredba, pomoću koje se uvjetno određuje izvršenje neke naredbe, te if-else naredba,
   pomoću koje se vrši selekcija naredbi. Kasnije će biti opisane još neke naredbe za kontrolu
   toka programa.
5. Opisan je jednostavni način interakcije s korisnikom programa.
6. Pokazano je kako se koriste funkcije iz standardne biblioteke i kako korisnik može
   definirati nove funkcije.
7. Pokazano je da se funkcija može pozivati višestruko.
8. Pokazano je da se proračuni u računalu mogu izvršiti s ograničenom točnošću. Na primjeru
   eksponencijalne funkcije pokazano je kako je implementirana većina trigonometrijskih
   funkcija.
9. Razvijen je algoritam za proračun n-faktorijela i izvršena implementaciju tog algoritma u C
   jeziku. Sam tijek razvoja algoritma može programerima - početnicima biti zbunjujući, jer su
   stalno vršene dodatne analize i dorada algoritma. Iskusniji programeri znaju da je to jedini
   ispravni način razvoja programa, jer se samo postupnom analizom i doradom programa
   može napraviti kvalitetan program.
    Razvoj programa postupnom analizom i doradom (ili razvoj u koracima preciziranja) je
metoda koju su popularizirali E. Dijkstra, u knjizi "Structured Programming", Academic Press,
1972, i N. Wirth u članku "Program Development by Stepwise Refinement",CACM, April
1971.
Sam postupak se može opisati na sljedeći način:
   1. Formuliraj problem na način da bude potpuno jasno što program treba obaviti.
   2. Formuliraj temeljni tijek algoritamskog rješenja običnim govornim jezikom.
   3. Izdvoji pogodnu manju cjelinu i razloži je detaljnijim algoritmom.
   4. Ponavljaj korak (3) dok se ne dobiju algoritmi koji se mogu zapisati programskim
       jezikom (ili pseudo-jezikom).
   5. Odaberi dio algoritamskog zapisa i zapiši ga programskim jezikom. Pri tome odredi
       potrebne struktura podataka.
   6. Sustavno ponavljaj korak (5) i pri tome povećaj razinu dorade programskih rješenja.

     Na kraju, mora se nažalost reći, da ni danas u programiranju nema gotovih recepata, pa i
dalje vrijedi izneseni metodološki pristup razvoju programskih algoritama.




                                                                                            70
6 Izrazi i sintaksa C jezika


Naglasci:
   • aritmetički, logički i relacijski izrazi
   • pravila prioriteta i asocijativnosti
   • bitznačajni operatori
   • složeni operatori
   • ternarni izrazi
   • automatska i eksplicitna pretvorba tipova
   • typedef
   • sintaksa i leksika programskih jezika
   • BNF notacija za zapis sintakse


6.1 Izrazi
    Izrazi su zapisi koji sadrže operande i operatore. Svaki izraz daje neku vrijednost. Operandi
mogu biti varijable, funkcije i konstante. U izrazima može biti više operatora i više različitih
tipova operanada.
S obzirom na složenost izraza razlikuju se:

    •   Unarni izrazi – imaju samo jedan operator i jedan operand,
    •   Binarni izrazi – imaju dva operanda i jedan operator,
    •   Ternarni izrazi – imaju tri operanda i dva operatora,
    •   Složeni izrazi – sastoje se od više operanada, operatora i zagrada koje služe za
        grupiranje izraza. Pravilo je da se najprije računa vrijednost izraza koji je napisan u
        zagradama, a zatim se ta vrijednost tretira kao prosti operand. Ukoliko nema zagrada,
        tada za redoslijed izvršenja složenog izraza vrijede posebna pravila prioriteta i
        asocijativnosti djelovanja operatora.

S obzirom na upotrebu različitih operatora, izrazi mogu biti aritmetički, relacijski i logički.

Bit će pokazno:
    • Kako se izvršavaju izrazi?
    • Koji su pravila prioriteta i asocijativnosti djelovanja operatora?
    • Kako se vrši pretvorba tipova ako u nekom izrazu postoji više različitih tipova?

6.1.1 Aritmetički izrazi
     Binarni aritmetički izrazi koriste dva operanda i jedan operator: + za zbrajanje, - za
oduzimanje, * za mnnoženje, / za djeljenje i % za ostatak dijeljenja cjelobrojnih tipova (modulo
operacija). Operandi mogu biti varijable, konstante i funkcije koja vraćaju numeričku
vrijednost.

Operator % se može primijeniti samo na cjelobrojne operande jer se njime dobija ostatak
cjelobrojnog dijeljenja, primjerice izraz




                                                                                                  71
x % 2

daje vrijednost ostatka dijeljenja s 2. Taj ostatak može biti 0 ili 1 (ako je 0, broj x je paran, a ako
je 1, broj x je neparan).
     Unarni aritmetički izrazi imaju jedan operand i jedan operator: - za negaciju (daje
negativnu vrijednost) i + za 'afirmaciju' (ne mijenja vrijednost operanda). Operatori se zapisuju
ispred imena varijable, konstante ili funkcije koja vraća vrijednost.
Prefiks i postfiks unarni operatori
     Prefiks i postfiks operatori: ++ i --, uvećavaju, odnosno umanjuju, vrijednost numeričkih
varijabli za 1. Mogu se primijeniti ispred ili iza imena varijable,

      ++n;       /* uvećava n za 1 */
      --n;       /* umanjuje n za 1 */

Prefiks operator djeluje na operand prije nego se koristi njegova nova vrijednost.

      n = 5;
      x = ++n;        /* x je jednak 6, n je jednak 6 */

Postfiks operator djeluje na operand nakon korištenja njegove trenutne vrijednosti.

      n = 5;
      x = n++;       /*    x je jednak 5, n je jednak 6 */

Operandi na koje djeluju operatori ++ i -- moraju biti varijable.

Asocijativnost i prioritet djelovanja operatora
     Kada u izrazima ima više operanada i operatora, redoslijed kojim se računa izraz određen je
pravilima prioriteta i asocijativnosti. Prioritet djelovanja operatora određuje koji se podizraz
prvi izvodi.
     Aritmetički operatori imaju sljedeći prioritet izvršenja:

           viši prioritet             unarni operatori - + prefiks op (++ --)
           .....                      binarni operatori * / %
           niži prioritet             binarni operatori + -

Primjerice, -2* a + b se izvodi kao da je napisano (((- 2)* a) + b).
     Asocijativnost određuje redoslijed izvođenja izraza koji imaju više operanada istog
prioriteta. Svi aritmetički operatori imaju asocijativnost s lijeva na desno.

      a + b + c <=> (( a + b) + c)

Redoslijed izvođenja se uvijek može predodrediti upotrebom zagrada. Tada se najprije izvršava
izraz u zagradama.
Kako se vrši potenciranje?
   U C jeziku ne postoji operator potenciranja. Kada je potrebno potencirati neki broj ili
numeričku varijablu, može se koristiti dva postupka:
     1. ako se potencira s cijelim brojem tada se potenciranje može realizirati pomoću
        višestrukog množenja, primjerice
                a3 se realizira izrazom a*a*a
                a-3 se realizira izrazom 1/(a*a*a)




                                                                                                    72
2. ako se potencira s realnim brojem tada se može koristiti standardna funkcija

      double pow(double x, double y);

        koja vraća realnu vrijednost koja je jednka xy. Ova funkcija je deklarirana u <math.h>.

6.1.2 Relacijski i logički izrazi
    Relacijski ili uvjetni izrazi se sastoje se od dva operanda numeričkog tipa i sljedećih
operatora:

      <     manje
      <=    manje ili jednako
      ==    jednako
      !=    nije jednako
      >     veće
      >=    veće ili jednako


    Rezultat relacijskog izraza je vrijednost 0 ili 1. Primjerice,

      x = (a == b);        /* x je 1, ako je a jednako b, inače x je 0 */
      x = (a != b);        /* x je 0, ako je a jednako b, inače x je 1 */
      x = (a > b);         /* x je 1, ako je a veće od b, inače x je 0 */

    Pošto u C-u ne postoji logički tip varijabli, nula predstavlja logičku vrijednost false, a
nenulta vrijednost predstavlja logičku vrijednost true.

Logički operatori su:

      &&    logička konjunkcija (i)
      ||    logička disjunkcija (ili)
      !     negacija

Djelovanje logičkih operatora se određuje prema pravilu:

      izraz1 && izraz2           ->   1 ako su oba izraza različita od nule,
      inače 0
      izraz1 || izraz2           ->   0 ako su oba izraza jednaka nuli, inače 1
      !izraz                     ->   0 ako je izraz različit od nule, inače 1

     Asocijativnost relacijskih i logičkih operatora je s lijeva na desno, a prioritet je manji od
aritmetičkih operatora
 viši              Aritmetički
 prioritet          operatori            a + b < max || max == 0 && a == b

                   <, <=, >, >=         se izvršava kao:
                    ==, !=
 niži               &&                  (( a + b) < max) || (max == 0 && (a == b))
 prioritet          ||


Primjer: Godina je prestupna ako je djeljiva sa 4, a ne i s 100, ali godine koje su djeljive s 400
su uvijek prestupne godine. Ta se činjenicu može programski iskazati ne sljedeći način:

    if ((godina % 4 == 0 && godina % 100 != 0) || godina % 400 == 0)




                                                                                               73
printf("%d je prestupna godinan", godina);
    else
        printf("%d nije prestupna godina n", godina);

Primjer: Definirana je funkcija isupper() kojom se određuje da li neka cjelobrojna
vrijednost predstavlja ASCII kod kojim su kodirana velika slova

      int isupper(int c)
      /* ukoliko je argument c iz intervala ASCII vrijednosti u kojem su
      */
      /* velika slova, funkcija vraća vrijednost 1, inače vraća 0    */
      {
         return (c >= 'A' && c <= 'Z');
      }


Primjer: Definirana je funkcija tolower() koja vraća veliko slovo, ako je argument malo
slovo.

            int tolower(int c)
            /* argument c je vrijednoost iz ASCII skupa
             * Ako c predstavlja ASCII kod nekog velikog slova,
             * funkcija vraća vrijednost koja predstavlja
             * ekvivalentno malo slovo
             */
            {
               if (isupper(c))
                    return c + 'a' - 'A';
                else
                    return c;
            }


      U C jeziku se znakovne konstante tretiraju kao cijeli brojevi


Zadatak: Napišite funcije

  Funkcija                      vraća vrijednost različitu od nule (true), ako je znak c
  int isupper(int c);           veliko slovo
  int islower(int c);           malo slovo
  int isalpha(int c);           veliko ili malo slovo
  int iscntrl(int c);           kontrolni znak
  int isalnum(int c);           slovo ili znamenka
  int isdigit(int c);           decimalna znamenka
  int isxdigit(int c);          heksadecimalna znamanka
  int isgraph(int c);           tiskani znak osim razmaka
  int isprint(int c);           tiskani znak uključujući razmak
  int ispunct(int c);           tiskani znak osim razmaka, slova ili znamanke
  int isspace(int c);           razmak, tab, vert. tab, nova linija, povrat, nova stranica

Ove funkcije su implementirane u standardnoj biblioteci, a njihova deklaracija je dana u
datoteci "ctype.h".

6.1.3 Bitznačajni operatori
    U C jeziku se koristi 6 bitznačajnih operatora, koji se mogu primijeniti na integralne tipove
(char, short, int i long) .




                                                                                              74
&     bitznačajni "i" ( AND)
                      |     bitznačajni "ili" (OR)
                      ^     bitznačajno "ekskluzivno ili" (XOR)
                      <<    posmak bitova u lijevo
                      >>    posmak bitova u desno
                      ~     bitznačajna negacija (unarni op.)
                            (komplement jedinice)

Bitznačajne operacije se provode na bitovima istog značaja.

Bitznačajni "i" operator & se najčešće koristi za maskiranje bitova, primjerice nakon naredbe

      n = n & 0x000F;

u varijabli n će svi bitovi biti postavljeni na nula osim 4 bita najmanjeg značaja, bez obzira na
vrijednost od n;

      1010111000011011          n
      &
      0000000000001111          0x000F
      ----------------
      0000000000001011          rezultat

Bitznačajni "ili" operator | se najčešće koristi za postavljanje bitova, primjerice

      n = n | 0x000F;

ima učinak da se u varijabli n četiri bita najmanjeg značaja postavljaju na vrijednost 1, a ostali
bitovi su nepromijenjeni;

      1010111000011011          n
      |
      0000000000001111          0x000F
      ----------------
      1010111000011111          rezultat

     Bitznačajni "ekskluzivno ili" operator ^ postavlja bitove na vrijednost 1 na mjestima gdje
su bitovi oba operanda različiti, odnosno na nulu na mjestima gdje su bitovi oba operanda isti.

    Posmačni operatori djeluju tako da pomiču bitove udesno (>>) ili ulijevo (<<), primjerice x
<< 2 daje vrijednost od x s bitovima pomaknutim za dva mjesta udesno ( u 2 prazna mjesta se
upisuje 0).

Dokažite: Kada posmačni operatori djeluju na varijable unsigned tipa onda pomak bitova za
jedno mjesto u lijevo je ekvivalentno množenju s 2, a pomak bitova za jedno mjesto u desno je
ekvivalentno dijeljenju s cijelim brojem 2.

Primjer: Definirana je funkcija getbit(x,n) kojom se ispituje da li u cijelom broju x n-ti bit
ima vrijednost 1.

      int getbit (unsigned x, int n)
      {
          if (n>=0 && n<32) /* unsigned ima 32 bita */
          {




                                                                                                75
return (x & 01 << n) != 0;
               }
               return 0;
       }

Objasnite primjenu << operatora u ovom primjeru.

Primjer: U programu binary.c korisnik unosi cijeli broj, a program ispisuje njegov binarni
oblik.

       /* datoteka: binary.c */
       /* program ispisuje binarni kod cijelog broja*/
       #include <stdio.h>

       int main()
       {
           int x, i, n;
           printf("Otkucaj cijeli broj:n");
           scanf("%d", &x);
           n = 8*sizeof(x);
           printf("Binarni kod je: ");
           i =n-1;
           while(i >=0)
               printf("%d",getbit(x,i--));
           printf("n");
           return 0;
       }


Ispis je sljedeći:

       Otkucaj        cijeli broj:
       -2
       Binarni        kod je: 11111111111111111111111111111110
       ili
       Otkucaj        cijeli broj:
       67
       Binarni        kod je: 00000000000000000000000001000011

6.1.4 Složeni operatori pridjele vrijednosti
Izraz oblika

       i = i + 2

u kojem se ista varijabla pojavljuje s obje strane znaka pridjele vrijednosti, može se zapisati u
obliku:

       i += 2

Operator += se naziva složeni operator pridjele vrijednosti.
Ovaj oblik se može primijeniti na većinu binarnih operatora: +=, -=, *=, /=, %=, <<=,
>>=, &=, ^= i |=, koristeći opće pravilo:


Ako su izraz1 i izraz2 neki izrazi, tada

                     izraz1 op= izraz2




                                                                                              76
je ekvivalentno

                 izraz1 = (izraz1) op (izraz2)

        pri tome izraz1 mora biti izraz koji označava položaj u memoriji (ime varijable ili
        dereferencirani pokazivač).
     Ovi operatori, kao i operator pridjele vrijednosti, imaju niži prioritet od aritmetičkih,
relacijskih i logičkih operatora, stoga iskaz

        x *= y + 1;

znači

        x = x * (y + 1);

a ne

        x = x * y + 1;


Primjer: Definirana je funkcija brojbita(x) koja vraća broj bita koji u argumentu x imaju
vrijednost 1.

        int brojbita(unsigned x)
        /* daje broj bita koji u argumentu x imaju vrijednost 1*/
        {
            int broj=0;
            while( x != 0)
            {
                if (x & 01)
                    broj++;
                x >>= 1;
            }
            return b;
        }

Zadatak: napišite program u kojem korisnik unosi cijeli broj, a program ispisuje broj bita koji
su u tom broju različiti od nule.

6.1.5 Ternarni uvjetni izraz
Ternarni izraz se sastoji od tri izraza međusobno odvojena upitnikom i dvotočkom:

        izraz1 ? izraz2 : izraz3

a značenje mu je slijedeće: ako je izraz1 različit od nule, vrijednost ternarnog izraza je jednaka
izrazu2, a ako je izraz1 jednak nuli vrijednost ternarnog izraza je jednaka izrazu3. Primjerice u
naredbi:

         max = (x>y) ? x : y;

vrijednost varijable max će biti jednaka x ako je x>y, u suprotnom vrijednost od max će biti
jednaka vrijednosti varijable y.

Ternarni izraz je zapravo skraćeni oblik naredbe selekcije:




                                                                                               77
if(x>y)
         max = x;
      else
         max = y;

međutim, često je prikladnija njegova upotreba od naredbe selekcije jer ga se može koristiti u
izrazima.


6.2 Automatska i explicitna pretvorba tipova
Automatska pretvorba tipova
     Svaki izraz daje neku vrijednost čiji tip ovisi o tipu članova izraza. Kada su u nekom izrazu
svi članovi i faktori istog tipa tada je i vrijednost izraza tog tipa. Primjerice, za

      float y = 5, x=2;

izraz y/x daje realnu vrijednost 2.5.
Ako su x i y cjelobrojne varijable,

      int y = 5, x=2;

tada izraz y/x daje cjelobrojnu vrijednost 2 (ostatak dijeljenja se odbacuje).
     U C jeziku se svi standardni tipovi tretiraju kao numerički tipovi i može ih se koristiti u
svim izrazima. Kada u nekom izrazu ima više različitih tipova tada kompilator u izvršnom kodu
vrši automatsku pretvorbu tipova. Princip je da se uvijek izvršava jedna operacija s maksimalno
dva operanda. Ako su ta dva operanda različitog tipa onda se prije označene operacije vrši
pretvorba tipa niže opsežnosti u tip više opsežnosti.


   Opsežnost tipa, u redoslijedu od manje prema većoj opsežnosti je:
            char → int → unsigned → long → float → double.


Primjerice, ako se koriste varijable

      int j=5, k=7;
      float x=2.1;

u izrazu:

      j+7.1*(x+k)

on se izvršava sljedećim redoslijedom:

    1. najprije se izvršava proračun izraza u zagradama. U tom izrazu se najprije vrijednost
       varijable k pretvara (kodira) u tip float, jer je drugi operand tipa float. Zatim se toj
       vrijednosti dodaje vrijednost varijable x.
    2. Vrijednost dobivenog izraza se zatim množi s realnom konstantom 7.1, jer množenje
       ima viši prioritet od zbrajanja.
    3. Konačno preostaje da se zbroji vrijednost varijable j s vrijednošću prethodno
       izračunatog izraza (7.1*(x +k)), koji je realnog tipa. Pošto je to izraz s dva različita
       tipa, najprije se vrši pretvorba vrijednosti varijable i u tip float, i tek tada se izvršava
       operacija zbrajanja.




                                                                                                 78
Pretvorba tipova u naredbi pridjele vrijednosti
    Pretvorba tipova u naredbi pridjele vrijednosti se uvijek vrši tako da se vrijednost koja se
dobije iz izraza koji je na desnoj strani pretvara u tip koji ima varijabla na lijevoj strani. U
slučaju da je s lijeve strane tip veće opsežnosti pretvorba se uglavnom može izvršiti bez gubitka
točnosti. Primjerice, nakon izvršenja naredbi

      float x;
      int i = 3;
      x = i;
      printf("x=%f", x);

bit će ispisano: x=3.00000.
     U slijedećem slučaju pretvorba tipa int u tip unsigned neće imati smisla. Nakon
izvršenja naredbi:

      unsigned u;
      int i = -3;
      u = i;
      printf("u=%u", u);

bit će ispisano: u= 4294967293.

     Kada se u izrazima miješaju tipovi int i unsigned, logični rezultat možemo
     očekivati samo za pozitivne brojeve.


    Kada se s lijeve strane nalazi tip manje opsežnosti, pretvorba se vrši sa smanjenjem
točnošću. Često se vrijednost tipa float ili double pridjeljuje cjelobrojnoj varijabli, primjerice
za

      double d = 7.99;
      int i ;
      i = d;
      printf("i=%d", i);

bit će ispisano i = 7.

      Pravilo je da se pri pretvorbi realnog u cijeli broj odbacuje decimalni dio. To
      vrijedi bez obzira koliki je decimalni dio.


     U mnogim programskim zadacima pojavit će se potreba da se pretvorba realnog broja u
cijeli broj obavi na način da se vrijednost cijelog broja što manje razlikuje od vrijednosti realnog
broja. To znači da ako je d=7.99, tada je poželjno da se ova vrijednost pretvori u cjelobrojnu
vrijednost 8. To se može postići tako da se prije pretvorbe u cijeli broj decimalnom broju doda
vrijednost 0.5, ako je pozitivan, odnosno da se od decimalnog broja odbije vrijednost 0.5 ako je
negativan. U tu svrhu može se definirati funkciju Double2Int(), koja vraća cjelobrojnu
vrijednost realnog argumenta;

      int Double2Int(double x)
      {
      /* funkcija vraća cijeli broj koji je
       * najbliži relnoj vrijednosi x
       */
         if(x>0)




                                                                                                 79
return x+0.5;
          else
             return x-0.5;
      }

Ekplicitna pretvorba tipova
Ukoliko se ispred nekog izraza ili varijable u zagradama zapiše oznaka tipa, primjerice

      (float) x

time se eksplicitno naređuje kompilatoru da se na tom mjestu izvrši pretvorba vrijednosti
varijable x u tip float.

      Kada se oznaka tipa zapiše u zagradama to predstavlja operator pretvorbe tipa
      (eng. cast operator).


     Primjenu ovog operatora ilustrira program u kojem se vrijednost dijeljenja cijelog broja s
cijelim brojem pridijeljuje realnoj varijabli.

      int main()
      {
          int i1 = 100, i2 = 40;
          float f1;
          f1 = i1/i2;
          printf("%lfn", f1);
          return(0);
      }

Dobije se ispis:

      2.000000

     Pri dijeljenju je izgubljen decimalni dio iako je rezultat izraza i1/i2 pridijeljen realnoj
varijabli. Zašto? Zato jer se pretvorba tipa vrši samo ako se u izrazu nalaze različiti tipovi.
Pošto su u izrazu i1/i2 oba operanda tipa int izvršava se dijeljenje s cijelim brojevima. Ako
želimo da se sačuva i decimalni dio može se primijeniti operator pretvorbe u jednom od tri
oblika:

             f = (float)i1/i2;
      ili    f = i / (float)j;
      ili    f = (float)i / (float)j;

    Dovoljno je da se pretvorba tipa označi na samo jednom operandu, jer se izrazi računaju
tako da se uvijek vrši pretvorba u tip veće opsežnosti.
Pokažimo još jedan primjer u kojem je potrebno primijeniti operator pretvorbe tipova

      short int i = 32000, j = 32000;
      long li;
      li = (long)i + j;

    Operator (long) je primjenjen zbog toga jer maksimalna vrijednosti za tip short int
iznosi 32767. Stoga, ako bi se zbrojile dvije short int kodirane vrijednosti iznosa 32000
rezultat bi bio veći od 32767. Operator (long) ispred jednog operanda osigurava da će se
zbrajanje izvršiti na način kao da su operandi tipa long.




                                                                                             80
6.3 Definiranje sinonima tipa pomoću typedef
Kada se ispred deklaracije napiše typedef, primjerice
           typedef int cijelibroj;

time se označava da identifikator, u ovom slučaju cijelibroj, neće biti deklariran kao
varijabla ili funkcija, već da taj identifikator postaje sinonim za tip koji je opisan deklaracijom.
U ovom primjeru, identifikator cijelibroj postaje sinonim za tip int, pa ga se u kasnije
može koristiti u drugim deklaracijama, na isti način kako se koristi i originalni tip, primjerice

      cijelibroj i;               /* deklaracija sa typedef tipom */

    Važno je napomenuti da se pomoću typedef deklaracije stvaraju sinonimi tipova; a ne neki
novi tipovi. Njihova je upotreba korisna za povećanje apstraktnosti programskog zapisa. Prema
ANSI standardu, u C jeziku je definirano nekoliko typedef tipova kako bi se jasnije označilo
područje njihove primjene. Primjerice, size_t predstavlja tip unsigned int, kojim se često
označava veličina, u bajtima, objekata smještenih u datotakama ili u memoriji. Implementacija
je provedena deklaracijom

      typedef unsigned int size_t;

u datoteci "stddef.h".
    Drugi primjeri su FILE, time_t, ptrdiff_t i wchar_t (pogledajte njihovo značenje u
opisu standardne C-biblioteke).


6.4 Formalni zapis sintakse C-jezika
Pisanje programa podliježe jezičnim pravilima:

    1. leksička pravila određuju kako se tvore leksemi na zadanom alfabetu (ASCII skup),
    2. sintaktička (gramatička) pravila određuju kojim se redom leksemi slažu u programske
       iskaze,
    3. semantička pravila određuju značenje programskih iskaza.

     Leksička struktura C jezika se temelji na pravilima koja određuju kako se formiraju leksemi
jezika (niz znakova koji čini prepoznatljivu nedjeljivu cjelinu), na zadanom alfabetu (ASCII
skup znakova).

Temeljne leksičke kategorije su:

1. Ključne riječi jezika (if, while, else, do, int, char, float,..) služe za definiranje programskih
   iskaza. Pišu se malim slovima.

2. Identifikatori služe za zapis imena varijabli, funkcija i korisničkih tipova. Pišu se pomoću
   niza velikih i malih slova, znamenki i znaka podvlake ('_'), uz uvjet da prvi znak u nizu
   mora biti slovo ili podvlaka.

3. Literalne konstante služe za zapis numeričkih i tekstualnih (znakovnih) konstanti (pr. 135,
   3.14, 'A', "Hello World").




                                                                                                   81
4. Operatori (+,-*/,..=, []..(), &, .+=,*=.) služe označavanju aritmetičko-logičkih i drugih
   operacija koje se provode sa memorijskim objektima (funkcije i varijable) i konstantama.

5. Leksički separatori su znakovi koji odvajaju lekseme. Jedan ili više znakova razmaka,
   tabulatora i kraja retka tretiraju se kao prazno mjesto, kojim se razdvajaju leksemi.
   Operatori, također, imaju značaj leksičkih separatora. Znak točka-zarez (';') predstavlja
   specijalni separator koji se naziva terminator naredbi.

6. Komentar se piše kao proizvoljni tekst. Početak komentara se označava znakovima /*, a
   kraj komentara s */. Komentar se može pisati u bilo kojem dijelu programa, i u više linija
   teksta. Mnogi kompilatori kao komentar tretiraju i tekst koji se unosi iza dvostruke kose crte
   //, sve do kraja retka.

7. Specijalne leksičke direktive su označene znakom # na početku retka. Izvršavaju se prije
   procesa kompiliranja, pa se nazivaju i pretprocesorske direktive. Primjerice, #include
   <stdio.h> je pretprocesorska direktiva kojom se određuje da se u proces kompiliranja
   uvrsti sadržaj datoteke imena stdio.h.

Kao što se zapis u prirodnom jeziku sastoji od različitih elemenata (subjekt, predikat, pridjev,
rečenica, poglavlje itd.), tako se i zapis u programskom jeziku sastoji od temeljnih elemenata,
koje prikazuje tablica 6.2.

   Elementi programa      Značenje                                         Primjer
   Tipovi                 oznake za skup vrijednosti                       int , float ,
                          s definiranim operacijama                        char
   Konstante              literalni zapis vrijednosti                      0 , 123.6 ,
                          osnovnih tipova                                  "Hello"
   Varijable              imenovane memorijskih lokacije koje              i , sum
                          sadrže vrijednosti nekog tipa
   Izrazi                 zapis proračuna vrijednosti kombiniranjem        sum + i
                          varijabli, funkcija, konstanti i operatora
   Naredbe ili iskazi     zapisi pridjele vrijednosti, poziva funkcije i   sum = sum + i;
                          kontrole toka programa                           while (--i)
                                                                           if(!x).. else ..;
   Funkcije               imenovano grupiranje naredbi                     main()
   (potprogrami)                                                           printf(...)
   Kompilacijska          skup međuovisnih varijabli i funkcija koji       datoteka.c
   jedinica               se kompilira kao jedinstvena cjelina
                   Tablica 6.2 Temeljni elementi zapisa programa u C jeziku


     Navedeni elementi jezika se iskazuju kombinacijom leksema prema strogim gramatičkim,
odnosno sintaktičkim pravilima, koji imaju nedvosmisleno značenje.
     U prirodnim jezicima iskazi mogu imati više značenja, ovisno o razmještaju riječi, o
morfologiji (tvorba riječi) i fonetskom naglasku. U programskim jezicima se ne koristi
morfološka i fonetska komponenta jezika, pa se gramatika svodi na sintaksu, također, dozvoljen
je samo onaj raspored riječi koji daje nedvosmisleno značenje. Uobičajeno se kaže da gramatika
programskih jezika spada u klasu bezkontesktne gramatike.




                                                                                               82
Slika 2.1. Osnovne faze u procesu kompiliranja

     Za opis sintakse nekog jezika koristi se posebni jezik koji se naziva metajezik. Jezik koji se
opisuje metajezikom naziva se ciljni jezik. Za opis semantike nekog jezika ne postoje prikladni
metajezici već se semantika izražava opisno, primjenom prirodnih jezika.
     Prije nego se izvrši opis metajezika, koji će biti upotrijebljen za opis sintakse C jezika, bit
će opisani neki pojmovi iz teorije programskih jezika.
     Na slici 2.1 ilustriran je proces kompiliranja. On se odvija na sljedeći način. Izvorni kod
može biti spremljenu u jednoj datoteci ili u više datoteka koje se u toku jezičkog
pretprocesiranja formiraju kao jedna datoteka, koja se naziva kompilacijska jedinica. Zatim se
vrši leksička analiza izvornog koda, na način da se izdvoje leksemi (nizovi znakova koji
predstavljaju nedjeljivu cjelinu). Ukoliko je leksem zapisan u skladu s leksičkom strukturom
jezika on predstavlja terminalni simbol jezika (token) kojem se u radu kompilatora pridjeljuje
jedinstveno značenje. U jezičke simbole spadaju: ključne riječi (if, else, while,...), specijalni
simboli (oznake operatora i separatora), identifikatori (imena varijabli, konstanti, funkcija,
procedura i labele), literalne numeričke i tekstualne konstante. Pojedinom simbolu pridjeljuju se
različiti atributi koji se koriste u procesu generiranja koda. Primjerice, za varijable se unosi
atribut koji opisuje tip varijable, ili uz literalno zapisanu numeričku konstantu se unosi i binarno
kodirana numerička vrijednost konstante. Sintaktički analizator (parser) dobavlja jezičke
simbole i određuje da li su oni grupirani u skladu s definiranom sintaksom. Ukoliko je to
zadovoljeno, vrši se prevođenje u objektni kod usklađeno sa semantikom jezika.

    Pogreške u procesu kompiliranja se dojavljuju kao:
    •    leksičke pogreške       (pr. nije ispravno zapisano ime varijable)
    •    sintaktičke pogreške    (pr. u aritmetičkom izrazu nisu zatvorene zagrade)
    •    semantičke pogreške     (pr. primijenjen operator na dva nekompatibilna operanda)

U programu mogu biti prisutne i logičke pogreške (pr. petlja se ponavlja beskonačno). Njih
može otkriti korisnik tek prilikom izvršenja programa.

Za pojašnjenje navedenih pojmova razmotrimo iskaz:

        if (a > 3) max = 5.4; else max = a;




                                                                                                 83
Ovaj iskaz predstavlja ispravno zapisani sintaktički entitet - IskazIf. U njemu se pojavljuju
sljedeći simboli: ključne riječi (if, then, else), operatori (>, =), identifikatori varijable (a i max),
numeričke konstante (3 i 5.4) i terminator iskaza (;). Napomenimo da "razmak" predstavlja
leksički separator. On se ne smatra simbolom jezika i može se umetnuti između leksema
proizvoljan broj puta. Odnos leksema, tokena i atributa prikazuje donja tablica.

           Leksem                 kategorija tokena                atribut
           "if", "else"           ključna riječ                    -
           "max", "a"             Identifikator                    varijabla
           "=", ">"               operatori                        -
           ";"                    terminator naredbe
           ( ...)                 separator izraza
           "5.4", "3"             konstanta                        numerička vrijednost: 5.4 i 3

Za IskazIf u C jeziku vrijedi sintaktičko pravilo:

IskazIf        "je definiran kao" if (Izraz) Iskaz else Iskaz
                      "ili kao"           if (Izraz) Iskaz

      Gornji iskaz zadovoljava ovo sintaktičko pravilo jer (a>3) predstavlja relacijski izraz, dakle
predstavlja sintaktički entitet Izraz, a iskazi x=5.4; i x=a; predstavljaju iskaze dodjele
vrijednosti, dakle pripadaju sintaktičkom entitetu Iskaz. Ako se izneseno sintaktičko pravilo
shvati kao zapis u nekom sintaksnom metajeziku onda IskazIf, Izraz i Iskaz predstavljaju
metajezičke varijable koje u odnosu na ciljni jezik predstavljaju neterminalne simbole, "je
definiran kao" i "ili kao" su metajezički operatori, a leksemi: if, then i else i znakovi zagrada su
metajezičke konstante koje odgovaraju simbolima ciljnog jezika, pa se nazivaju terminalni
simboli ili tokeni. Uočimo da "ili kao" operator ima značaj logičkog operatora ekskluzivne
disjunkcije.
      Sintaktička pravila, kojima se jedan neterminalni simbol definira pomoću niza terminalnih
i/ili neterminalnih simbola, nazivaju se produkcije jezika.

    Prema ANSI/ISO standardu produkcije C-jezika se zapisuju na sljedeći način:
    1. Operator "je definiran kao" je zamijenjen znakom dvotočke, a produkcije imaju oblik:

          neterminalni_simbol : niz terminalnih i/ili neterminalnih simbola

    2. Alternativna pravila ("ili kao") se pišu u odvojenim redovima.
    3. Neterminalni simboli se pišu kurzivom.
    4. Terminalni simboli se pišu na isti način kao u ciljnom jeziku
    5. Opcioni simboli se označavaju indeksom opt (Simbolopt ili Simbolopt).
Primjerice, zapis produkcije if-else iskaza glasi

           IskazIf :   if (Izraz) Iskaz                  else Iskaz
      if (Izraz) Iskaz

Ovo se pravilo može se napisati i na sljedeći način:

      IskazIf :     if (Izraz) Iskaz                  ElseIskazopt
      ElseIskaz : else Iskaz




                                                                                                     84
U prvom je pravilu uveden je ElseIskaz kao opcioni neterminalni simbol. Ako postoji,
onda je njegova sintaksa opisana drugim pravilom, a ako ne postoji onda prvo pravilo
predstavlja pravilo proste uvjetne naredbe.
    Gornja pravila ćemo proširiti na način da se operator "ili kao" može eksplicitno označiti
okomitom crtom (|), zbog dva razloga:
    1. Na taj način gornja pravila (1-4) su ekvivalentna popularnoj BNF notaciji (BNF
       notacija je metajezik razvijen 1960. godine prilikom definicije programskog jezika
       ALGOL, pri čemu su bitne doprinose dali J.W.Bakus i P.Naur, pa BNF predstavlja
       kraticu za "Backus-ova normalna forma" ili "Backus-Naur-ova forma").
    2. Na taj način se alternativne produkcije mogu pisati u istom redu
Pomoću prethodno definiranih pravila lako se može definirati i leksička struktura jezika.
Primjerice, temeljni se leksički objekti znamenka i slovo mogu definirati pravilima:


      slovo : A⎪B⎪C⎪D⎪E⎪F⎪G⎪H⎪I⎪J⎪K⎪L⎪M⎪N⎪O⎪P⎪Q⎪R⎪S⎪T⎪U⎪V⎪W⎪X⎪Y⎪Z
              ⎪a⎪b⎪c⎪d⎪e⎪f⎪g⎪h⎪i⎪j⎪k⎪l⎪m⎪n⎪o⎪p⎪q⎪r⎪s⎪t⎪u⎪v⎪w⎪x⎪y⎪z.

      znamenka : 0⎪1⎪2⎪3⎪4⎪5⎪6⎪7⎪8⎪9.

      heksa_znamenka : 0⎪1⎪2⎪3⎪4⎪5⎪6⎪7⎪8⎪9⎪A⎪B⎪C⎪D⎪E⎪F⎪a⎪b⎪c⎪d⎪e⎪f.

      oktalna_znamenka : 0⎪1⎪2⎪3⎪4⎪5⎪6⎪7⎪.

    Koristeći objekte znamenka i slovo može se definirati objekt znak (koji može biti slovo ili
znamenka):
      znak : znamenka ⎪ slovo.

Sintaksa znaka može po potrebi biti i drugačije definirana, naročito ukoliko se pod pojmom
znak mogu koristiti i specijalni znakovi, ili još i šire, cijela ASCII kolekcija simbola.
    Vrlo često, potreban element jezika je niz znakova. Njega se može definirati korištenjem
rekurzivnog pravila:
      niz_znakova : znak ⎪ niz_znakova znak,

što se tumači na sljedeći način: niz znakova je ispravno zapisan ako sadrži samo jedan znak ili
ako sadrži niz znakova i s desne strane još jedan znak. Dakle, alternativno pravilo prepoznaje
sve nizove koji imaju dva ili više znakova. Može se napisati i slijedeće:

      niz_znakova : znak ⎪ znak niz_znakova,

što se tumači ovako: niz znakova je ispravno zapisan ako sadrži samo jedan znak ili ako iza
znaka sadrži niz znakova. Uočimo da alternativno pravilo, također, prepoznaje nizove koji
sadrže dva ili više znakova.
      Identifikatori u C-jeziku (nazivi varijabli, labela, funkcija i tipova) moraju početi sa slovom
ili znakom podvlake '_', pa vrijedi :


             identifikator : slovo
                      | _
                      | identifikator slovo
                      | identifikator znamenka
                      | identifikator _




                                                                                                  85
Na osnovu ovog pravila, kao ispravno zapisani identifikatori, ocjenjuju se: BETA7 , A1B1
, x , xx , xxx , dok sljedeći zapisi ne predstavljaju indetifikatore: 7, A+B , 700BJ , -beta , x*5 ,
a=b , x(3).
      Pod pojmom liste identifikatora podrazumijeva niz identifikatora međusobno razdvojenih
zarezom.

      lista_identifikatora : identifikator
                              ⎪ identifikator , lista_identifikatora

U Dodatku B dana je potpuna specifikacija sintakse C jezika.




                                                                                                 86
7 Proste i strukturalne naredbe C jezika


Naglasci:
   • proste i strukturalne naredbe
   • naredbe bezuvjetnog skoka i označene naredbe
   • naredbe s logičkom i cjelobrojnom selekcijom
   • tipovi petlji i beskonačne petlje

     Naredbe su programski iskazi pomoću kojih se kontrolira izvršenje programa. Prema razini
apstrakcije računarskog procesa, kojeg predstavljaju, dijele se na proste naredbe i strukturalne
naredbe. U prethodnim poglavljima se korištene strukturalne naredbe tipa sekvence, selekcije
(if-else-naredba) i iteracije (while-petlja), te proste naredbe pridjele vrijednosti i poziva
potprograma. Interesantno je napomenuti da se pomoću tih naredbi može napisati bilo koji
algoritam koji je moguće izvršiti računalom. U ovom poglavlju će biti opisane sve naredbe C
jezika koje se koriste za kontrolu toka programa.


7.1 Proste naredbe
    Proste ili primitivne naredbe su one naredbe koje odgovaraju naredbama strojnog jezika. U
C jeziku "najprostija" naredba je izraz iza kojeg se napiše znak točka-zarez. Takova naredba se
naziva naredbeni izraz. Sintaksa naredbe je:
      naredbeni izraz :
              izrazopt ;

Primjerice, 1+3*7; je naredba izračun vrijednosti izraza 1+3*7. Kada računalo izvrši operacije
opisane ovim izrazom, rezultat će ostati negdje u memoriji ili u procesoru računala, stoga ova
naredba nema nikakvog smisla. Ako se pak napiše naredba

      x = 1+3*7;

tada će rezultat biti spremljen u memoriji na adresi koju označava varijabla imena x. Do sada je
ovakva naredba nazivana naredba pridjele vrijednosti, jer se ona tako naziva u većini
programskih jezika. U C jeziku se ova naredba zove naredbeni izraz pridjele vrijednosti, jer
znak = predstavlja operator pridjele vrijednosti koji se može koristiti u izrazima. Primjerice, u
naredbi

      x = 3 + (a=7);

znak = se koristi dva puta. Nakon izvršenja ove naredbe vrijednost varijable a je 7, a vrijednost
varijable x je 10.

Prema iznesenom sintaktičkom pravilu naredbom se smatra i znak točka-zarez:

      ;   /* ovo je naredba C jezika */




                                                                                              87
Ova se naredba naziva nulta ili prazna naredba. Njom se ne izvršava nikakvi proces. Makar to
izgledalo paradoksalno, ovu se naredbu često koristi, i često je uzrok logičkih pogreški u
programu. Zašto se koristi i kako nastaju pogreške zbog korištenja ove naredbe bit će pokazano
kasnije.
    Upoznavanje s naredbenim izrazima završit će sa sljedećim primjerima prostih naredbi:

      x++;                       /*   povećaj vrijednost x za 1                   */
      --x;                       /*   umanji vrijednost x za 1                    */
      printf("Hi");              /*   poziv potprograma                           */
      x=a+3.14+sin(x);           /*   kompleksni izraz s pozivom funkcije         */

   Posljednju naredbu, u kojoj se računa kompleksni izraz, s pozivom funkcije sin(x),
moglo se zapisati pomoću više naredbenih izraza:

      x = a;                  /*   vrijednost od a pridijeli varijabli x                    */
      x += 3.14;              /*   uvećaj x za 3.14                                         */
      tmp=sin(x);             /*   pomoćnoj varijabli tmp pridijeli vrijednost              */
                              /*   koju vraća funkcija sin(x)                               */
      x += tmp;               /*   uvećaj x za vrijednost varijable tmp                     */

     Operacijska semantika, odnosno način kako se naredbe izvršavaju u računalu, u oba zapisa
je potpuno ista, jer C prevodilac složene izraze razlaže u više prostih izraza koji se mogu
direktno prevesti u strojni kôd procesora.
     U proste naredbe spadaju još naredbe bezuvjetnog i uvjetnog skoka. Pomoću ovih naredbi
se može eksplicitno zadati da se izvršenje programa nastavi naredbom koja je označena nekim
imenom.
Sintaksa označene naredbe je:

    označena_nareba :
            identifikator : naredba

Identifikator kojim se označava neka naredba često se naziva programska labela.

Sintaksa naredbe bezuvjetnog skoka je:

      naredba_skoka :
            goto identifikator ;

Semantika naredbe je da se izvrši skok, odnosno da se izvršenje programa nastavi naredbom
koja je označena identifikatorom i znakom dvotočke. Primjerice, u nizu naredbi:

                   goto next;
                   naredba2
             next: naredba3

nikad se neće izvršiti naredba2, jer se u prethodnoj naredbi vrši bezuvjetni skok na naredbu
koja je označena identifikatorom next. Skok se može vršiti i unatrag, na naredbe koje su već
jednom izvršene. Na taj način se mogu realizirati iterativni procesi – petlje.
Naredba skoka i naredba na koju se vrši skok, moraju biti definirani unutar iste funkcije.
    Zapis naredbe uvjetnog skoka, koji se izvodi na temelju ispitivanja logičke vrijednosti
nekog izraza, je:
      if ( izraz ) goto identifikator ;




                                                                                             88
što znači: ako je izraz logički istinit (različit od nule) vrši se skok na označenu naredbu, a ako
nije izvršava se slijedeća naredba.
    Naredbe uvjetnog i bezuvjetnog skoka vjerno opisuju procese u računalu, međutim njihova
se upotreba ne preporučuje. Sljedeći primjer pokazuje zašto programeri "ne vole goto
naredbu". Razmotrimo zapis:

          if (izraz) goto L1;
          goto L2;
      L1: naredba
      L2: .....

U prvoj se naredbi ispituje vrijednost izraza. Ako je on različit od nule, izvršava se naredba
označena s L1, u suprotnom izvršava se naredba goto L2. Primjenom logičke negacije na
izraz u prvoj naredbi dobije se ekvivalentni algoritam:

         if (!izraz) goto L2:
         naredba
      L2: ......

Mnogo jednostavnije se ovaj programski tijek zapisuje tzv. uvjetnom naredbom:

      if (izraz) naredba

Ova naredba spada u strukturalne naredbe selekcije. Ona, već na "prvi pogled", jasno iskazuje
koji proces treba izvršiti. Ako se pak pogleda prethodna dva zapisa, u kojima je korištena goto-
naredba, trebat će znatno više mentalnog napora za razumijevane opisanog procesa. Ovaj
problem posebno dolazi do izražaja kod većih programa, gdje primjena goto naredbe dovodi do
stvaranja nerazumljivih i "zamršenih" programa.
    Jedino kada se može opravdati upotreba goto naredbe jest kada se želi napisati algoritam
koji treba biti "ručno" preveden na asemblerski jezik. U svim ostalim slučajevima, u
programiranju i u razvoju algoritama, treba koristiti naredbe selekcije i petlje kojima se dobija
jasna i pregledna struktura programa.


7.2 Strukturalne naredbe
7.2.1 Složena naredba ili blok
      Pod pojmom složene naredbe podrazumijeva se niz naredbi i deklaracija napisan unutar
vitičastih zagrada. Naziva se i blok jer se u okviru neke druge strukturalne naredbe može
tretirati kao cjelina. Lijeva zagrada '{' označava početak, a desna zagrada '}' označava kraj
bloka.
Sintaksa složene naredbe je:

      složena-naredba :
            { niz-deklaracijaopt niz-naredbiopt }

Unutar bloka dozvoljeno je deklarirati varijable, ali samo na mjestu neposredno iza vitičastih
zagrada.
    Pogledajmo programski odsječak u kojem se vrši zamjena vrijednosti dvije varijable x i y.

      int x, y;
      ........




                                                                                               89
x=7;
      y=5;

      {
          int tmp;             /* tmp je lokalna varijabla bloka*/
          tmp = x;             /* tmp == 7 */
          x = y;               /*   x == 5 */
          y = tmp;             /*   y == 7 */
      }
      printf("x=%d, y=%d", x, y)

     Zamjena vrijednosti se vrši pomoću varijable tmp, koja je deklarirana unutar bloka, i koja
ima karakter lokalne varijable, što znači da joj se ime može koristiti samo unutar bloka. Uočite
da se najprije vrijednost od x upisuje u tmp. Zatim se varijabli x pridjeljuje vrijednost varijable
y, i konačno se varijabli y pridjeljuje vrijednost od x, koja je bila sačuvana u varijabli tmp.
Nakon izlaska iz bloka nije potrebna varijabla tmp. U C-jeziku se automatski obavlja
odstranjenje iz memorije lokalnih varijabli po izlasku iz bloka u kojem su definirane. Kasnije će
o ovom problemu biti više govora.
   Sa semantičkog stajališta blok analizirano kao niz deklaracija i naredbi, dok u analizi
   sintakse i strukture programa, blok predstavlja jedinstvenu naredbu. To ujedno znači da u
   zapisu sintakse, na svakom mjestu gdje pišemo naredba, podrazumijeva se da može biti
   napisana i prosta i složena naredba i ostale strukturalne naredbe.

7.2.2 Naredbe selekcije
    Općenito se pod selekcijom nazivaju programske strukture u kojima dolazi do grananja
programa, a nakon prethodnog ispitivanja vrijednosti nekog izraza.
U C jeziku se koriste se tri tipa naredbi selekcije:
      1. Uvjetna naredba (if- naredba)
      2. Uvjetno grananje (if-else naredba)
      3. Višestruko grananje (switch-case naredba)
     U prva dva tipa naredbi grananje se vrši na temelju ispitivanja logičke vrijednosti nekog
izraza, a u switch-case naredbi grananje može biti višestruko, ovisno o cjelobrojnoj vrijednosti
nekog selektorskog izraza.

Uvjetna naredba (if-naredba)
Sintaksa if-naredbe je :

      if_naredba:
            if ( izraz ) naredba

gdje naredba može biti bilo koja prosta, složena ili strukturalna naredba. Značenje naredbe je:
ako je izraz različit od nule se izvršava naredba, a ako je izraz jednak nuli, program se nastavlja
naredbom koja slijedi iza if-naredbe.




                                                                                                90
Slika 7.1 Dijagram toka if- naredbe

Uzmimo primjer da analiziramo dvije varijable : x i y. Cilj je odrediti koja je od te dvije
vrijednosti manja, a zatim tu manju vrijednost upisati u varijablu imena min. To se može
ostvariti naredbama:


      min = y;                      /* pretpostavimo da je y manje od x */
      if (x < y)                    /* ako je x manje od y              */
           min = x;                 /* minimum je jednak x-u            */


Uvjetno grananje (if-else naredba)
Sintaksa if-else naredbe je

      if-else-naredba:
             if ( izraz ) naredba1 else naredba2

gdje naredba1 i naredba2 predstavljaju bilo koji prostu, složenu ili strukturalnu naredbu.
Značenje if-else-naredbe je: ako je izraz različit od nule, izvršava se naredba1, inače izvršava se
naredba2.
Primjerice, iskaz:

      if (x < y) min = x; else min = y;

omogućuje određivanje minimalne vrijednosti.




                              Slika 7.2 Dijagram toka if-else naredbe




                                                                                                91
Uzmimo sada da je potrebno odrediti da li je vrijednost varijable x unutar intervala {3,9}.
Problem se može riješiti tako da se unutar if-else naredbe, u kojem se ispituje donja granica
intervala, umetne if-else naredba kojom se ispituje gornja granica intervala.

    Tri sintaktički i semantički ekvivalentna if-else iskaza (nakon izvršenja, varijabla unutar
             ima vrijednost 1 ako je x unutar intervala {3,9}, inače ima vrijednost 0.)

     if (x >= 3)                                   if (x >= 3)
     {                                                if (x <= 9)
        if (x <= 9) unutar = 1;                          unutar = 1;
        else        unutar = 0;                       else
     }                                                   unutar = 0;
     else                                          else
        unutar = 0;                                    unutar = 0;

         if (x >= 3) if (x <= 9) unutar = 1; else unutar = 9; else unutar
         = 0;



     U prvom se zapisu može, bez dodatnih pojašnjenja, znati pripadnost odgovarajućih if i else
naredbi, jer vitičaste zagrade označavaju da umetnuta if-else naredba predstavlja prosti umetnuti
iskaz. Ta pripadnost nije očita u drugom, i posebno ne u trećem zapisu, iako su to sintaktički
potpuno ispravni C iskazi, jer se u C-jeziku koristi pravilo da "else naredba" pripada najbližem
prethodno napisanom "if uvjetu". Dobar je programerski stil da se umetnuti iskazi pišu u
odvojenim redovima s uvučenim početkom reda, kako bi se naglasilo da se radi o umetnutom
iskazu.


    “Uvlačenje redova” je stil pisanja programskih algoritama kojim se dobiva bolja
    preglednost strukture programskih iskaza.


Prije izneseni problem se može riješiti korištenjem samo jedne if-else naredbe:

      if (x >= 3 && x <= 9 )
         unutar = 1;
      else
         unutar = 0;

Pregledom ovog iskaza već se na prvi pogled može utvrditi koju radnju obavlja, jer se u početku
if naredbe ispituje puni interval pripadnosti x varijable.


    Često je moguće smanjiti broj umetnutih if-else naredbi uvođenjem prikladnih
    logičkih izraza.


     U programiranju se često pojavljuje potreba za višestrukim selekcijama. Primjerice,
dijagram toka na slici 7.3 prikazuje slučaj u kojem se ispituje više logičkih izraza (L1,L2,..Ln).
Ukoliko je ispunjen uvjet Li izvršava se naredba Ni, a ukoliko nije ispunjen ni jedan uvjet
izvršava se naredba Ne.




                                                                                               92
Slika 7.3 Dijagram toka za višestruku logičku selekciju

Programski se ovakva selekcija može realizirati pomoću umetnutih if-else naredbi u obliku:

      if (L1)      N1
      else if (L2) N2
      ......
      else if (Ln) Nn
      else         Ne


Primjer: U programu ifelseif.c od korisnika traži da odgovori na upit:

      Predjednik SAD je:
       (a) Bill Clinton
       (b) Bill Gates
       (c) Bill Third
           Otipkaj slovo.

Ako korisnik pritisne malo ili veliko slovo 'a', program ispisuje poruku "Točno". U slučaju (b) i
(c) poruka treba bit "Netočno". Ako pritisne slovo različito od 'a','b' ili 'c', tada se ispisuje
poruka "Otipkali ste pogrešno slovo". Za dobavu znaka s tipkovnice koristi se standardna
funkcija getchar(), koja vraća ASCII kôd pritisnte tipke.

      /* Datoteka: ifelseif.c ---------------*/
      /* Primjer višestruke logičke selekcije */
      #include <stdio.h>

      int main(void)
      {
        char ch;
        printf(" Predjednik SAD je:n";
        printf(" (a) Bill Clintonn (b) Bill Gatesn (c) Bill Thirdn");
        printf("nOtipkaj slovo.n");

          ch=getchar();
          if      (ch =='A' || ch =='a') printf              ("Tocnon");
          else if (ch =='B' || ch =='b') printf              ("Nije tocnon");
          else if (ch =='C' || ch =='c') printf              ("Nije tocnon");
          else    printf("Otipkali ste pogresno              slovo";

          return 0;
      }




                                                                                              93
Višestruko grananje (switch-case naredba)
     Prethodne selekcije su vršene na temelju ispitivanja logičke vrijednosti nekog izraza. Sada
će biti predstavljena switch-case naredba pomoću koje se selekcija grananja vrši na temelju
ispitivanja cjelobrojne vrijednosti nekog izraza kojeg se naziva selektorski izraz. Logika switch-
case naredbe je prikazana na slici 7.4.




                        Slika 7.4 Prikaz selekcije u switch-case naredbi

     U dijagramu toka "selektorski" izraz je označena sa sel. Skup {a,b,..z} podrazumjeva skup
različitih konstanti cjelobrojnog tipa.U slučaju da izraz sel poprimi vrijednost konstante “a”
izvršava se naredba Na. Ako izraz sel ima vrijednost konstante “b” izvršava se naredba Nb. Ako
izraz sel ima vrijednost iz skupa {z1,z2,..z3} izvršava se naredba Nz, a ako vrijednost
selektorskog izraza nije iz skupa {a,b,..,z1,z2,..z3} izvršava se naredba Nx. To se u C jeziku
zapisuje iskazom:


      switch (sel)
      {
      case a: Na;
               break;

      case b:     Nb;
                  break;
        ....
        case z1:
        case z2:
      case z3: Nz;
               break;

      default: Nx;
      }




                                                                                               94
Naredba break predstavlja naredbu skoka na prvu naredbu izvan aktivnog bloka. Ime break
(prekini) simbolički označava da naredba break "prekida" izvršenje naredbi u aktivnom bloku.
Ukoliko iza označene case-naredbe nije navedena break-naredba, nastavlja se ispitivanje
slijedeće case-naredbe. Ukoliko u ni jednoj case-naredbi nije pronađena vrijednost konstante
koja je jednaka vrijednosti selektorskog izraza, izvršava se naredba koja je označena s default.
Semantiku prethodnog iskaza pokazuje ekvivalentna if-else konstrukcija:

      if (sel == a)
           Na;
      else if(sel == b)
           Nb;
      else if (sel == z1 || sel == z2 || sel == z3 )
           Nz;
      else
           Nx;

U sljedećem primjeru dan je programski fragment kojim se ispituje vrijednost broja kojeg unosi
korisnik.

      unsigned x;
      scanf("%d", &x);
      switch (x)
      {
        case 1: printf("otkucali ste            broj 1")
                break;
        case 2: case 3: case 4: case            5:
                printf("otkucali ste            jedan od brojeva: 2,3,4,5");
                break;
        default:
                printf("otkucali ste            0 ili broj veći od 5");
      }

Sintaksa switch-naredbe je prema ANSI standardu dosta slobodno definirana:
      switch-naredba:
            switch ( izraz ) naredba

pa bi prema tom pravilu za naredbu mogla biti zapisana bilo koja naredba. Međutim, semantički
ima smisla koristiti samo tzv. označene-naredbe:

      case konstanti-izraz : naredba
       i
      default : naredba

te break naredbu, kojom se izlazi iz switch-bloka .

Prema prethodnom pravilu sintaktički je potpuno ispravna naredba

      switch(n) case 1: printf(" n je jednak 1n");

Njome je iskazano da će biti ispisano "n je jednak 1" u slučaju kada je n jednako 1. Ova
naredba nema praktičnog smisla, jer switch-naredba nije efikasno rješenje za ispitivanje samo
jednog slučaja. Tada je bolje koristiti if-naredbu. U slučaju kada se koristi više označenih
naredbi (što je redovit slučaj) sintaksa switch-case naredbe se može zapisati u znatno
razumljivijem obliku:




                                                                                             95
niz-case-naredbi:
    switch-case-naredba:                     case-naredba
         switch ( izraz ) {                 | niz-case-naredbi case-naredba
            niz-deklaracijaopt         case-naredba:
            niz-case-naredbi                 case konstanti-izraz : niz-naredbiopt prekidopt
            default-naredbaopt         default-naredba:
        }                                    default: niz-naredbiopt
                                       prekid:
                                          break ;


7.2.3 Naredbe iteracije - petlje
Iterativni procesi ili petlje su procesi u kojima se ciklički ponavlja programski kod koji je
definiran unutar petlje, sve dok za to postoje potrebni uvjeti. Uvjete ponavljanja ili izlaska iz
petlje postavlja programer. S obzirom na način kako su postavljeni uvjeti izlaska iz petlje,
definirani su slijedeći tipovi petlji:
       1. Petlje s uvjetnim izlazom na početku petlje
       2. Petlje s uvjetnim izlazom na kraju petlje
       3. Petlje s višestrukim uvjetnim izlazom
       4. Beskonačne petlje

Pokazat ćemo kako se ovi tipovi petlji realiziraju u C jeziku.




                                         Slika 7.5 Petlje
Za iskaze petlje, kao i za sve strukturalne iskaze, vrijedi da unutar njih mogu biti definirani svi
tipovi strukturalnih iskaza. Prema tome, unutar petlje može biti definiran proizvoljan broj
umetnutih petlji. Preklapanje strukturalnih iskaza nije dozvoljeno.

Petlja s uvjetnim izlazom na početku petlje (while i for petlje)
Sintaksa while naredbe glasi:

      while-naredba:
            while ( izraz ) naredba

Značenje je: dok je (eng. while) izraz različit od nule, izvršava se naredba. Izraz predstavlja
uvjet ponavljanja petlje.




                                                                                                96
Slika 7.6 Dijagram toka while petlje
Uvjet ponavljanja petlje se ispituju na samom ulazu u petlju. Postoji mogućnost da se naredba
uopće ne izvrši ako je početno izraz jednak 0 . Primjerice, za izračunavanje vrijednosti f (n) = n!
(n≥0), mogu se koristiti iskazi:

                       i = 1;                           i = n;
                       f = 1;                           f = 1;
                       while (i < n)                    while (i > 1)
                       {                                {
                          i++;                             f *= i;
                          f *= i;                          i–-;
                       }                                }

Ako je n<2 naredbe unutar while petlje se ne izvršavaju, stoga je prije početka petlje definirano
da f ima vrijednost 1. Kontrola izvršenja petlje obavlja se pomoću cjelobrojne kontrolne
varijable i, kojoj se vrijednost iterativno uvećava za 1 (u drugom iskazu se smanjuje za 1). U
principu, može se koristiti više kontrolnih varijabli.
     Prije početka while-petlje gotovo uvijek treba inicirati vrijednost neke kontrolne varijable,
koja se koristi u uvjetu ponavljanja petlje. Stoga se može napisati obrazac korištenja while-
petlje u obliku:

      iniciraj_kontrolne_varijable
      while ( izraz_s_kontrolnim_varijablama )
      {
             niz_naredbiopt
             naredba_promjene_vrijednosti_kontrolnih_varijabliopt
              niz_naredbiopt
      }

U nekim slučajevima je potrebno da naredba_promjene_vrijednosti_kontrolnih_varijabli bude
prva naredba u petlji, u nekim slučajevima ona će biti posljednja naredba ili pak umetnuta
naredba. Za zapis procesa u kojima je naredba_promjene_vrijednosti_kontrolnih_varijabli
posljednja naredba petlje često je prikladnije koristiti for – petlju.
   For-petlja se zapisuje tako da se iza ključne riječi for u zagradama zapišu tri izraza
međusobno odvojena točka-zarezom, a iza njih naredba koja čini tijelo petlje:

      for ( izrazopt ; izrazopt ; izrazopt )
          naredba




                                                                                                97
Primjerice, segment programa u kojem se računa n –faktorijela, se može zapisati pomoću for-
petlje u obliku:

      f=1
      for (i=n; i > 1; i--)
         f *= i;

     U prvom naredbenom izrazu se inicira kontrolna varijabla i na vrijednost n, u drugom
izrazu se zapisuje uvjet ponavljanja pelje (i>1), a u trećem izrazu se zapisuje naredbeni izraz
kojim se definira promjena kontrolne varijable pri svakom ponavljanju petlje(i--).

Semantiku for-petlje može se objasniti pomoću ekvivalentne while-petlje

      izrazopt ;
      while ( izrazopt ) {
                    naredba
                   izrazopt;
      }
    U svakom od ovih izraza može se navesti više izraza odvojenih zarezom. Primjerice, za
proračun n-faktorijela (f=n!) vrijede ekvivalentni iskazi:


      (1)      f=1;
               for(i=2; i<=n; i=i+1)
                   f=f*i;


      (2)      for(f=1, i=2;        i<=n;     i=i+1)
                   f=f*i;


      (3)      for(f=1, i=2; i<=n; f=f*i, i=i+1)
                   ;

     Drugi iskaz koristi listu izraza za početne uvjete, a u trećem je iskazu čak i naredba iz bloka
petlje f=f*i uvrštena u listu izraza iteracije. Samim time, naredba petlje je transformirana u
nultu naredbu (tj. naredbu koja stvarno ne postoji, ali je sintaktički prisutna postavljanjem
znaka točka-zarez kao znaka za završetak naredbe).
     Ovaj primjer ujedno ukazuje na jednu od najčešćih pogrešaka pri pisanju programa u C
jeziku, a to je u slučaju kada se točka-zarez napiše odmah iza zagrada for naredbe. Time prestaje
djelovanje for petlje na naredbu koja je definirana iza zagrada jer točka-zarez označava kraj
naredbe makar to bila i nulta naredba. Ista pogreška se često javlja kod zapisa if i while naredbi.


     Postavlja se pitanje: kada koristiti for-naredbu, a kada koristiti while-naredbu. U C
     jeziku je to pitanje jezičkog stila, jer su to dvije ekvivalentne naredbe.


Petlje s uvjetnim izlazom na kraju petlje ( do-while naredba)
Sintakse do-while naredbe je:

      do-while-naredba:
           do naredba while ( izraz ) ;




                                                                                                 98
Izraz predstavlja uvjet za ponavljanje petlje. Značenje je: izvrši naredbu, a zatim ponavljaj tu
naredbu dok je izraz logički istinit. Temeljna karakteristika ove naredbe je da se naredbe u tijelu
petlje izvršavaju barem jedan put.




                            Slika 7.7 Dijagram toka do-while petlje

Primjer: Izrada jednostavnog izbornika.


      /* Datoteka: do-while.c
       * Primjer do while petlje
       *****************************/

      #include <stdio.h>

      int main(void)
      {
        char ch;
        do
        {
          printf("Predjednik SAD je:n");
          printf("(1) Bill Clintonn(2) Bill Gatesn(3) Bill Thirdn");
          printf("nOtipkaj 1, 2 ili 3 <enter>!n");
          ch = getchar();
        }while( ch != '1' && ch != '2'&& ch != '3');

          if(ch == '1')       printf("Tocnon");
          else                printf("Nije tocnon");

          return 0;
      }



Naredbe za prekid i djelomično izvršenja petlje (break i continue)
    U C jeziku iskaz break; predstavlja i naredbu za prekid petlje, a iskaz continue; predstavlja
naredbu za povrat na početak petlje. Logika break iskaza je:

 Pseudo-asembler                          C jezik

 L1: početak petlje                       početak petlje {
     .......                                .......




                                                                                                99
if (L) goto L2;                         if (L) break;
     .......                                 .......
     kraj petlje                           } kraj petlje
 L2: ........                              ......


Prekid petlje, sam za sebe nema nikakovog smisla već se uvijek iskazuje u sklopu neke uvjetne
naredbe. Ukoliko postoji više umetnutih petlji, prekida se izvršenje samo one unutarnje petlje u
kojoj je definiran iskaz prekida.

Logika continue iskaza je:

 Pseudo-asembler                                     C jezik
 L1: početak petlje                                       početak petlje {
       .......                                               .......
       if (L) goto L1;                                       if (L) continue;
       .......                                               .......
     kraj petlje                                          } kraj petlje



Bekonačne petlje
Ukoliko je u stukturi petlje uvjet za ponavljanje petlje uvijek istinit, kao u iskazu

      while (1) { N }

dobije se tzv. beskonačna pelja Ona očito ne predstavlja suvislu algoritamsku strukturu jer je
trajanje njenog izvršenja beskonačno, to je struktura koja ima ulaz ali nema izlaza. S
programskog pak stajališta beskonačne petlje imaju smisla kod onih programa kod kojih se
ciklički ponavlja jedan ili više procesa za vrijeme dok je računalo uključeno. Primjerice, jedan
takovi program je i operativni sustav računala. Od beskonačne petlje se uvijek može dobiti
petlja koja ima izlaz, ako se u zapis bekonačne petlje doda uvjetna naredba za prekid petlje
(pomoću break ili goto naredbe). Primjerice, ako sekvencu petlje N čine dva iskaza B1 i B2,
tada

      while (1) {
        B1
        if (L) break;
        B2
      }

predstavlja petlju s uvjetnim izlazom unutar same petlje.

Beskonačna petlja se može realizirati i pomoću for petlje:

      for(;;)
      {
        /* beskonačna petlja */
      }


Primjer: U programu cont.c korisnik unosi niz znakova, završno sa <enter>. Program
koristi break i continue naredbe u beskonačnoj pelji, koja se izvršava sve dok se ne otkuca 5
malih slova. Nakon toga program ispisuje tih 5 malih slova. Ako se otkuca <enter> prije nego
je uneseno 5 malih slova program se prekida i ispisuje poruku: "PREKINUT UNOS".




                                                                                            100
/* Datoteka: cont.c
 * filtrira unos znakova s tipkovnice
 * tako da se propušta prvih 5 malih slova
 */
#include<stdio.h>
#include<ctype.h>

int main()
{
char slovo;
int i;     /* i registrira broj malih slova */
  printf ("Upisite niz znakova i <enter>: ");
  i= 0;
  while(1) /* ili for(;;) */
  {
      slovo= getchar();
      if(slovo == 'n')
      {
            printf("nPREKINUT UNOS!n");
         break;
      }
      if (islower(slovo))
      {
            i++;
         printf("%c", slovo);
      }
      if(i<=5)
         continue;
      else
      {
         printf("UNOS OK!");
         break;
      }
  }
}




                                                 101
8 Nizovi


Naglasci:
   • jednodimenzionalni nizovi
   • inicijalizacija nizova
   • višedimenzionalni nizovi
   • prijenos nizova u funkcije

     U ovom je poglavlju opisano kako se formiraju i koriste nizovi. Rad s nizovima je
"prirodni" način korištenja računala, jer memorija računala nije ništa drugo nego niz bajta. U
programiranju, kao i u matematici, zanimaju nas nizovi kao kolekcija istovrsnih elemenata koji
su poredani jedan za drugim.

    Elementi niza su varijable koje se označavaju indeksom:
      ai           označava i-ti element niza u matematici
      a[i]         označava i-ti element niza u C jeziku          i=0,1,2....



8.1 Jednodimenzionalni nizovi
8.1.1 Definiranje nizova
    Niz je imenovana i numerirana kolekcija istovrsnih objekata koji se nazivaju elementi niza.
Elementi niza mogu biti prosti skalarni tipovi i korisnički definirani tipovi podataka.
Označavaju se imenom niza i cjelobrojnom izrazom – indeksom – koji označava poziciju
elementa u nizu. Indeks niza se zapisuje u uglatim zagradama iza imena niza. Primjerice, x[3]
označava element niza x indeksa 3.
Sintaksa zapisa elementa jednodimenzionalnog niza je

      element_niza:        ime_niza [ indeks ]
      indeks:              izraz_cjelobrojnog_tipa

Prvi element niza ima indeks 0, a n-ti element ima indeks n-1. Prema tome, x[3] označava
četvrti element niza.
    S elementima niza se manipulira kao s običnim skalarnim varijablama, uz uvjet da je
prethodno deklariran tip elemenata niza. Sintaksa deklaracije jednodimenzionalnog niza je:

      deklaracija_niza:
                oznaka_tipa ime_niza [ konstantni_izraz ] ;

Primjerice, deklaracijom

      int A[9];

definira se A kao niz od 9 elementa tipa int.




                                                                                           102
Deklaracijom niza rezervira se potrebna memorija, na način da Memorijski raspored
                                                              niza A
elementi niza zauzimaju sukcesivne lokacije u memoriji.
Vrijedi pravilo:                                                    adresa      sadržaj
      adresa(A)=adresa(A[0])                                        1000        data[0]
                                                                    1004        data[1]
      adresa(A[n])=adresa(A[0]) + n*sizeof(A[0]))                   1008        data[2]
                                                                    1012        data[3]
Elementima niza se pristupa pomoću cjelobrojnog indeksa,            1016        data[4]
primjerice:                                                         1020        data[6]
                                                                    1024        data[5]
      A[0] = 7;                                                     1028        data[7]
      int i=5;                                                      1032        data[8]
      A[2]= A[i];
      for(i=0; i<9; i++)                                          Napomena:    int   zauzima
          printf("%d ", A[i];                                     4 bajta




     U C jeziku se ne vrši provjera da li je vrijednost indeksnog izraza unutar deklariranog
intervala. Primjerice, iskaz:

      A[12] = 5;

je sintaktički ispravan i kompilator neće dojaviti grešku. Međutim, nakon izvršenja ove naredbe
može doći do greške u izvršenju programa, ili čak do pada operativnog sustava. Radi se o tome
da se ovom naredbom zapisuje vrijednost 5 na memorijsku lokaciju za koju nije rezervirano
mjesto u deklaraciji niza.

Primjer: U programu niz.c pokazano je kako se niz koristi za prihvat veće količine podataka -
realnih brojeva. Zatim, pokazano je kako se određuje suma elemenata niza te vrijednost i indeks
elementa koji ima najveću vrijednost.

      /* Datoteka: niz1.c */
      #include <stdio.h>

      #define N      5

      int main()
      {
         int i, imax;
         double suma, max;
         double A[N];   /* niz od N elemenata */


          /* 1. izvjesti korisnika da otkuca 5 realnih brojeva                    */
            printf("Otkucaj %d realnih brojeva:n", N);

             for (i=0; i<N; i++)
                  scanf("%lg", &A[i]);

          /* 2. izračunaj sumu elemenata niza */
            suma = 0;
            for (i=0; i<N; i++)




                                                                                           103
suma += A[i];

             printf("Suma unesenih brojeva je %fn", suma);

          /*3.odredi indeks(imax) i vrijednost(max) najvećeg elementa */
           imax = 0;
           max = A[0];
           for(i=1; i<N; i++) {
                if(A[i] > max ) {
                     max = A[i];
                     imax=i;
                }
           }
         printf ("%d. element je najveci (vrijednost mu je %f)n",
      imax+1, max);
         return 0;
      }


Izvršenje programa može izgledati ovako:

      Otkucaj 5 realnih brojeva:
      5 6.78 7.1 8 0.17
      Suma unesenih brojeva je 27.050000
      4. element je najveci (vrijednost mu je 8.000000)

8.1.2 Inicijalizacija nizova
     Za globalno i statičko deklarirane nizove automatski se svi elementi postavljaju na
vrijednost nula. Kod lokalo deklariranih nizova ne vrši se inicijalizacija početnih vrijednosti
elemenata niza. To mora obaviti programer. Za inicijalizaciju elemenata niza na neku vrijednost
često se koristi for petlja, primjerice naredba

      for (i = 0; i < 10; i++)
        A[i] = 1;

sve elemente niza A postavlja na vrijednost 1.

Niz se može inicijalizirati i s deklaracijom sljedećeg tipa:

      int A[9]= {1,2,23,4,32,5,7,9,6};

Lista konstanti, napisana unutar vitičastih zagrada, redom određuje početnu vrijednost
elemenata niza.

Ako se inicijaliziraju svi potrebni elementi niza, tada nije nužno u deklaraciji navesti dimenziju
niza. Primjerice,

      int A[]= {1,2,23,4,32,5,7,9,6};

je potpuno ekvivalentno prethodnoj deklaraciji. Broj elemenata ovakvog niza uvijek se može
odrediti pomoću iskaza:

      int brojelemenata = sizeof(A)/sizeof(int);

Niz se može i parcijalno inicijalizirati. U deklaraciji

      int A[10]= {1,2,23};




                                                                                              104
prva tri elementa imaju vrijednost 1, 2 i 23, a ostale elemente prevodilac postavlja na vrijednost
nula.
Kada se inicijalizira znakovni niz, tada se u listi inicijalizacije mogu navesti znakovne
konstante:

      char znakovi[2]= {'O','K'};

Primjer: U programu hex.c korisnik unosi cijeli broj bez predznaka, zatim se vrši ispis broja u
heksadecimalnoj notaciji.


      /* Datoteka: hex.c
       * ispisuje broj, kojeg unosi korisnik, u heksadecimalnoj notaciji
       */
      #include <stdio.h>

      int main()
      {
      int num, k;
      unsigned broj;
      char hexslova []= {'0', '1', '2', '3', '4', '5', '6', '7',
                         '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'} ;
      char reverse[8] = {0}; /* zapis do 8 heksa znamenki u reverznom
      redu */

            printf("Otkucaj cijeli broj bez predznaka: ");
            scanf("%u", &broj);
            if(broj <0) broj = -broj;
            printf("Heksadecimalni zapis je: ");

            num=0;
            do {
                k = broj % 16;                              /* iznos heksa znamenke */
                reverse[num++] = hexslova[k];               /* oznaka heksa znamenke */

                broj /= 16;                                 /* odstrani ovu znamenku */
            } while(broj != 0);

            /* num sadrži broj heksa znamenki */
            /* ispis od krajnjeg (n-1) do nultog znaka */

            for(k=num-1; k>=0; k--)
                printf("%c", reverse[k]);

            printf("n");
            return 0;
      }


Pri izvršenju programa ispis može biti:

      Otkucaj cijeli broj bez predznaka: 256001
      Heksadecimalni zapis je: 3E801


     Algoritam pretvorbe u heksadecimalnu notaciju je jednostavan. Temelji se na činjenici da
ostatak dijeljenja s 16 daje numeričku vrijednost heksadecimalne znamenke na mjestu
najmanjeg značaja. Ta se vrijednost (k=0..15) korisi kao indeks znakovnog niza




                                                                                              105
hexslova[k]. Vrijednost hexslova[k] je znak odgovarajuće heksadecimalne znamenke,
kojeg se pridjeljuje nizu reverse[] ( u njemu će na kraju biti zapisana heksadecimalna
notacija broja, ali obrnutm redoslijedom). Zatim se broj dijeli s 16 i dobavlja sljedeća
heksadecimalna znamenka. Taj se proces ponavlja sve dok je rezultat dijeljenja različit od nule.
Ispis broja u heksadecimalnoj notaciji se vrši tako da se ispiše sadržaj znakovnog niza reverzno,
počevši od znamenke najvećeg značaja, završno s znamenkom najmanjeg značaja (koja se u tom
nizu nalazi na indeksu 0).
Primjer: Histogram
    U datoteci "ocjene.dat", u tekstualnom obliku, zapisane su ocjene u rasponu od 1 do 10.
Sadržaj datoteke "ocjene.dat” neka čini sljedeći niz ocjena:


      2 3 4 5 6 7 2 4 7 8 9 1 3 4 6 9 8 7 2 5 6 7 8 9
      3 4 5 1 7 3 4 10 10 9 10 5 7 6 3 8 9 4 5 7 3 4 6 1
      2 9 6 7 8 5 4 6 3 2 4 5 6 1 3 4 6 9 10 7 2 10 7 8 1


    Zadatak je izraditi program imena hist.c pomoću kojeg se prikazuje histogram ocjena u
10 grupa (1, 2,.. 10), i srednja vrijednost svih ocjena. Podaci iz datoteke "ocjene.dat" se predaju
programu "hist.exe" preusmjeravanjem standardnog ulaza (tipkovnice) na datoteku "ocjene dat".
To se vrši iz komandne linije komandom:

c:mydir> hist < ocjena.dat.

Na ovaj način se pristupa sadržaju datoteke kao da je otkucan s tipkovnice.

      /* Datoteka: hist.c */
      #include <stdio.h>

      int main(void)
      {
      int i, ocjena, suma;
      int izbroj[11] = {0};           /* niz brojača ocjena */

      /* izbroj[0] sadrži ukupan broj ocjena
       * izbroj[i] sadrži podatak koliko ima ocjena veličine i.
       * Početno, elementi niza imaju vrijednost 0
       */

         while (scanf("%d", &ocjena) != EOF)
         {
            izbroj[ocjena]++; /* inkrementiraj brojač ocjena */
            izbroj[0]++;      /* inkrementiraj brojač broja ocjena */
         }

         printf("Ukupan broj ocjena je %dn", izbroj[0]);

         /* ispiši histogram - od veće prema manjoj ocjeni*/
         for (i = 10; i > 0; i--)
         {
            int n = izbroj[i];     /* n je ukupan broj ocjena iznosa i */
            printf("%3d ", i);    /* ispiši ocjenu, a zatim */
            while (n-- > 0)       /* ispiši n zvjezdica */
               printf("*");
            printf("n");




                                                                                               106
}

          /* izračunaj sumu svih ocjena */
          suma =0;
          for (i = 1; i < 11; i++)
             suma += izbroj[i]*i;

          /*ispiši srednju ocjenu */
          printf ("Srednja ocjena je %4.2fn", (float)suma / izbroj[0]);
          return 0;
      }


Program se poziva komandom:

      c:>hist < ocjene.dat

Dobije se ispis:

      Ukupan broj ocjena je 73
      10 *****
       9 *******
       8 ******
       7 **********
       6 *********
       5 *******
       4 **********
       3 ********
       2 ******
       1 *****
      Srednja ocjena je 5.79

Analiza programa hist.c:
     Prvo je deklariran niz izbroj od 11 elemenata tipa int, a inicijaliziran je na vrijednost 0.
Program će u tome nizu bilježiti koliki je broj ocjena neke vrijednosti (izbroj[1] će
sadržavati broj ocjena veličine 1, izbroj[2] će sadržavati broj ocjena veličine 2, itd., a
izbroj[0] sadrži ukupan broj ocjena).
     Bilježenje pojavnosti neke ocjene se dobije inkrementiranjem brojača izbroj[ocjena].
Ocjene se dobavljaju preusmjerenjem datoteke na standardni ulaz. Za unos pojedine ocjene
koristi se scanf() funkcija sve dok se na ulazu ne pojavi znak EOF (end-of-file), koji znači
kraj datoteke. Nakon toga se ispisuje histogram na način da se uz oznaku ocjene ispiše onoliko
zvjezdica koliko je puta ta ocjena zabilježena u nizu brojača ocjena.
     Na kraju se računa srednja vrijednost ocjena na način da se suma svih ocjena podijeli s
ukupnim brojem ocjena. Uočite da je u naredbi

          printf ("Srednja ocjena je %4.2fn", (float)suma / izbroj[0]);

izvršena eksplicitna pretvorba tipa operatorom (float), jer srednja vrijednost može biti realni
broj.


8.2 Prijenos nizova u funkciju
    Nizovi mogu biti argumeti funkcije. Pri deklaraciji ili definiranju funkcije formalni
argument, koji je tipa niza, označava se na način da se deklarira niz bez oznake veličine niza, tj.
u obliku




                                                                                               107
tip ime_niza[]

Pri pozivu funkcije, kao stvarni argument, navodi se samo ime niza bez uglatih zagrada.

Primjer: u programu prod.c korisnik unosi niz od N realnih brojeva. Nakon toga, program
računa produkt svih elemenata niza, pomoću funkcije produkt(), i ispisuje rezultat.

      /* Datoteka: prod.c */
      /* Računa produkt elemenata niza od 5 elemenata */
      #include <stdio.h>

      #define N      5                  /* radi s nizom od N elemenata */

      double produkt(double A[], int brojelemenata)
      {
           int i;
           double prod = 1;
           for (i=0; i<brojelemenata; i++)
                prod *= A[i];
           return prod;
      }

      int main()
      {
      int i;
      double A [N];

       /* 1. izvjesti korisnika da otkuca 5 realnih brojeva                       */
          printf("Otkucaj %d realnih brojeva:n", N);

            for (i=0; i<N; i++)
                 scanf("%lg", &A[i]);

           /* 2. izračunaj sumu elemenata niza */
             printf("Suma unesenih brojeva je %gn", produkt(A, N));
          return 0;
      }



    Uočite de se vrijednost elemenata niza može mijenjati unutar funkcije. Očito je da se niz ne
prenosi po vrijednosti (by value), jer tada to ne bi bilo moguće.


      Pravilo je: U C jeziku se nizovi – i to samo nizovi – u funkciju prenose kao
      memorijske reference (by reference), odnosno prenosi se adresa početnog elementa
      niza. Brigu o tome vodi prevodilac. Memorijska referenca (adresa) varijable se
      pamti "u njenom imenu" stoga se pri pozivu funkcije navodi samo ime, bez uglatih
      zagrada.


      Nizovi, koji su argumenti funkcije, ne smiju se unutar funkcije tretirati kao lokalne
      varijable. Promjenom vrijednosti elementa niza unutar funkcije ujedno se mijenja
      vrijednost elementa niza koji je u pozivnom programu označen kao stvarni
      argument funkcije.




                                                                                              108
Primjer: Prijašnji primjer programa za histogram ocjena bit će modificiran, na način da se
definira tri funkcije: pojavnost ocjena u nizu izbroj bilježit će se funkcijom registriraj(), crtanje
histograma vršit će funkcija histogram(), a proračun srednje vrijednosti vršit će funkcija
srednja_ocjena().

      /* Datoteka: histf.c */
      #include <stdio.h>

      void registriraj( int ocjena, int izbroj[])
      {
          izbroj[ocjena]++;
          izbroj[0]++;
      }

      void histogram(int izbroj[])
      {
        int n,i;
        for (i = 10; i > 0; i--) {
           printf("%3d ", i);
           n=izbroj[i];
           while (n-- > 0)
               printf("*");
           printf("n");
        }
      }

      float srednja_ocjena(int izbroj[])
      {
        /* izračunaj sumu svih ocjena */
        int i, suma =0;
        for (i = 1; i <= 10; i++)
           suma += izbroj[i]*i;
        return (float)suma / izbroj[0];
      }

      int main(void)
      {
        int i, ocjena, suma;
        int izbroj[11]={0};

          while (scanf("%d", &ocjena) != EOF)
             registriraj(ocjena, izbroj);
          histogram(izbroj);
          printf ("Srednja ocjena je %4.2fn",srednjaocjena(izbroj));
          return 0;
      }


Primjer: Često je potrebno odrediti da li u nekom nizu od N elemenata postoji element
vrijednosti x. U tu svrhu zgodno je definirati funkciju

      int    search(int A[], int N, int x);

koja vraća indeks elementa niza A koji ima vrijednost x. Ako ni jedan element nema vrijednost
x, funkcija vraća negativnu vrijednost -1. Implementacija funkcije je:

      int    search (int A[], int N, int x)
      {




                                                                                                 109
int indx;
             for(indx = 0; indx < N; indx++) {
                  if( A[indx] == x) /* element pronađen – prekini */
                      break;
             }
             if(indx == N) /* tada ni jedan element nema vrijednost x*/
                  return -1;
             else
                return indx;
      }

Uočite, ako u nizu postoji više elemenata koji imaju vrijednost x, vraća se indeks prvog
pronađenog elementa.


8.3 Višedimenzionalni nizovi
    Višedimenzionalnim nizovima se pristupa preko dva ili više indeksa. Primjerice,
deklaracijom:

      int x[3][4];

definira se dvodimenzionalni niz koji ima 3 x 4 = 12 elemenata. Deklaraciju se može čitati i
ovako: definiran je niz kojem su elementi 3 niza s 4 elementa tipa int.

Dvodimenzionalni nizovi se često koriste za rad s          Memorijski raspored niza
matricama. U tom slučaju nije potrebno razmišljati o        adresa      sadržaj
tome kako je niz složen u memoriji, jer se elementima
                                                              1000          x[   0][   0]
pristupa preko dva indeksa: prvi je oznaka retka, a           1004          x[   0][   1]
drugi je oznaka stupca matrice.                               1008          x[   0][   2]
                                                              1012          x[   0][   3]
      Matrični prikaz niza je:                                1016          x[   1][   0]
x[0][0]     x[0][1]     x[0][2]      x[0][3]                  1020          x[   1][   1]
x[1][0]     x[1][1]     x[1][2]      x[1][3]                  1024          x[   1][   2]
x[2][0]     x[2][1]     x[2][2]      x[2][3]                  1028          x[   1][   3]
                                                              1032          x[   2][   0]
                                                              1036          x[   2][   1]
                                                              1040          x[   2][   2]
Memorijski raspored elemenata dvodimenzionalnog               1044          x[   2][   3]
niza, koji opisuju neku matricu, je takovi da su
elementi složeni po redovima matrice; najprije prvi
redak, zatim drugi, itd..


Višedimenzionalni niz se može inicijalizirani već u samoj deklaraciji, primjerice

      int x[3][4] = { { 1, 21, 14, 8},
                      {12, 7, 41, 2},
                      { 1, 2, 4, 3} };

Navođenje unutarnjih vitičastih zagrada je opciono, pa se može pisati i sljedeća deklaracija:

      int x[3][4] = {1, 21, 14, 8, 12, 7, 41, 2, 1, 2, 4, 3};

Ovaj drugi način inicijalizacije se ne preporučuje, jer je teže uočiti raspored elemenata po
redovima i stupcima.




                                                                                                110
Elementima se pristupa preko indeksa niza. U sljedećem primjeru računa se suma svih
elemenata matrice x;

      int i, j, brojredaka=3, brojstupaca=4;
      int sum=0;
      for (i = 0; i < brojredaka; i++)
         for (j = 0; i < brojstupaca; j++)
             sum += x[i][j];

      printf("suma elemenata matrice = %d", sum);


Prijenos višedimenzionalnih nizova u funkciju
    Kod deklariranja parametara funkcije, koji su višedimenzionalni nizovi pravilo je da se ne
navodi prva dimenzija (kao i kod jednodimenzionalnih nizova), ali ostale dimenzije treba
deklarirati, kako bi program prevodilac "znao" kojim su redom elementi složeni u memoriji.

Primjer: Definirana je funkcija sum_mat_el() kojom se računa suma elemenata
dvodimenzionalne matrice, koja ima 4 stupca:

      int sum_mat_el(int x[][4], int brojredaka)
      {
      int i, j, sum=0;
        for (i = 0; i < brojredaka; i++)
          for (j = 0; i < 4; j++)
             sum += x[i][j];
        return sum;
      }


    Uočite da je u definiciji funkcije naveden i argument koji opisuju broj redaka matrice. Broj
stupaca je fiksiran već u deklaraciji niza na vrijednost 4. Očito da ova funkcija ima ograničenu
upotrebu jer se      može primijeniti samo na matrice koje imaju 4 stupca. Kasnije, pri
proučavanju upotrebe pokazivačkih varijabli, bit će pokazano kako se ova funkcija može
modificirati tako da vrijedi za matrice proizvoljnih dimenzija.




                                                                                            111
9 Blokovi, moduli i dekompozicija
programa


Naglasci:
    •   blok struktura programa
    •   lokalne i globalne varijable
    •   automatske i statičke varijable
    •   programski moduli i biblioteke
    •   "skrivanja podataka"
    •   dekompozicija programa "od vrha prema dolje"
    •   igra "točkice i kružići"


9.1 Blokovska struktura programa
    Ukoliko se u programiranju ne koristi goto naredba, programi tada imaju prepoznatljivu
blokovsku strukturu. Praksa je pokazala da se time dobiju “čitljivi” programi, koje je lako
održavati i dograđivati.
Blokovska struktura C programa ima četiri razine:

    •   razina datoteke (temeljna kompilacijska jedinica)
    •   razina definicije (tijela) funkcije
    •   razina bloka kontrolnih struktura (sekvenca, iteracija, selekcija)
    •   razina bloka koji je omeđen vitičastim zagradama

Blok niže razine može biti umetnut unutar bloka više ili iste razine proizvoljan broj puta, jedino
se ne smije vršiti definicija funkcije unutar tijela neke druge funkcije.
    Cilj je programirati tako da svaki blok predstavlja cjelinu koja je što manje ovisna o ostatku
programa. Da bi se to postiglo potrebno je dobro razumjeti pravila dosega identifikatora i
postojanosti varijabli.

9.1.1 Doseg
     Doseg nekog identifikatora (eng. scope) je dio programa u kojem se taj identifikator može
koristiti.
     Deklaracijom argumenata funkcije i lokalnih varijabli stvaraju se novi identifikatori. Za
njih vrijede sljedeća pravila dosega:
    •   Doseg argumenata funkcije je tijelo funkcije.
    •   Doseg lokalnih varijabli se proteže od mjesta deklariranja do kraja složenog iskaza koji
        je omeđen vitičastim zagradama.
    •   Identifikatori s različitim područjima dosega, iako mogu imati isto ime, međusobno su
        neovisni.
    •   Nisu dozvoljene deklaracije s istim imenom u istom dosegu. Primjerice,




                                                                                              112
float epowx( float x, float epsilon)
      {
        int x; /* greška, ime x je već pridjeljeno parametru funkcije*/
        ...
      }

    U sljedećem primjeru varijable x i ex u funkciji main(), te x i ex u funkciji my_exp()
su neovisne varijable iako imaju isto ime.

      int main( void)
      {
         double eps, x, ex;
         ...                                                          doseg ex, x
         return 0;
      }

      float my_exp( double x, double epsilon) {                        doseg x
        int i;
        double ex = 1.0, preth_ex = 0.0, … ;
               ...
        doseg ex
        return ex;
      }

    Lokalne deklaracije imaju prednost nad vanjskim deklaracijama. Kažemo da lokalna
deklaracija prekriva vanjsku deklaraciju. Primjer:

      f( int x, int a)
      {
         int y, b;
         y = x + a* b;

          if (...)
          {
            int a, b; /* a prekriva parameter a /*
            ...        /* b prekriva lokalnu var. b iz vanjskog dosega */
            y = x + a* b;
          }
      }

Uobičajeno se smatra da nije dobar stil programiranja kada se koriste ista imena u preklopljenim
dosezima, iako je to sintaktički dozvoljeno.

9.1.2 Automatske i statičke varijable
     Lokalne varijable imaju ograničeno vrijeme postojanja, pa se nazivaju i automatske
varijable. One nastaju (u memoriji) pozivom funkcije u kojoj su deklarirane, a nestaju (iz
memorije) nakon povrata u pozivnu funkciju.
     Kada se argumenti prenose u funkcije može se uzeti da se tada vrijednost stvarnih
argumenata kopira u formalne argumente, koji pak imaju lokalni doseg. Argumenti funkcije se
inicijaliziraju kao lokalne varijable, pa za njihovu upotrebu vrijede pravila kao za lokalne
varijable. To je ilustrirano u programu doseg.c.

      /* Datoteka doseg.c */
      #include <stdio.h>




                                                                                            113
void f( int a, int x)
      {
         printf(" a = %d, x = %dn",a, x);
         a = 3;
         {
           int x = 4;
           printf(" a = %d, x = %dn", a, x);
         }
         printf(" a = %d, x = %dn", a, x);
         x = 5; /*nema nikakovi efekt*/
      }

      int main( void)
      {
        int a = 1, b = 2;
        f( a, b);
        printf(" a = %d, b = %dn", a, b);
        return 0;
      }


      c:> cl args.c
      c:>args
      a = 1, x = 2
      a = 3, x = 4
      a = 3, x = 2
      a = 1, b = 2

    Ako se lokalna varijabla deklarira s prefiksom static, tada se za tu varijablu trajno
rezervira mjesto u memoriji (postoji i nakon izvršenja funkcije), iako je njen doseg ograničen
unutar tijela funkcije.
    U sljedećem primjeru opisana je funkcija incrCounter(), kojom se realizira brojač po
modulu mod. U funkciji je definirana statička varijabla count, čija se vrijednost inkrementira
pri svakom pozivu funkcije. Ako vrijednost postane jednaka argumentu mod, count se
postavlja na nulu.

      /* Datoteka: countmod3.c */

      #include <stdio.h>

      int incrCounter(int mod)
      {
         static int count=0;
         count++;
         if(count == mod)
             count = 0;
         return count;
      }

      int main(void)
      {
         int i,modul=3;
         for(i=0; i<=10; i++)
             printf("%d, ", incrCounter(modul));
         printf("...n");
         return 0;
      }




                                                                                          114
Dobije se ispis:

       1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2,...



    Važno je uočiti: kada se statička lokalna varijabla u deklaraciji i inicijalizira (pr. static
int count=0;), ta inicijalna vrijednost vrijedi samo pri prvom pozivu funkcije.


   Inicijalizacija nije ista kod lokalnih statičkih i lokalnih automatskih varijabli. Vrijednost
   lokalnih automatskih varijabli se inicijalizira na vrijednost opisanu inicijalizacijom pri
   svakom pozivu funkcije, a vrijednost lokalnih statičkih varijabli se inicijalizira na
   vrijednost opisanu inicijalizacijom samo pri prvom pozivu funkcije.


9.1.3 Globalne varijable
     Globalne varijable su varijable koje se deklariraju izvan tijela funkcije. To su
"permanentne" varijable koje trajno zauzimaju memoriju za vrijeme trajanja programa. One se
uvijek inicijaliziraju na vrijednost 0. Doseg globalnih varijabli je od točke definiranja do kraja
datoteke u kojoj su definirane. Kasnije će biti pokazano kako se njihov doseg može proširiti i na
druge datoteke

      int main( void)
      {
            …                  /* ovdje ne postoji varijabla max */
      }

      int max = 0;                      /* mjesto definicije varijable max */

      void fun( … )
      {
         … max =                        /* ovdje postoji globalna var. max */
      }

Argumenti funkcije i lokalne varijable prekrivaju globalne varijable, ako imaju isto ime.

      void fun( … )
      {
         int max; /* lokalna var. max prekriva globalnu var max */
         max = ...
      }

Primjer: Prethodni je program countmod3.c izmijenjen je na način da obje funkcije,
incrCounter() i main(), koriste globalnu varijablu count.

      /* Datoteka: count.c */
      #include <stdio.h>

      int count;       /* globalna varijabla,
                       /* inicijalizirana na vrijednost 0 */

      int incrCounter(int mod)
      {
         count++;
         if(count == mod)
            count = 0;




                                                                                               115
return count;
      }

      int main(void)
      {
        int i,modul=3;
      for(i=0; i<=10; i++)
      {
          incrCounter(modul);
            printf("%d, ", count);
        }
        printf("...n");
        return 0;
      }



Eksterne globalne varijable
    Ako se nekoj datoteci deklarira globalna varijabla, ona se može dosegnuti i iz drugih
kompilacijskih jedinica, ako se u tim datotekama deklarira kao eksterna (ili vanjska) varijabla.
Deklaracija eksterne varijable označava se prefiksom extern, primjerice:

      extern int max;
      void dump( … )
      {
         max = ...
      }

Primjer: Prethodni program brojača po modulu mod, napisan je u dvjema datotekama. U prvoj
datoteci, imena "counter.c", definirana je globalna varijabla count i funkcija incrCounter(). U
drugoj datoteci, imena "countmain.c", definirana je funkcija main() koji koristi vrijednost od
count i funkciju incrCounter().

      /* Datoteka1: counter.c */

      /* stanje brojača prije poziva
         funkcije counter */

          int count=0;

          incrCounter(int mod)
          {
            count++;
            if(count >= mod)
               count = 0;
      }

      /* Datoteka2: countmain.c */

      extern int count;
      void incrCounter(int mod);

      int main(void)
      {
        int i,mod=3;
        for(i=0; i<=10; i++)
        {
            incrCounter(mod);




                                                                                            116
printf("%d, ", count);
          }
          printf("...n");
          return 0;
      }


Program se kompilira komandom:

      c:>cl countmain.c counter.c

U komandnoj liniji se navode imena obje izvorne datoteke. Konačni izvršni program će imati
ime datoteke koja je prva zadana: countmod.exe. Rezultat izvršenja programa biti će isti kao i
u prethodnom primjeru.
Statičke globalne varijable
    Globalne varijable se također mogu deklarirati s prefiksom static. Takove varijable se
nazivaju statičke globalne varijable. One su vidljive samo u kompilacijskoj jedinici (datoteci)
unutar koje su i definirane. Ne može ih se koristiti u drugim datotekama.
    Zašto se koriste statičke globalne varijable? To će biti objašnjeno kada se objasni ideja
modularnog programiranja i princip "skrivanja podataka" (eng. data hidding).

9.1.4 Moduli
    Modul sadrži skup međuovisnih globalnih varijabli i funkcija zapisanih u jednoj ili više
datoteka. Moduli se obično formiraju u dvije grupe datoteka:
    1. datoteke specifikacije modula (ime.h ) sadrže deklaracije funkcija i (eksternih)
       globalnih varijabli koje su implementirane unutar modula.
    2. datoteke implementacije (ime.c ) sadrže definicije varijabli i funkcija

    Implementacijske se datoteke mogu kompilirati kao samostalne kompilacijske jedinice, a
dobiveni objektni kod se može pohraniti u biblioteku potprograma. Neposrednu korist od
ovakvog načina formiranja programa najbolje će pokazati sljedeći primjer.
Primjer: bit će realiziran modul koji sadrži funkcije brojača po modulu mod. Problem će biti
obrađen nešto općenitije nego u prethodnim primjerima. Najprije se vrši specifikacija modula.

1. Specifikacija modula je opisana u datoteci "counter.h".

      /* Datoteka: counter.h
      * specifikacija funkcija brojača po modulu mod
      */

      void reset_count(int mod);
      /* Funkcija: inicira brojač na početnu vrijednost nula
       * i modul brojača na vrijednost mod. Ako je mod<=1,
       * modul brojaca se postavlja na vrijednost INT_MAX
       */

      int getCount(void);
      /* Funkcija: vraća trenutnu vrijednost brojača */

      int getModulo(void);
      /* Funkcija: vraća trenutnu vrijednost modula brojača */

      int incrCounter(void);




                                                                                           117
/* Funkcija: incrementira vrijednost brojača za 1
       * Ako vrijednost brojača postane jednaka ili veća,
       * od zadamog modula vrijednost brojača postaje nula.
       * Vraća: trenutnu vrijednost brojača
       */



    Prema ovoj specifikaciji predviđeno je da se brojačem upravlja pomoću dvije funkcije:
incrCounter(), koja inkrementira brojač, i reset_count(), koja postavlja početno stanje
brojača. Stanje brojača se očitava pomoću funkcija getCount() i getModulo(). Nije
predviđeno da korisnik pristupa globalnim varijablama.
2. Implementacija modula je opisana u datoteci "counter.c". U toj datoteci su definirane dvije
statičke globalne varijable _count i _mod. Prefiks static znači da su one vidljive samo u
ovoj datoteci. Početno je vrijednost _mod postavljena na maksimalnu moguću cjelobrojnu
vrijednost. Zatim slijede definicije funkcija koje su određene specifikacijom.

      /* Datoteka: counter.c
       * Implementacija funkcija brojača po modulu: mod
       */
       #include <limits.h>          /* zbog definicija INT_MAX*/

      /* globalne varijable */

       static int _count = 0;                 /* početno stanje brojača */
       static int _mod = INT_MAX;             /*2147483647*/

       void resetCounter(int mod)
       {
          _count= 0;
          if(mod <= 1)   _mod = INT_MAX;
          else           _mod = mod;

       }

       int getCount(void) { return _count;}

       int getModulo(void){ return _mod; }

       int incrCounter(void)
       {
          _count++;
          if(_count >= _mod) _count = 0;
          return _count;
       }


3. Testiranje modula se vrši programom "testcount.c":

      /* Datoteka: testcount.c */

      #include <stdio.h>
      #include "counter.h"

      int main(void)
      {
        int i;
        resetCounter(5);




                                                                                          118
printf("Brojac po modulu %d n", getModulo());
          for(i=0; i<=10; i++)
          {
             incrCounter();
             printf("%d, ", getCount());
          }
          printf("...n");
          return 0;
      }

      c:>cl testcounter.c counter.c
      c:> testcounter
      Brojac po modulu 5
      1, 2, 3, 4, 0, 1, 2, 3, 4, 0, 1, ...

    Nakon što je modul testiran, može ga se primijetiti i u drugim programima. Veza između
glavnog programa i modula je opisana u deklaracijama specifikacijske datoteke "counter.h", pa
za primjenu modula nije potrebno znati kako je implementiran, već samo kako se koriste
njegove funkcije.
    Može se reći da modul opisuje apstraktni objekt brojača, koji sam nadgleda vlastito stanje.
Sve varijable koje su potrebne za definiranje stanja ovog objekta su deklarirane kao statičke, pa
nisu vidljive izvan modula. Ovaj se princip, “skrivanja varijabli” od korisnika modula, naziva
"data hidding" ili "data encapsulation", a neobično je popularan u softverskom inženjerstvu jer
se njime postiže neovisnost podataka iz različitih modula. Na ovaj je način lakše provoditi
projekte u kojima sudjeluje više programera.

9.1.5 Formiranje programskih biblioteka
Prethodni program se može kompilirati na sljedeći način:

      c:>cl testcounter.c counter.c

Kada se radi s modulima, često je zgodniji način razvoja programa da se modul, koji je testiran,
prevede u objektnu datoteku. To se vrši komandom:

      c:>cl –c counter.c

(Parametar komandne linije –c je poruka kompilatoru da se prijevod izvrši u strojni kod, bez
stvaranja izvršne datoteke). Nakon ove komande dobije se datoteka "counter.obj".

Sada se izvršni program može dobiti i pomoću komande:

      c:>cl testcounter.c counter.obj

Dobra strana ovakovog pristupa je da se ubrzava proces stvaranja programa, jer ne treba uvijek
iznova kompilirati datoteku "counter.c".

Više objektnih datoteka se može združiti u programske biblioteke. Uobičajeno, programske
datoteke imaju ekstenziju .lib (ili .a na Unixu). Pokazat ćemo kako se formira biblioteka
potprograma pomoću Microsoft program "lib.exe".
Pretpostavka je da želimo objektnu datoteku "counter.obj" uvrstiti u biblioteku koja se zove
"mylib.lib". To se ostvaruje komandom:

      c:>lib /OUT:mylib.lib          counter.obj

(/OUT: je parametar komandne linije iza kojeg se navodi ime biblioteke).




                                                                                             119
Izvršni se program može dobiti komandom:

         c:>cl testcount.c mylib.lib

Očito je da kompilator kôd za modul brojača dobiva iz biblioteke "mylib.lib".

Na sličan način formirana je standardna biblioteka C-jezika.


9.2 Funkcionalna dekompozicija programa "od vrha prema dolje"
    Kada se razvija neki program, polazište je zadatak kojeg treba obaviti. Analizom problema
određuju se radnje i algoritmi kojima se može taj zadatak obaviti. U proceduralnim je jezicima
najbolji način programske realizacije neke radnje da se ona specificira i implementira pomoću
neke funkcije.
    Radnje, odnosno zadaci, koje treba obaviti neka funkcija, također se mogu realizirati
pomoću niza funkcija. U tom slučaju kažemo da se razvoj programa vrši funkcionalnom
dekompozicijom "od vrha prema dolje". Praksa je pokazala da je funkcionalna dekompozicija
prihvatljiva kao metoda programiranja u većini slučajeva.
    Kao studijski primjer razvoja programa funkcionalnom dekompozicijom izradit ćemo
program pomoću kojeg se igra popularna igra "točkice i kružići" (ili tic-tac-toe).

Tic-Tac-Toe
Tic-Tac-Toe je igra u kojoj se nadmeću dva igrača. Igrači, jedan za drugim označavaju polja u 9
kvadratića, prvi s križićem, a drugi s točkicom. Pobjednik je onaj koji redno, stupčano ili
dijagonalno prvi ispuni 3 polja. Prvi igrač unosi križić, a drugi točkicu.

         Primjer igre u kojoj je pobijedio drugi    Označavanje pozicije kvadratića na
         igrač, tj. onaj koji unosi točkicu.        igračkoj ploči

             | o | X | o |                                | 1 | 2 | 3 |
             |---|---|---|                                |---|---|---|
             | X | o | X |                                | 4 | 5 | 6 |
             |---|---|---|                                |---|---|---|
             | o |   | X |                                | 7 | 8 | 9 |


Kako napisati program u kojem će jedan od igrača biti računalo, a drugi igrač je čovjek.
Problem se može formulirati na sljedeći način:

    1. Nacrtaj igraču ploču, i upute za igru.
    2. Postavi početno stanje (ploča je prazna).
    3. Ispitaj da li prvi potez vuče računalo ili čovjek.
    4. Prati igru sve dok se ne ostvari pobjednička kombinacija, ili dok se ne ispune svi
       kvadratići.
    5. Ako korisnik želi ponoviti igru vrati se na korak 2, inače završi program.

     Problem je postavljen sasvim općenito. Sada treba izvršiti razradu problema, tako da se
detaljnije opišu pojedini koraci u opisanom apstraktnom algoritmu. Pored postupaka koje treba
obaviti, neophodno je odrediti strukturu podataka, kojom će se pratiti stanje programa.
     U ovom slučaju, za stanje igrače ploče koristit će se znakovna matrica od 3x3 elementa,
deklarirana s




                                                                                           120
char kvadrat[3][3];

Pojedini element matrice može poprimiti samo tri vrijednosti: ' ', 'X' i 'o' (prazno, križić i
točkica).
Za označavanje igrača koristit će se dvije globalne varijable

      char racunalo, covjek;

prva će sadržavati znak koji unosi računalo, a druga znak koji unosi čovjek. Ako jedna od ovih
varijabli, primjerice covjek, ima vrijednost 'X', to znači da prvi potez vuče čovjek, inače, prvi
potez vuče računalo.
    Na temelju početnog algoritma može se zaključiti da se problem može realizirati pomoću 5
neovisnih funkcija. Specifikacija tih funkcija je sljedeća:

      void Upute(void);
      /* Ispisuje igraču ploču, i upute za igru */

      void OcistiPlocu(void);
      /* Postavlja početno stanje,
       * svi elementi matrice kvadrat imaju vrijednost ' '
       */

      void PostaviPrvogIgraca(void);
      /* Na temelju interakcije s korisnikom programa odlučuje da li
       * prvi potez vuče računalo ili čovjek.
       * Ako prvi potez vuče:   racunalo = 'o'; covjek = 'X';
       * inače: racunalo = 'X'; covjek = 'o';
       */

      void IgrajIgru(void);
      /* Ovom se funkcijom kontrolira tijek igre, interakcija s
       * korisnikom, odlučuje o potezima koje inteligentno izvršava
       * računalo i izvještava se o ispunjenosti igrače ploče. Funkcija
       * završava kada se ispune uvjeti za pobjedu prvog ili drugog
       * igrača ili ako su označeni svi kvadratići.
       */

      int PonoviIgru(void);
      /* Ovom funkcijom se od korisnika traži da potvrdi da li želi
       * ponoviti igru. Ako korisnik želi ponoviti igru tada funkcija
       * vraća 1, inače vraća vrijednost 0.
       */

Uz pretpostavku da će se kasnije uspješno implementirati ove funkcije, glavni program se može
napisati u obliku:

      /* Program TicTacToe.c */
      #include <stdio.h>

      void Upute(void);
      void OcistiPlocu(void);
      void PostaviPrvogIgraca(void);
      void IgrajIgru(void);
      int PonoviIgru(void);

      char kvadrat[3][3];
      char racunalo, covjek;




                                                                                             121
int main(void)
      {
          Upute();                            /* korak 1*/
          do
          {
             OcistiPlocu();                   /*   korak   2*/
             PostaviPrvogIgraca();            /*   korak   3.*/
             IgrajIgru();                     /*   korak   4.*/
          }while(PonoviIgru());               /*   korak   5.*/
          return 0;
      }

Što je do sada napravljeno?
Problem je rastavljen (dekomponiran) na pet manjih, međusobno neovisnih problema. Neki od
ovih problema se mogu odmah riješiti, primjerice funkcije Upute() i OcistiPlocu() se
mogu implementirati na sljedeći način:
void Upute()
{
  printf("nIgra - tic tac toe - krizici i tockicenn");
  printf("t | 1 | 2 | 3 | n");
  printf("t |---|---|---| n");
  printf("t | 4 | 5 | 6 | n");
  printf("t |---|---|---| n");
  printf("t | 7 | 8 | 9 | nn");
  printf("Cilj igre je ispuniti tri kvadratica u redu: n");
  printf("horizontalno, vertikalno ili dijagonalno. n"
  printf("Igra se protiv racunala.n");
  printf("Prvi igrac je oznacen krizicem X, a drugi tockicom o.nn");
}

void OcistiPlocu(void)
{
  int redak, stupac;
  for (redak = 0; redak < 3; redak++)
    for (stupac = 0; stupac < 3; stupac++)
      kvadrat[redak][stupac] = ' ';
}

Za ostale funkcije potrebna je daljnja dorada problema. Posebno opsežan problem je definiranje
funkcije IgrajIgru() u kojoj se obavlja više operacija. Prije definiranja te funkcije izvršit će
se definiranje funkcija PostaviPrvogIgraca() i PonoviIgru() jer je njihova
implementacija jednostavna i može se odrediti neposredno iz zadane specifikacije.
Dorada funkcije PostaviPrvogIgraca():
    1. Izvijesti korisnika da on bira tko će povući prvi potez. Ako želi biti prvi na potezu neka
       pritisne tipku 'D', inače neka pritisne tipku 'N'.
    2. Motri korisnikov odziv, sve dok se ne pritisne jedna od ove dvije tipke.
    3. Ako je pritisnuta tipka 'D', tada: racunalo = 'o'; covjek = 'X';
       inače: racunalo = 'X'; covjek = 'o';.

      void PostaviPrvogIgraca(void)
      {
        int key;
        printf("Da li zelite zapoceti prvi? (d/n)n");
        do {
           key = toupper(getchar());




                                                                                              122
} while ((key != 'D') && (key != 'N'));

          if (key == 'D') { racunalo = 'o';               covjek = 'X'; }
          else            { racunalo = 'X';               covjek = 'o'; }
      }

Dorada funkcije PonoviIgru():
    1. Upitaj korisnika da li želi ponoviti igru.
    2. Motri korisnikov odziv, sve dok se ne pritisne tipku 'D' ili 'N'.
    3. Ako je pritisnuta tipka 'D' funkcija vraća vrijednost 1, inače vraća vrijednost 0.

      int PonoviIgru(void)
      {
        int key;
        printf("Da li zelite ponovo igrati? (D/N) ");
        do {
            key = toupper(getchar());
        } while ((key != 'D') && (key != 'N'));
        return ( key == 'D');
      }

Dorada funkcije IgrajIgru():
Prvo treba uočiti da može biti maksimalno 9 poteza, jer ploča ima 9 kvadratića. Ako poteze
numeriramo od 1 do 9 onda vrijedi da prvi igrač vuče poteze koji su numerirani 1,3,5... Dakle,
kada je neparna vrijednost poteza, prvi igrač unosi 'X' u izabrani kvadratić, a kada je parna
vrijednost poteza drugi igrač unosi 'o'.

Operacije koje izvodi ova funkcija mogu se iskazati algoritmom:
     1. Za maksimalno 9 poteza
        1.1 Dobavi izbor aktivnog igrača (računalo ili korisnik)
        1.2 Nacrtaj ploču s točkicama i križićima
        1.3 Ako je pobjednik računalo ispiši: "Pobijedio sam te!" i prekini igru,
           inače, ako je pobjednik korisnik, ispiši: "Pobijedio si!" i prekini igru.
     2. Ako nije određen pobjednik ni nakon 9 poteza, ispiši poruku:
         "Ovaj put nema pobjednika"

Ovaj se algoritam može programski realizirati pomoću varijable potez, u kojoj se bilježi redni
broj poteza, i sljedeće tri funkcije:

      void DobaviPotez(int potez);
      /* na temelju rednog broja poteza određuje se koji je igrač
       * na potezu, dobavlja njegov izbor i
       * označava novo stanje matrice kvadrat
       */

      void NacrtajPlocu(void);
      /* crta ploču na temelju stanja matrice kvadrat */

      int PostajeDobitnik(char simbol);
      /* određuje da li simbol (X ili o) ispunja matricu na način da
       * je postignuta dobitnička kombinacija
       */

Uz pretpostavku uspješne implementacije ovih funkcija, može se funkcija IgrajIgru()
napisati u obliku:




                                                                                            123
void IgrajIgru(void)
     {
       int potez = 1;
       while (potez <= 9)
       {
          DobaviPotez(potez);
          NacrtajPlocu();

           if (PostajeDobitnik(racunalo)){
                printf("nPobijedio sam te!!!nn");
                break;
           }
           else if (PostajeDobitnik(covjek)) {
               printf("nCestitam, pobijedio si!nn");
               break;
           }
           potez++;
         }
         if (potez > 9)
              printf("nOvaj put nema pobjednika.nn");
     }

Realizacija funkcija DobaviPotez(), NacrtajPlocu() i PostajeDobitnik()
/* Funkcija: DobaviPotez(int potez)
 * Izbor se dobije tako da se utvrdi
 * da li je varijabla potez parna ili neparna.
 * Ako je parna, igra 'X', inače igra 'o'
 */
 void DobaviPotez(int potez)
 {
      if (potez % 2 == 1)
         if (racunalo == 'X') PotezRacunala();
         else PotezCovjeka();
     else
         if (racunalo == 'o') PotezRacunala();
         else PotezCovjeka();
}

/* Funkcija: NacrtajPlocu()
 * prikazuje igraču ploču na standardnom izlazu
 */
void NacrtajPlocu(void)
{
  int redak, stupac;
  printf("n");
  for (redak = 0; redak < 3; redak++)
  {
     printf("t| %c | %c | %c |n",
     kvadrat[redak][0], kvadrat[redak][1], kvadrat[redak][2]);
     if (redak != 2)
        printf("t|---|---|---|n");
  }
  printf("n");
  return;
}

/* Funkcija: int PostajeDobitnik(char simbol)
 * Provjera da li je simbol (X i o) pobjednik, ispitivanjem




                                                                         124
* ispunjenosti redaka, stupaca ili dijagonala ploče
 */
int PostajeDobitnik(char simbol)
{
  int redak, stupac;

    for (redak = 0; redak < 3; redak++) /* ispitajmo 3 retka */
    {
        if ( (kvadrat[redak][0] == simbol)
           && (kvadrat[redak][1] == simbol)
           && (kvadrat[redak][2] == simbol))
       return 1;
    }
    for (stupac = 0; stupac < 3; stupac++) /* ispitajmo 3 stupca */
    {
        if ( (kvadrat[0][stupac] == simbol)
           && (kvadrat[1][stupac] == simbol)
           && (kvadrat[2][stupac] == simbol))
       return 1;
    }
    /* i konačno dvije dijagonalne kombinacije */
    if ( (kvadrat[0][0] == simbol)
       && (kvadrat[1][1] == simbol)
       && (kvadrat[2][2] == simbol))
      return 1;

    if ( (kvadrat[0][2] == simbol)
       && (kvadrat[1][1] == simbol)
       && (kvadrat[2][0] == simbol))
      return 1;

    return 0;
}


/* Funkcija: PotezCovjeka(void) */
/* vrši dobavu poteza čovjeka   */

void PotezCovjeka(void)
{
  int pozicija;
  do {
      printf("Otipkaj poziciju znaka %c (1..9): ", covjek);
      scanf("%d", &pozicija);
  } while (!IspravnaPozicija(pozicija));

    kvadrat[(pozicija - 1) / 3][ (pozicija - 1) % 3] = covjek;
}

/* Funkcija: IspravnaPozicija(int pozicija)
 * vraća 1 ako pozicija prazna, inače vraća 0
 */
int IspravnaPozicija(int pozicija)
{
  int redak, stupac;
  redak = (pozicija - 1) / 3;
  stupac = (pozicija - 1) % 3;

    if ((pozicija >= 1) && (pozicija <= 9))




                                                                      125
if (kvadrat[redak][stupac] == ' ')
             return 1;
    return 0;
}

/* Funkcija: PotezRacunala()
 * inteligentno određuje potez racunala
 * (algoritam je napisan u obliku komentara)
*/

void PotezRacunala(void)
{
  int pozicija;                         /* pronađi kvadrat u kojem    */
  pozicija = DobitnaPozicija(racunalo); /* može pobijediti racunalo   */
  if (!pozicija)                        /* ako ga nema, pronađi       */
    pozicija = DobitnaPozicija(covjek); /* gdje čovjek pobijeđuje     */
  if (!pozicija)                        /* ako ga nema */
    pozicija = PraznaSredina();         /* centar je najbolji potez   */
  if (!pozicija)                    /* ako ga nema */
    pozicija = PrazanUgao();        /* najbolji je potez u kutovima   */
  if (!pozicija)                    /* ako ga nema */
    pozicija = PrazanaStrana();     /* ostaje mjesto na stranicama    */

    printf("nJa sam izabrao kvadratic: %d!n", pozicija);
    kvadrat[(pozicija - 1) / 3][ (pozicija - 1) % 3] = racunalo;
}

/*
* Funkcija: int DobitnaPozicija(char simbol),
* ako postoji dobitna kombinacija za simbol,
* vraća poziciju kvadratića, inaće vraća 0;
*/
int DobitnaPozicija(char simbol)
{
  int pozicija, redak, stupac;
  int rezultat = 0;

/* Analiziraj stanje u svih 9 kvadratića. Za svaki kvadratić:
 * ako je prazan, ispuni ga danim simbolom, i provjeri da li je
 * je to dobitni potez. Ako jest, zapamti ga u varijabli rezultat
 * i ponovo poništi taj kvadratić.
 * Nakon završetka petlje rezultat sadrži dobitni potez ili nulu ako
 * nije pronađen dobitni potez.
 * Funkcija vraća vrijednost varijable rezultat
 */
 for (pozicija = 1; pozicija <= 9; pozicija++)
  {
      redak = (pozicija - 1) / 3;
      stupac = (pozicija - 1) % 3;

     if (kvadrat[redak][stupac] == ' ')
     {
        kvadrat[redak][stupac] = simbol;
        if (is_wining(simbol))
          rezultat = pozicija;
        kvadrat[redak][stupac] = ' ';
     }
    }
    return rezultat;




                                                                       126
}

/* Funkcija: int PraznaSredina()
 * vraća 5 ako je srednji kvadratić prazan, inaće vraća 0
 */
int PraznaSredina(void)
{
  if (kvadrat[1][1] == ' ')    return 5;
  else    return 0;
}

/* Funkcija: int PrazanUgao()
 * vraća poziciju jednog od praznih kuteva,
 * ako su svi kutevi zauzeti, vraća 0
 */
int PrazanUgao(void)
{
  if (kvadrat[0][0] == ' ')    return 1;
  if (kvadrat[0][2] == ' ')    return 3;
  if (kvadrat[2][0] == ' ')    return 7;
  if (kvadrat[2][2] == ' ')    return 9;
  return 0;
}

/* Funkcija: int PrazanaStrana()
 * vraća poziciju jedne od praznih stranica kvadrata,
 * ako su sve pozicije zauzete vraća 0
 */
int PrazanaStrana(void)
{
  if (kvadrat[0][1] == ' ')    return 2;
  if (kvadrat[1][0] == ' ')    return 4;
  if (kvadrat[1][2] == ' ')    return 6;
  if (kvadrat[2][1] == ' ')    return 8;
  return 0;
}


Slijed dekompozicije funkcija tipa "od vrha prema dolje" ilustriran je na slici 9.1.




                                                                                       127
TIc-tac-Toe
                                          main()




      Upute              OcistiPlocu           PostaviPrvogIgraca          IgrajIgru             Ponovi Igru




                                                     NacrtajPlocu         DobaviPotez          PostajeDobitnik




                                                        PotezKompjutera                 PotezCovjeka




  DobitnaPozicija    PraznaSredina         PrazanUgao          PraznaStrana                      IspravnaPozicija




                    Slika 9.1 Dekompozicije funkcija u programu Tic-Tac-Toe




9.3 Zaključak
Programi se u C jeziku mogu pisati u više odvojenih datoteka - modula.

Ponovno prevođenje cijelog programa uzima dosta vremena, dok se pojedina datoteka, koja je
manja od ukupnog programa, prevodi mnogo brže.

U modulu se može definirati određeni skup funkcija koji se može koristiti i u drugim
programima.

Module, koji sadrže često korištene funkcije, može se u obliku strojnog koda uvrstiti u
biblioteke potprograma.

Modul se može pisati, testirati, i ispravljati neovisno od ostatka programa. Proces ispravljanja
je pojednostavljen, jer se analizira manji dio programa.

Moduli omogućavaju veću preglednost i logičku smislenost programskog koda, jer se u njima
obično obrađuje jedinstvena problematika. Primjerice, za obradu matematičkih problema
postoje različiti programski paketi s odvojenim modulima za rad s kompleksnim brojevima,
vektorima, matricama, itd.

Korištenjem principa odvajanja specifikacije od implementacije modula, i skrivanjem podataka
koji bilježe stanja objekta koji modul opisuje, dobivaju se moduli neovisni od programa u kojem
se koriste. To znatno olakšava timski rad u razvoju softvera.




                                                                                                         128
10 Rad s pokazivačima


Naglasci:
   • tip pokazivača
   • operacije s pokazivačima
   • ekvivalentnost reference niza i pokazivača
   • prijenos varijable u funkciju
   • void pokazivači
   • pokazivači na funkcije
   • polimorfne funkcije

    Pokazivači su varijable koje sadrži adresu nekog memorijskog objekta: varijable ili
funkcije. Njihova primjena omogućuje napredne programske tehnike: dinamičko alociranje
memorije, apstraktni tip podataka i polimorfizam funkcija. Da bi se moglo shvatiti ove tehnike
programiranja, u ovom će poglavlju biti pokazano kako se vrše temeljne operacije s
pokazivačima, a u sljedećim će poglavljima biti pokazana njihova primjena u programiranju.


10.1 Tip pokazivača
Pokazivačkim varijablama se deklaracijom pridjeljuje tip. Deklariranje pokazivačke varijable se
vrši na način da se u deklaraciji ispred imena pokazivačke varijable upisuje zvjezdica *.
Primjerice,

      int *p,*q;                 /* p i q su pokazivači na int */

označava da su deklarirane pokazivačke varijable p i q, kojima je namjena da sadrže adresu
objekata tipa int. Kaže se da su p i q "pokazivač na int".
     Pokazivači, prije upotrebe, moraju biti inicijalizirani na neku realnu adresu. To se ostvaruje
tzv. adresnim operatorom &:

      p = &sum;             /* p iniciran na adresu varijable sum */
      q = &arr[2];          /* q iniciran na adresu trećeg elementa niza arr*/


                        Adresa         Vrijednost       Identifikator

                        0x09AC         -456             sum
                        0x09B0         ......           ......
                        ......         ......           ......
                        0x0F10         ......           arr[0]
                        0x0F14                          arr[1]
                        0x0F18                          arr[2]
                        ......                          ......
                        0x1000         0x09AC           p
                        0x1004         0x0F18           q



                             Slika 10.1 Adresa i vrijednost varijabli




                                                                                               129
10.2 Operacije s pokazivačima
Temeljne operacije s pokazivačima su:

    1. Deklariranje pokazivača je postupak kojim se deklarira identifikator pokazivača, na
       način da se između oznake tipa na koji pokazivač pokazuje i identifikatora pokazivača
       upisuje se operator indirekcije *.

             int x, *p;         /* deklaracija varijable x i pokazivača p */

    2. Inicijalizacija pokazivača je operacija kojom se pokazivaču pridjeljuje vrijednost koja
       je jednak adresi objekta na koji on pokazuje. Za dobavu adrese objekta koristi se unarni
       adresni operator '&' .

      p = &x;        /*p sadrži adresu od x */

    3. Dereferenciranje pokazivača je operacija kojom se pomoću pokazivača pristupa
       memorijskom objektu na kojega on pokazuje, odnosno, ako se u izrazima ispred
       identifikatora pokazivača zapiše operator indirekcije *, dobiva se dereferencirani
       pokazivač (*p). Njega se može koristiti kao varijablu, odnosno referencu memorijskog
       objekta na koji on pokazuje.

      y = *p;       /*   y dobiva vrijednost varijable koju p pokazuje*/
                    /*   isti učinak kao y = x */
      *p = y;       /*   y se pridjeljuje varijabli koju p pokazuje */
                    /*   isti učinak kao x = y */

Djelovanje adresnog operatora je komplementarno djelovanju operatora indirekcije, i vrijedi da
naredba y = *(&x); ima isti učinak kao i naredba y = x;.
Unarni operatori * i & su po prioritetu iznad većine operatora (kada se koriste u izrazima).

      y = *p + 1;        ⇔   y = (*p) + 1;

Jedino postfiksni unarni operatori (-- ++, [], ()) imaju veći prioritet od * i & prefiks
operatora:

      y = *p++;          ⇔   y = *(p++);

Pokazivač kojem je vrijednost nula (NULL) naziva se nul pokazivač.

      p = NULL;      /* p pokazuja na ništa */

     S dereferenciranim pokazivačem (*p) se može manipulirati kao sa varijablom pripadnog
tipa, primjerice,

      int x, y, *px, *py;
      px = &x;              /* px sadrži adresu od x – ne utječe na x                          */
      *px = 0;            /* vrijednost x postaje 0 - ne utječe na px                          */
      py = px;    /* py također pokazuje na x - ne utječe na px ili x                          */
      *py += 1;            /* uvećava x za 1 - ne utječe na px ili py                          */
      y = (*px)++;          /* y = 1, a x =2 - ne utječe na px ili py                          */




                                                                                               130
10.3 Pokazivači kao argumenti funkcije
     Pokazivači se često koriste kao argumenti funkcije, jer se na taj način može prenositi
varijable u funkciju. To se postiže na način da se kao parametar funkcije deklarira pokazivač na
neki objekt, primjerice:

       void Increment(int *pVar);

     Prema pravilu C jezika, pri pozivu funkcije se prenosi vrijednost stvarnog argumenta, a u
funkciji se parametar funkcije tretira kao lokalna varijabla koja ima vrijednost stvarnog
argumenta. U slučaju kada je parametar funkcije pokazivač, stvarni argument funkcije je adresa
objekta koji ima isti tip kao pokazivač. Korištenjem indirekcije pokazivača može se pristupiti
tom objektu i mijenjati njegov sadržaj, dakle taj se objekt može tretirati kao varijabla koju se
koristi i u funkciji.
     Ovaj način prenošenja parametara funkcije je prikazan u programu ptr-parm.c. U njemu
se pomoću funkcije void Increment(int *pVar){(*pVAr)++;} može inkrementirati
vrijednost bilo koje cjelobrojne varijable imena var. To se vrši s pozivom funkcije u obliku
Increment(&var);. Uočite da se u definiciji funkcije koristi pokazivački parametar, a pri
pozivu funkcije se kao argument koristi adresa varijable na koju ova funkcija djeluje.

       /* Datoteka: ptr-parm.c */
       #include <stdio.h>

       void Increment(int *pVar)
       {
        /* Funkcija inkrementira vrijednost varijable, čija se adresa
         * prenosi u funkciju kao vrijednost pokazivača pVar
         * Varijabli se pristupa pomoću indirekcije pokazivača pVar
         */
            (*pVar)++;
       }

       int main()
       {
           int var = 7;
           Increment(&var);        /* argument je adresa varijable */
           printf ("var = %dn", var);
           return 0;
       }


Ispis je:

       var = 8

     U nekim knjigama se ovaj način prijenosa argumenata naziva "prijenos reference" (call by
reference), međutim taj naziv nije ispravan jer se u ovom slučaju ne prenosi referenca varijable
( ime koje označava adresu) već se prenosi vrijednost pokazivača (adresa), a varijabli na koju on
pokazuje pristupa se indirekcijom pokazivača.


  Prijenos varijabli u funkcije pomoću pokazivača koristi se uvijek kada želimo da se
  pomoću jedne funkcije istovremeno mijenja vrijednost više varijabli.




                                                                                             131
Primjer: Definirana je funkciju swap(), pomoću koje se može izvršiti zamjena vrijednost
dvije varijable:


       /* Datoteka: swap.c
        * Zamjena vrijednosti dvije varijable pomoću swap funkcije */
        */
       #include <stdio.h>

       void swap( int *x, int *y)
       {
          int t;
          t = *x;
          *x = *y;
          *y = t;
       }

       int main()
       {
          int a = 1, b = 2;

            printf("a=%d b=%dn", a, b);

            swap(&a , &b);

            printf("Nakon poziva swap(&a, &b)n");
            printf("a=%d b=%dn", a, b);
       }


Ispis je:

       a=1 b=2

Nakon poziva swap(&a, &b)

       a=2 b=1



10.4 Pokazivači i nizovi
     Potrebno je najprije navesti nekoliko pravila koje vrijede za reference niza i pokazivače
koji pokazuju na nizove. Analizirat ćemo niz a i pokazivač p:

       int a[10];
       int *p;

Pravila su:
1. Ime niza, iza kojeg slijedi oznaka indeksa, predstavlja element niza s kojim se manipulira na
isti način kao i sa prostim varijablama.

2. Ime niza, zapisano bez oznake indeksa je "pokazivačka konstanta - adresa" koja pokazuje na
prvi element niza. Ona se može pridijeliti pokazivačkoj varijabli. Naredbom

       p = a;

za vrijednost pokazivača p postavlja se adresa a[0]. Ovo je ekvivalentno naredbi:




                                                                                            132
p = &a[0];


3. Pravilo pokazivačke aritmetike:
      Ako p pokazuje na a[0], tada (p + i) pokazuje na a[i].

      Ako pi pokazuje na a[i], tada pi + k pokazuje na a[i+k], odnosno, ako je

      pi = &a[i];

      tada vrijedi odnos;

      *(pi+k) ⇔ *(p+i+k) ⇔ a[i+ k]

    Gornja pravila određuju da se aritmetičke operacije s pokazivačima ne izvode na isti način
kao što se izvode aritmetičke operacije s cijelim brojevima.

    Za prosti cijeli broj x vrijedi:
          vrijednost(x ± n) = vrijednost(x) ± n
    dok za pokazivač p vrijedi
          vrijednost(p ± n) = vrijednost(p) ± n*sizeof(*p)

4. Ekvivalentnost indeksne i pokazivačke notacije niza.
    Ako je deklariran niz array[N], tada izraz *array označava prvi element, *(array +
1) predstavlja drugi element, itd. Poopći li se ovo pravilo na cijeli niz, vrijede sljedeći odnosi:

                *(array) ⇔ array[0]
                *(array + 1) ⇔ array[1]
                *(array + 2) ⇔ array[2]
                ...
                *(array + n) ⇔ array[n]

što odgovara činjenici da ime niza predstavlja pokazivačku konstantu.

S pokazivačima se također može koristiti indeksna notacija. Tako, ako je inicijaliziran
pokazivač p:

      p = array;

tada vrijedi:

      p[0]      ⇔ *p       ⇔ array[0]
      p[1]      ⇔ *(p + 1) ⇔ array[1]
      p[2]      ⇔ *(p + 2) ⇔ array[2]
      ...
      p[n]      ⇔ *(p + n) ⇔ array[n]


      Jedina razlika u korištenju reference niza i pokazivača na taj niz je u tome da se
      memorijskim referencama ne može mijenjati vrijednost adrese koju oni
      označavaju.

      a++; je nedozvoljen izraz, jer se ne može mijenjati konstanta




                                                                                               133
p++; je dozvoljen izraz, jer je pokazivač p varijabla

 Pošto se pokazivaču može mijenjati vrijednost, obično se kaže da se pomoću pokazivača može
 "šetati po nizu".

 Primjer: U obje sljedeće petlje ispisuje se vrijednost 10 elemenata niza a.

      int *p = a;
      for (i = 0; i < 10; i++, p++)                  for (i = 0; i < 10; i++)
           printf("%dn", *p);                            printf("%dn", a[i]);


 U prvoj petlji se koristi pokazivač p za pristup elementima niza a[]. Početno on pokazuje na
 prvi element niza a[]. U petlji se zatim ispisuje vrijednost tog elementa (dereferencirani
 pokazivač *p), i inkrementira vrijednost pokazivača, tako da se u narednom prolazu petlje s
 njime referira sljedeći element.
      Kompilator ne prevodi ove petlje na isti način, iako je učinak u oba slučaja isti: bit će
 ispisana vrijednost 10 elemenata niza a[]. Ako je učinak isti, možemo se dalje upitati koja će se
 od ovih petlji brže izvršavati. To ovisi o kvaliteti kompilatora i o vrsti procesora. Kod starijih
 procesora brže se izvršava verzija s pokazivačem, jer se u njoj u jednom prolazu petlje vrše dva
 zbrajanja, dok se u drugom slučaju mora (skriveno) izvršiti i jedno množenje. Ono je potrebno
 da bi se odredila adresa elementa a[i], jer je
       adresa(a[i])= adresa(a[0]) + i*sizeof(int).
     Kod novijih se procesora operacija indeksiranja izvršava veoma brzo, pa se u tom slučaju
 preporučuje korištenje verzije s indeksnim operatorom.


 10.5 Pokazivači i argumenti funkcije tipa niza
      Pri pozivu funkcije, stvarni se argument kopira u formalni argument (parametar) funkcije.
 U slučaju da je argument funkcije ime niza kopira se adresa prvog elementa, dakle stvarni
 argument koji se prenosi u funkciju je vrijednost pokazivača na prvi element niza. Stoga se
 prijenos niza u funkciju može deklarirati i pomoću pokazivača.
 Primjer: Sljedeće tri funkcije imaju isti učinak i mogu se pozvati s istim argumentima:

void print(int x[],               void print(int *x,               void print(int *x,
           int N )                           int N)                           int N)
{                                 {                                {
int i;                              while (N--) {                  int i;
for (i = 0; i < N; i++)                                            for (i=0; i<N;i++)
   printf("%dn", x[i]);                  printf("%dn", *x);         printf("%dn", x[i]);
}                                         x++;                     }
                                      }
                                  }


                              /* poziv funkcije print */
                             int niz[10], size=10;
                                . . . . .
                             print(niz, size);




                                                                                               134
const osigurači
    U prethodnom primjeru u funkciji print() se koriste vrijednosti elemenata niza x, a ne
mijenja se njihova vrijednost. U takovim slučajevima je preporučljivo da se parametri funkcije
deklariraju s prefiksom const.

              void print(const int x[], int N );

      ili     void print(const int *x, int N );

     Ovakva deklaracija je poruka kompilatoru da dojavi grešku ako u funkciji postoji naredba
kojom se mijenja sadržaja elemenata niza, a programeru služi kao dodatno osiguranje da će
izvršiti implementaciju funkcije koja neće mijenjati elemente niza.

Pomoću pokazivača se u funkcije mogu prenositi i proste varijable i nizovi.
Primjer: Napisat ćemo funkciju getMinMax() kojom se određuje maksimalna i minimalna
vrijednost niza. Testirat ćemo je programom u kojem korisnik unosi 5 brojeva, a program
ispisuje maksimalnu i minimalnu vrijednost.

      /* Datoteka: minmax.c */
      #include <stdio.h>

      #define N     5                  /* radit ćemo s nizom od N elemenata */

      void getMinMax(double *niz, int nelem, double *pMin, double *pMax)
      {
       /* funkcija određuje minimalni i maksimalni element niza
        * Parametri funkcije su:
        * niz – niz realnih brojeva
        * nelem – broj elemenata u nizu
        * pMin – pokazivac na minimalnu vrijednost
        * pMax - pokazivac na maksimalnu vrijednost
       /*
         int i= 0;
         *pMin = *pMax = niz[0];
         for(i=1; i<N; i++) {
            if(niz[i] > *pMax ) *pMax = niz[i];
            if(niz[i] < *pMin) *pMin = niz[i];
         }
      }

      int main()
      {
      int i;
      double min, max, data [N];

       /* 1. izvjesti korisnika da otkuca 5 realnih brojeva                    */
          printf("Otkucaj %d realnih brojeva:n", N);
          for (i=0; i<N; i++)
              scanf("%lg", &data[i]);

          getMinMax(data, N, &min, &max);

          /* ispisi minimalnu i maksimalnu vroijednost */
          printf ("min = %lf max = %lfn", min, max);
          return 0;
      }




                                                                                          135
10.6 Patrametri funkcije tipa void pokazivača
Ako se neki pokazivač deklarira pomoću riječi void,

      void *p;

tada nije određeno na koji tip podatak on pokazuje. Njemu se može pridijeliti adresa bilo kojeg
memorijskog objekta, ali se ne može vršiti pristup memorijskim objektima pomoću operatora
indirekcije, jer on pokazuja na ništa. Većina današnjih kompilatora ne dozvoljava aritmetičke
operacije s void pokazivačima.
      Očito je da nema smisla koristiti void pokazivače kao regularne varijable. Oni pak mogu
biti korisni kao parametri funkcija. Kada se void pokazivač koristi kao parametar funkcije tada
se pri pozivu funkcije tom pokazivaču može pridijeliti adresa bilo kojeg memorijskog objekta, a
unutar same funkcije se može s prefiksom (tip *) vršiti forsirana pretvorba pokazivačkog
tipa.

Primjer: Definirana je funkcija void UnesiVrijednost(void *p, int tip), pomoću
koje se može izvršiti unos različitih tipova podataka

      /* Datoteka: unos.c
       * koristenje void pokazivaca kao parametra funkcije
       */
      #include <stdio.h>

      #define    CHAR     0
      #define    INT      1
      #define    FLOAT    2
      #define    DOUBLE   3

      void UnesiVrijednost(void *p, int tip)
      {
           switch (tip) {
                case CHAR:
                       printf( "Unesite jedan znak: n");
                        scanf("%c", (char *) p);
                       break;
                case INT:
                       printf( "Unesite cijeli broj:n");
                        scanf("%d", (int *) p);
                       break;
                case FLOAT:
                       printf( "Unesite realni broj:n");
                        scanf("%g", (float *) p);
                       break;
                case DOUBLE:
                       printf( "Unesite realni broj:n");
                        scanf("%lg", (double *) p);
                       break;
           }
       fflush(stdin); /* odstrani višak znakova s ulaza*/
      }

      int main()
      {
         double dval;
         int ival;




                                                                                           136
UnesiVrijednost(&ival, INT);
            printf("Vrijednost je %dn" , ival);

            UnesiVrijednost(&dval, DOUBLE);
            printf("Vrijednost je %lgn" , dval);

            return 0;
       }


    Uočite kako je korištena funkcija scanf(). Ispred imena argumenta nije korišten adresni
operator jer je vrijednost pokazivača p adresa. Ispred argumenta je eksplicitno označen tip.
Ovakvi način korištenje void pokazivača je opasan, jer ako se pri pozivu funkcije ne pozovu
kompatibilni argumenti, može doći do nepredvidivih rezultata, čak i do blokade računala.


10.7 Pokazivači na funkcije
    Funkcije su također memorijski objekti pa se može deklarirati i inicijalizirati pokazivače na
funkcije. Pravilo je da se za funkciju imena F, koja je deklarirana (ili definirana) u obliku:
       oznaka_tipa F (list_ parametara);

pokazivač na tu funkciju, imena pF, deklarira u obliku:

       oznaka_tipa ( *pF) (list_ parametara);

Ime funkcije, napisano bez zagrada predstavlja adresu funkcije, pa se pridjelom vrijednosti:

       pF = F;

inicira pokazivač pF na adresu funkcije F. Kada je pokazivač iniciran, indirekcijom pokazivača
može se izvršiti poziv funkcije u obliku:

       (*pF)(lista_argumenata);

ili još jednostavnije, sa

       pF(lista_argumenata);

jer, oble zagrade predstavljaju operator poziva funkcije (kompilator sam vrši indirekciju
pokazivača, ako se iza njega napišu oble zagrade).
Primjerice, iskazom

        double (*pMatFun)(double);

deklarira se pokazivač pMathFun kojem možemo pridijeliti adresu standardnih matematičkih
funkcija jer one imaju isti tip parametara i rezultata funkcije (pr. double(sin(double)).

Pokazivač na funkciju može biti i argument funkcije, primjerice funkcija

       void Print( double (*pMatFun)(double), double                  x)
       {
          printf( "%lf", pMatFun (x));
       }




                                                                                               137
se može koristiti za ispis vrijednosti matematičkih funkcija.

      Print(sin, 3.1);             /* ispisuje se vrijednost sinusne funkcije
                                      za vrijednost argumenta 3.1*/
      Print(cos, 1.7)              /* ispisuje se vrijednost kosinus funkcije
                                       za vrijednost argumenta 1.7*/

Primjer: U programu pfunc.c koriste se pokazivači na funkciju za ispis vrijednosti
standardnih i korisnički definiranih matematičkih funkcija. Izbor funkcije i argumenta funkcije
vrši se u interakciji s korisnikom programa.

      /* Datoteka: pfun.c
       * korištenje pokazivača na funkciju
       */

       #include <stdio.h>
       #include <math.h>

       double Kvadrat (double x)
       {
          return x*x;
       }

       void PrintVal( double (*pFunc)(), double x)
       {
          printf( "nZa x: %lf dobije se %lfn", x, pFunc(x));
       }

       int main()
       {
          double val=1;
          int choice;

            double (*pFunc)(double);

            printf("Upisi broj:");
            scanf("%lf", &val);
            fflush(stdin); /* odstrani višak znakova s ulaza */
            printf( "n(1)Kvadrat n(2)Sinus n(3)Kosinus n");
            printf( "nOdaberi 1, 2 li 3n");

            choice = getchar();
            switch (choice)
            {
                case '1': pFunc = Kvadrat; break;
                case '2': pFunc = sin; break;
                case '3': pFunc = cos; break;
                default: return 0;
            }
            PrintVal (pFunc, val);
            return 0;
      }



Često se koriste nizovi pokazivača na funkciju. Primjerice, deklaracijom

      #include math.h




                                                                                           138
double (*pF[4])(double) = {sin, cos, tan, exp};

deklariran je niz od 4 elementa koji sadrže pokazivač na funkciju kojoj je parametar tipa
double i koja vraća vrijednost tipa double. Također je izvršena i inicijalizacija elemenata niza
na adresu standardnih matematičkih funkcija.

Sada je naredba x = (*pF[1])(3.14) ekvivalentna naredbi x= cos(3.14).

Primjer: U programu niz-pfun.c koristi se niz pokazivača na funkciju. U nizu se bilježe
adrese funkcija sin(), cos() i korisnički definirane funkcije Kvadrat(). Zatim se od
korisnika traži da unese broj i da odabere funkciju. Na kraju se ispisuje rezultat primjene
funkcije na uneseni broj.

      /* Datoteka: niz-pfun.c
       * korištenje niza pokazivača na funkciju
       */

          #include <stdio.h>
          #include <math.h>

          double Kvadrat (double x) {return x*x;}

          int main()
          {
              double val=1;
              int izbor;

              double (*pF[3])(double)= {Kvadrat, sin, cos};

              printf("Upisi broj:");
               scanf("%lf", &val);
              fflush(stdin);
               printf( "n(1)Kvadrat n(2)Sinus               n(3)Kosinus n");
              printf( "nOdaberi 1, 2 li 3n");
               scanf("%d" ,&izbor);

              if (izbor >=1 && izbor <=3)
                   printf( "nRezultat je %lfn", (*pF[izbor-1])(val));
              return 0;
          }



10.8 Kompleksnost deklaracija
Očito je da se u C jeziku koriste vrlo kompleksne deklaracije. One na prvi pogled ne otkrivaju o
kakovim se tipovima radi. Sljedeća tablica pokazuje deklaracije koje se često koriste.

      U deklaraciji:         x je ime koje predstavlja ...
      T   x;                 objekt tipa T
      T   x[];               (otvoreni) niz objekata tipa T
      T   x[n];              niz od n objekata tipa T
      T   *x;                pokazivač na objekt tipa T
      T   **x;               pokazivač na pokazivač tipa T
      T   *x[];              niz pokazivača na objekt T
      T   *(x[]);            niz pokazivača na objekt T




                                                                                            139
T   (*x)[];             pokazivač na niz objekata tipa T
      T   x();                funkcija koja vraća objekt tipa T
      T   *x()                funkcija koja vraća pokazivač na objekt tipa T
      T   (*x());             funkcija koja vraća pokazivač na objekt tipa T
      T   (*x)();             pokazivač na funkciju koja vraća objekt tipa T
      T   (*x[n])();          niz od n pokazivača na funkciju koja vraća objekt tipa T

Dalje će biti pokazano:

    1. Kako sistematski pročitati ove deklaracije.
    2. Kako se uvođenjem sinonima tipova (pomoću typedef) može znatno smanjiti
       kompleksnost deklaracija.

Deklaracija nekog identifikatora se čita ovim redom:

      Identifikator je (... desna strana deklaracije) (.. lijeva strana deklaracije)

      S desne strane identifikatora mogu biti uglate ili oble zagrade.
      Ako su uglate zagrade čitamo : identifikator je niz ,
      Ako su oble zagrade čitamo identifikator je funkcija.
      Ukoliko ima više operatora s desne strane nastavlja se čitanje po istom pravilu.

      Zatim se analizira zapis s lijeve strane identifikatora ( tu može biti operator indirekcije i
      oznaka tipa).

      Ako postoji operator indirekcije, čitamo: identifikator je (... desna strana) pokazivač na
      tip. Ako postoji dvostruki operator indirekcije, čitamo: identifikator je (... desna strana)
      pokazivač na pokazivač tip

      Ukoliko je dio deklaracije napisan u zagradama onda se najprije čita značaj zapisa u
      zagradama.

Primjerice, u deklaraciji

      T (*x[n])(double);

najprije se čita dio deklaracije (*x[n]) koji znači da je x niz pokazivača. Pošto je s desne strane
ovog izraza (double) znači da je x niz pokazivača na funkciju koja prima argument tipa
double i koja vraća tip T.

Znatno jednostavniji i razumljiviji način zapisa kompleksnih deklaracija postiže se korištenjem
sinonima za kompleksne tipove. Sinonimi tipova se definiraju pomoću typedef. Primjerice,
typedef deklaracijom

      typedef double t_Fdd(double);                /* tip funkcije ... */

uvodi se oznaka tipa t_fdd koja predstavlja funkciju koja vraća double i prima argument tipa
double.
Dalje se može definirati tip t_pFdd koji je pokazivač na funkciju koja vraća double i prima
argument tipa double, s deklaracijom:

      typedef t_Fdd *t_pFdd;               /* tip pokazivača na funkciju ...*/




                                                                                               140
što je ekvivalentno deklaraciji sinonima tipa:

      typedef double (*t_pFdd)(double);

Pomoću tipa t_pFdd može se deklarirati niz pokazivača na funkciju iz prethodnog programa:

      t_pFdd     pF[3] = {Kvadrat, sin, cos};

Očito da je ovakovu deklaraciju znatno lakše razumjeti.

Primjer: Primjeni li se navedene typedef deklaracije u programu niz-pfun.c, dobije se

       /* Datoteka: niz-pfun1.c */
       #include <stdio.h>
       #include <math.h>

       typedef double t_Fdd(double); /* tip funkcije koja ... */
       typedef t_pFdd *t_pFdd;    /* tip pokazivača na funkciju ...*/

       double Kvadrat (double x) {return x*x;}

       void PrintVal(t_pFdd pFunc, double x)
       {
           printf( "nZa x: %lf dobije se %lfn", x, pFunc(x));
       }

       int main()
       {
           double val=1;
           int izbor;
           t_pFdd pF[3] = {Kvadrat, sin, cos};

             printf("Upisi broj:");
             scanf("%lf", &val);
             fflush(stdin);
             printf( "n(1)Kvadrat n(2)Sinus             n(3)Kosinus n");
             printf( "nOdaberi 1, 2 li 3n");
             scanf("%d" ,&izbor);

             if (izbor >=1 && izbor <=3)
                  printf( "nRezultat je %lfn", (*pF[izbor-1])(val));
             return 0;
       }



10.9 Polimorfne funkcije
     Funkcije koje se mogu prilagoditi različitim tipovima argumenata nazivaju se polimorfne
funkcije. Polimorfne funkcije se u C jeziku realiziraju pomoću parametara koji imaju tip void
pokazivača i pokazivača na funkcije. Dvije takove funkcije implementirane su u standardnoj
biblioteci C jezika. To su qsort() i bsearch() funkcija, čija je deklaracija dana u
<stdlib.h>.


      void qsort(void *a,                        /* pokazivač niza */
                 size_t n,                       /* broj elemenata */
                 size_t elsize,                  /* veličina elementa u bajtima */




                                                                                         141
int (*pCmpF)(void *, void *))

qsort() funkcija služi za sortiranje elemenata niza a, koji sadrži n elemenata veličine
elsize bajta. Elementi se sortiraju od manje prema većoj vrijednosti, odnosno prema kriteriju
usporedbe koji određuje funkcija (*pCmpF)().

Usporedna funkcija mora biti deklarirana u obliku

      int ime_funkcije(const void *p1, const void *p2);

Argumenti ove funkcije su pokazivači na dva elementa niza. Funkcija mora vratiti vrijednost
nula ako su ta dva elementa jednaka, pozitivnu vrijednost ako je prvi element veći od drugoga, a
negativnu vrijednost ako je prvi element manju od drugoga.

Primjerice, za sortiranje niza cijelih brojeva usporedna funkcija ima oblik:

      int CmpInt(const void *p1, const void *p2)
      {
         int i1 = *((int *)p1);
         int i2 = *((int *)p2);

          if( i1 == i2)
              return 0;
          else if( i1 > i2)
              return 1;
          else
              return -1; /* i2 > i1 */
      }

     Najprije se s adresa p1 i p2 dobavlja cjelobrojne vrijednosti i1 i i2. Dobava vrijednosti se
vrši tako da se najprije izvrši pretvorba void pokazivača u int*, a zatim se primijeni
indirekcija pokazivača. Nakon toga se vrši usporedba vrijednosti i vraća dogovorena vrijednost.
     Testiranje primjene funkcije qsort() je u programu sorti.c. U programu se vrši
sortiranje niza cijelih brojeva.

      /* Datoteka: sorti.c */
      /* koristenje polimorfne qsort funkcije */

      #include <stdio.h>
      #include <stdlib.h>

      int CmpInt(const void *p1, const void *p2)
      { ...... prethodna definicija ... }

      int main()
      {
          int i, A[] = {3,1,13,2,17};
          int numel=sizeof(A)/sizeof(A[0]);
          int elsize = sizeof(A[0]);

            for(i=0; i <numel; i++)
             printf(" %d", A[i]);
            printf("n Nakon sortiranjan");

            qsort(A,     numel,     elsize,      CmpInt);




                                                                                             142
for(i=0; i <numel; i++)
             printf(" %d", A[i]);
           printf("n");
           return 0;
      }


Nakon izvršenja dobije se ispis:

      3 1 13 2 17
      Nakon sortiranja
      1 2 3 13 17


Zadatak: Napišite program sortf.c u kojem se vrši sortiranje niza realnih brojeva. Koristite
qsort() funkciju. Definirajte prikladnu usporednu funkciju int FloatCmp(..).


Funkcija za polimorfno traženje elementa niza
    Slijedi opis polimorfne funkcije search() kojom se određuje da li u nekom nizu postoji
element zadane vrijednosti. Funkcija vraća pokazivač na traženi element, ako postoji , ili NULL
ako u tom nizu ne postoji zadana vrijednost. Koristi se deklaracija funkcije slična funkciji
qsort(), jedino se još dodaje argument tipa void pokazivača na varijablu koja sadrži vrijednost
koju se traži u nizu A.

      void * search(const void *x, /* pokazivač na zadanu vrijednost */
              const void *A,        /* pokazivač niza A*/
              size_t n,             /* broj elementa niza */
              size_t elsize,        /* veličina elementa u bajtima */
              int (*pCmpF)( const void *, const void *)) /*funkcija */
      {
           int indx;
           char *adr;
           for(indx = 0; indx < n; indx++)
           {
             adr= (char*)A +indx*elsize;   /* adresa elementa niza */
             if( (*pCmpF)((void *)adr, x) == 0)
                  break;
           }
           if(indx == n) /* tada ni jedan element nema vrijednost x*/
                return NULL;
           else
              return (void *) adr; /*vrati adresu elementa */
      }


Testiranje primjene funkcije search() vrši se programom searchi.c:


      /* Datoteka: searchi.c*/
      #include <stdio.h>
      #include <stdlib.h>

      void *search(const void *x, const void *A, size_t n,
                   size_t elsize,
                   int (*pCmpF)( const void *, const void *))
      {




                                                                                           143
....... prema prethodnoj definiciji
      }

      int CmpInt(const void *p1, const void *p2)

      {
           ....... prema prethodnoj definiciji
      }

      int main()
      {
        int i, indx, x = 2;                /* x- tražena vrijednost */
        int A[] = {3,1,13,2,17, 7, 0, 11}; /* u nizu A*/
        int numel=sizeof(A)/sizeof(A[0]);
        int elsize = sizeof(A[0]);
        int *pEl;

          for(i=0; i <numel; i++)
                printf(" %d", A[i]);

          pEl=(int *) search(&x, A,           numel,     elsize,     CmpInt);

          printf("n Element vrijednosti %d, na adresi %Fpn", *pEl, pEl);

          printf("n");
          return 0;
      }


Nakon izvršenja dobije se ispis:

      3 1 13 2 17 7 0 11
      Element vrijednosti 2, na adresi 0022FF4C

     U standardnoj biblioteci je implementirana funkcija bsearch() koja ima istu deklaraciju
kao funkcija search(). Razlika ove dvije funkcije je u tome što je bsearch() funkcija
specijalizirana i optimirana za slučaj da se traženje elementa niza provodi na prethodno
sortiranom nizu. Kasnije ćemo pokazati kako su realizirane funkcije bsearch() i qsort().


10.10 Zaključak
     Pokazivači zauzimaju središnje mjesto u oblikovanju C programa. Pokazivač je varijabla
koja sadrži adresu. Ako je to adresa varijable, kaže se da pokazivač "pokazuje" na tu varijablu.
     U radu s pokazivačima koriste se dva specifična operatora: adresni operator (&) i operator
indirekcije(*). Adresni operator napisan ispred imena varijable vraća u izraz adresu varijable, a
operator indirekcije *, postavljen ispred imena pokazivača, referira sadržaj varijable na koju
pokazivač pokazuje.
     Pokazivači i nizovi su u specijalnom odnosu. Ime niza, napisano bez uglatih zagrada
predstavlja pokazivačku konstanu koja pokazuje na prvi element niza.
     Indeksna notacija ima ekvivalentni oblik pokazivačke notacije. Uglate zagrade imaju
karakter indeksnog operatora, jer kada se koriste iza imena pokazivača, uzrokuju da se tim
pokazivačem može operirati kao s nizom.
     Nizovi se prenose u funkcije na način da se u funkciju prenosi pokazivač na prvi element
niza, odnosno adresa prvog elementa niza. Pošto funkcija zna adresu niza, u njoj se mogu




                                                                                             144
koristiti naredbe koje mijenjaju sadržaj elemenata niza bilo u indeksnoj ili u pokazivačkoj
notaciji.
     Deklaracija niza kao parametra funkcije može se izvršiti u indeksnoj ili pokazivačkoj
notaciji. Preporučuje se upotreba pokazivačke notacije jer sa tada poziv funkcije može vršiti sa
statičkim nizovima i pokazivačima koji pokazuju na neki niz.
     Funkcija ne raspolaže s podatkom o broju elemenata niza. Zadatak je programera da tu
vrijednost, ukoliko je potrebna, predvidi kao parametar funkcije.
     Korištenjem void pokazivača i pokazivača na funkcije mogu se realizirati polimorfne
funkcije.
     Ime funkcije je konstantni pokazivač na funkciju.




                                                                                            145
11 Nizovi znakova - string


Naglasci:
   • ASCIIZ stringovi
   • standardne funkcije za rad sa stringovima
   • ulazno izlazne operacije sa stringovima
   • konverzije stringa
   • nizovi stringova
   • argumenti komandne linije operativnog sustava

    Bit će pokazano kako se formiraju i obrađuju nizovi znakova. Od posebnog interesa su
nizovi znakova koji imaju karakteristike stringa.


11.1 Definicija stringa
     String je naziv za memorijski objekt koji sadrži niz znakova, a posljednji znak u nizu mora
biti nulti znak ('0'). Deklarira se kao niz znakova (pr. char str[10]) , ili kao pokazivač
na znak (char *str), ali pod uvjetom da se pri inicijalizaciji niza i kasnije u radu s nizom
uvijek vodi računa o tome da posljednji element niza mora biti jednak nuli. Zbog ove se
karakteristike stringovi u C jeziku nazivaju ASCIIZ stringovi. Sastoje od niza ASCII znakova i
nule (eng. Z - zero).
     Duljina stringa je cjelobrojna vrijednost koja je jednaka broju znakova u stringu (bez nultog
znaka). Indeks "nultog" znaka jednak je broju znakova u stringu, odnosno duljini stringa.
Primjerice, string koji sadrži tekst: Hello, World!, u memoriji zauzima 14 bajta. Njegova duljina
je 13, jer je indeks nultog znaka jednak 13.

    0     1     2     3     4    5     6     7     8     9      10   11    12   13
    H     e     l     l     o    ,           W     o     r      l    d     !    0




Pogledajmo primjer u kojem se string tretira kao niz znakova.

        /* Prvi C program – drugi put. */
        #include <stdio.h>
        #include <string.h>

        int main()
        {
            char hello[14] = { 'H', 'e', 'l', 'l', 'o', ',', ' ',
                               'W', 'o', 'r', 'l', 'd', '!', '0' };
            printf("%sn", hello);
            printf("Duljina stringa je %d.n", strlen(hello));
            return 0;
        }




                                                                                              146
Nakon izvršenja programa, dobije se poruka:

      Hello, World!
      Duljina stringa je 13.

    Ovaj program vrši istu funkciju kao i prvi C-program iz ove knjige, tiska poruku: Hello,
World!. U programu je prvo definirana i inicijalizirana varijabla hello. Ona je tipa znakovnog
niza od 14 elemenata. Inicijalizacijom se u prvih 13 elemenata upisuju znakovi (Hello World!),
a posljednji element se inicira na nultu vrijednost. Ispis ove varijable se vrši pomoću printf()
funkcije sa specifikatorom formata ispisa %s.

Duljina stringa je određena korištenjem standardne funkcije

      size_t strlen(char *s);

Parametar funkcije je pokazivač na char, odnosno, pri pozivu funkcije argument je adresa
početnog elementa stringa. Funkcija vraća vrijednost duljine stringa (size_t je sinonim za
unsigned).

Funkcija strlen() se može implementirati na sljedeći način:

      unsigned strlen(const char             *s)
      {
         unsigned i=0;
         while (s[i] != '0') /*             prekini petlju za s[i]==0, inače */
            i++;              /*             inkrementiraj brojač znakova     */
         return i;            /*             i sadrži duljinu stringa         */
      }

ili pomoću pokazivačke aritmetike:

      unsigned strlen(const char             *s)
      {
         unsigned i = 0;
         while (*s++ != '0') /*             prekini petlju za *s==0, inače */
           i++;               /*             inkrementiraj brojač i pokazivač*/
         return i;            /*             i sadrži duljinu stringa        */
      }

String se može inicijalizirati i pomoću literalne konstante:

      char hello[] = "Hello, World!"; /* kompilator rezervira mjesto u
      memoriji */

Elementi stringa se mogu mijenjati naredbom pridjele vrijednosti, primjerice naredbe:

      hello[0] = 'h';
      hello[6] = 'w';

mijenjaju sadržaj stringa u "hello world";
     Ako se procjenjuje da će trebati više mjesta za string, nego se to navodi inicijalnim
literalnim stringom, tada treba eksplicitno navesti dimenziju stringa. Primjerice, deklaracijom

      char hello[50] = "Hello, World!";




                                                                                            147
kompilator rezervira 50 mjesta u memoriji te inicira prvih 14 elemenata na "Hello, World!",
zaključno s nultim znakom.
    String je i svaki pokazivač koji se inicijalizira na adresu memorijskog objekta koji ima
karakteristike stringa. Stoga, i sljedeći iskaz predstavlja deklaraciju stringa:

      char *digits ="0123456789ABCDEF";
      /* kompilator inicijalizira pokazivač */
      /* na adresu od "0123456789ABCDEF"; */

    Ovakva inicijalizacija je moguća jer kompilator interno literalni string tretira kao referencu,
pa se njegova adresa pridjeljuje pokazivaču digits. Dozvoljeno je čak literalnu string
konstantu koristiti kao referencu niza, primjerice

      printf("%c", "0123456789ABCDEF"[n]);

ispisuje n-ti znak stringa "0123456789ABCDEF".


11.2 Standardne funkcije za rad sa stringovima
    U standardnoj biblioteci postoji niz funkcija za manipuliranje sa stringovima. One su
deklarirane u datoteci "string.h". Funkcija im je:

      size_t strlen(const char *s)
Vraća duljinu stringa s.

      char *strcpy(char *s, const char *t)
Kopira string t u string s, uključujući '0'; vraća s.

      char *strncpy(char *s, const char *t, size_t n)
Kopira najviše n znakova stringa t u s; vraća s. Dopunja string s sa '0' znakovima ako t
ima manje od n znakova.

      char *strcat(char *s, const char *t)
Dodaje string t na kraj stringa s; vraća s.

      char *strncat(char *s, const char *t, size_t n)
Dodaje najviše n znakova stringa t na string s, i znak '0'; vraća s.

      int strcmp(const char *s, const char *t)
Uspoređuje string s sa stringom t, vraća <0 ako je s<t, 0 ako je s==t, ili >0 ako je s>t.
Usporedba je leksikografska, prema ASCII "abecedi".

      int strncmp(const char *s, const char *t, size_t n)
Uspoređuje najviše n znakova stringa s sa stringom t; vraća <0 ako je s<t, 0 ako je s==t, ili
>0 ako je s>t.

      char *strchr(const char *s, int c)
Vraća pokazivač na prvu pojavnost znaka c u stringu s, ili NULL znak ako c nije sadržan u
stringu s.




                                                                                               148
char *strrchr(const char *s, int c)
Vraća pokazivač na zadnju pojavnost znaka c u stringu s, ili NULL znak ako c nije sadržan u
stringu s.

      char *strstr(const char *s, const char *t)
Vraća pokazivač na prvu pojavu stringa t u stringu s, ili NULL ako string s ne sadrži string t.

      size_t strspn(const char *s, const char *t)
Vraća duljinu prefiksa stringa s koji sadrži znakove koji čine string t.

      size_t strcspn(const char *s, const char *t)
Vraća duljinu prefiksa stringa s koji sadrži znakove koji nisu prisutni u stringu t.

       char *strpbrk(const char *s, const char *t)
Vraća pokazivač na prvu pojavu bilo kojeg znaka iz string t u stringu s, ili NULL ako nije
prisutan ni jedan znak iz string t u stringu s.

      char *strerror(int n)
Vraća pokazivač na string kojeg interno generira kompilator za dojavu greške u nekim
sistemskim operacijama. Argument je obično globalna varijabla errno, čiju vrijednost također
postavlja kompilator pri sistemskim operacijama.

      char *strtok(char *s, const char *sep)
 vrši razlaganje stringa s na niz leksema koji su razdvojeni znakovima-separatorima. Skup
znakova-separatora se zadaje u stringu sep. Funkcija vraća pokazivač na leksem ili NULL ako
nema leksema.

Korištenje funkcije strtok() je specifično jer u stringu može biti više leksema, a ona vraća
pokazivač na jedan leksem. Da bi se dobili sljedeći leksemi treba ponovo zvati istu funkciju, ali
s prvim argumentom jednakim NULL. Primjerice, za string

      char *     s = "Prvi drugi,treci";

ako odaberemo znakove separatore: razmak, tab i zarez, tada sljedeći iskazi daju ispis tri
leksema (Prvi drugi i treci):

      char *leksem = strtoken(s, " ,t"); /* dobavi prvi leksem     */
      while( leksem != NULL) {            /* ukoliko postoji        */
         printf("", leksem);              /* ispiši ga i            */
         lexem = strtok(NULL, " ,t");    /* dobavi sljedeći leksem */
      }                                  /* pa ponovi postupak     */



     Sljedeća dva primjera pokazuju kako se mogu napisati standardne funkcije za kopiranje
stringa (strcpy) i leksičku usporedbu dva stringa (strcmp).

Funkcija strcpy() kopira znakove stringa src u string dest, a vraća adresu od dest.

1. Verzija s indeksnim operatorom:




                                                                                             149
char *strcpy( char *dst, const char *src)
      {
      int i;
      for (i = 0; src[i] != '0'; i++)
      dst[i] = src[i];
      dst[i] = '0';
      return dst;
      }


2. Verzija s pokazivačkom aritmetikom

      char *strcpy(char *dest, const char *src)
      {
             char *d = dest;
             while(*d = *src) /* true dok src ne bude '0'*/
             {
                d++;
                src++;
             }
             return dest;
      }

Funkcija strcmp()služi usporedbi dva stringa s1 i s2. Deklarirana je s:

      int strcmp(const char *s1, const char *s2)

Funkcija vraća vrijednost 0 ako je sadržaj oba stringa isti, negativnu vrijednost ako je s1
leksički manji od s2, ili pozitivnu vrijednost ako je s1 leksički veći od s2. Leksička usporedba
se izvršava znak po znak, prema kodnoj vrijednosti ASCII standarda.

      /*1. verzija*/
      int strcmp( char *s1, char *s2)
      {
      int i = 0;
         while(s1[i] == s2[i] && s1[i] != '0')
            i++;
         if (s1[i] < s2[i])
            return -1;
         else if (s1[i] > s2[i])
            return +1;
         else
           return 0;
      }

Najprije se u glavi petlje uspoređuje da li su znakovi s istim indeksom isti i da li je dostignut
kraj niza (znak '0'). Kad petlja završi, ako su oba znaka jednaka, to znači da su i svi prethodni
znakovi jednaki. U tom slučaju funkcija vraća vrijednost 0. U suprotnom funkcija vraća 1 ili –1
ovisno o numeričkom kodu znakova koji se razlikuju.

       /*2. verzija s inkrementiranjem pokazivača*/
      int strcmp(char *s1, char *s2)
      {
           while(*s1 == *s2)
           {
               if(*s1 == '0')
                   return 0;
               s1++;




                                                                                              150
s2++;
             }
             return *s1 - *s2;
      }


      Verzije u kojima se koristi inkrementiranje pokazivačka često su kod starije
      generacije procesora rezultirale bržim izvršenjem programa. Kod novije generacije
      procesora to nije slučaj, pa se preporučuje korištenje indeksne notacije.


11.3 Ulazno-izlazne operacije sa stringovima
    Najjednostavniji način dobave i ispisa stringa je da se koriste printf() i scanf()
funkcije s oznakom formata %s. Primjerice, s

      char line[100];
      scanf("%s",str);
      printf("%s", str);

najprije se vrši unos stringa (korisnik otkuca niz znakova i <enter>). Zatim se vrši ispis tog
stringa pomoću printf() funkcije.

Funkcija scanf() nije pogodna za unos stringova u kojima ima bijelih znakova, jer i oni znače
kraj unosa, stoga se za unos i ispis stringa češće koriste funkcije:

      char *gets(char *str);
      int puts(char *str);

Funkcija gets() dobavlja liniju teksta sa standardnog ulaza (unos se prekida kada je na ulazu
'n') i sprema je u string str, kojeg zaključuje s nul znakom. Podrazumijeva se da je prethodno
rezervirano dovoljno memorije za smještaj stringa str. Funkcija vraća pokazivač na taj string
ili NULL ako nastupi greška ili EOF.

Funkcija puts() ispisuje string str na standardni izlaz. Vraća pozitivnu vrijednost ili -1 (EOF)
ako nastupi greška. Jedina razlika između funkcije printf("%s", str) i puts(str) je u
tome da funkcija puts(str) uvijek na kraju ispisa dodaje i znak nove linije.

Primjer: U programu str-io.c pokazana je upotreba raznih funkcija za rad sa stringovima.
Program dobavlja liniju po liniju teksta sve dok se ne otkuca: "kraj". Nakon toga se ispisuje
ukupno uneseni tekst.

/* str-io.c */
#include <stdio.h>
#include <ctype.h>
#include <string.h>

#define MAXCH 2000

int main( void)
{
char str[100];   /* string u kojeg se unosi jedna linija teksta */
char text[MAXCH]; /* string u kojeg se zapisuje ukupno uneseni tekst*/

/* iniciraj string text sa sljedecim tekstom */




                                                                                            151
strcpy(text, "Unijeli ste tekst;n");

    puts("Otkucaj nekoliko linija teksta."); /* informiraj korisnika */
    puts("Za kraj unosa otkuca: kraj");      /* kako se vrši unos    */

    while (gets(str) != NULL )               /* dobavljaj string                            */
    {
       if(strcmp(str, "kraj") == 0)   /* prekini ako je otkucan kraj                        */
          break;
       /* prekini ako duljina texta premaši veličinu MAXCH */
       if(strlen(str)+strlen(text) >= MAXCH)
          break;
       strcat(text, str);              /* dopuni ukupan tekst sa str                        */
       strcat(text, "n");                      /* i oznaci novi red                        */
    }
    puts(text);                       /* ispisi tekst koji je unesen                        */
    return 0;
}

Početno je string text inicijaliziran na način da je u njega kopiran string "Unijeli ste tekst;n"
pomoću funkcije strcpy(). Kasnije se taj string dopunjuje sa sadržajem stringa str koje
unosi korisnik programa u petlji:

      while (gets(str) != NULL )

U tijelu petlje prvo se ispituje sadržaj unesenog stringa. Ako je unesen string "kraj", petlja se
prekida. To se ispituje iskazom:

      if(strcmp(str, "kraj") == 0)              /* prekini ako je otkucan kraj*/
              break;

Dopuna stringa text vrši se iskazima:

      strcat(text, str);           /* dopuni ukupan tekst sa str*/
      strcat(text, "n");          /* i oznaci novi red */

Za string tekst je rezervirano MAXCH (2000) bajta, što je vjerojatno dovoljno za prihvat teksta.
Ako se pokuša unijeti više znakova, unos se prekida iskazom:

      if(strlen(str)+strlen(text) >= MAXCH)
         break;

Na kraju se ispisuje string text, koji sadrži ukupno otkucani tekst.

Zadatak: Modificirajte prethodni program na način da se program prekine i u slučaju ako se
bilo gdje unutar unesenog stringa nalazi riječ "kraj". U tu svrhu koristite funkciju strstr().

Zadatak: Modificirajte prethodni program na način da se na kraju programa ispiše koliko
ukupno ima riječi u stringu tekst s više od tri slova. U tu svrhu koristite funkciju strtok() i
strlen().



11.4 Korisnički definirane ulazne operacije sa stringovima
    Veliki nedostatak funkcija scanf() i gets() je u tome da se ne može ograničiti broj
unesenih znakova, pa treba koristiti stringove za koje je rezerviran veliki broj bajta. Zbog ovog
ograničenja mnogi programeri radije sami definiraju funkciju za unos stringa u obliku:




                                                                                              152
int getstring(char *str, int maxchar);

Parametri ove funkcije su string str i cijeli broj maxchar, kojim se zadaje maksimalno
dozvoljeni broj znakova koji će biti spremljen u string. Funkcija vraća broj znakova ili 0 ako je
samo pritisnut znak nove linije ili EOF ako je na početku linije otipkano Ctrl-Z.

Funkcija getstring() se može implementirati na sljedeći način:

      #include <stdio.h>

      int getstring(char *str, int maxchar)
      {
          int ch, nch = 0;       /* početno je broj znakova = 0 */
          --maxchar;         /* osiguraj mjesto za '0' */

           while((ch = getchar()) != EOF) /* dobavljaj znak */
           {
              if(ch == 'n')   /* prekini unos na kraju linije */
                 break;

                /* prihvati unos samo ako je broj znakova < maxchar */

                if(nch < maxchar)
                   str[nchar++] = ch;
           }

           if(ch == EOF && nch == 0)
               return EOF;

           str[nch] = '0'; /* zaključi string s nul znakom */
           return nch;
      }


Testiranje ove funkcije provodi se programom getstr.c. U njemu korisnik unosi više linija
teksta, a unos završava kada se otkuca Ctrl-Z.


      #include <stdio.h>

      int getstring(char *, int);

      main()
      {
          char str[256];

           while(getstring(str, 256) != EOF)
              printf("Otipkali ste "%s"n", str);

           return 0;
      }



11.5 Pretvorba stringa u numeričku vrijednost
    Funkciju getstring()se može iskoristiti i za unos numeričkih vrijednosti, jer u
standardnoj biblioteci postoje funkcije




                                                                                             153
int atoi(char *str);
      double atof(char *str);

koje vrše pretvorbu znakovnog zapisa u numeričku vrijednost. Ove funkcije su deklarirane u
<stdlib.h>. Funkcija atoi() pretvara string u vrijednost tipa int a funkcija atof()
pretvara string u vrijednost tipa double. Podrazumijeva se da string sadrži niz znakova koji se
koriste za zapis numeričkih literala. U slučaju greške ove funkcije vraćaju nulu. Greška se
uvijek javlja ako prvi znak nije znamenka ili znakovi + i -.

Primjer: U programu str-cnv.c prikazano je kako se može vršiti unos numeričkih vrijednosti
pomoću funkcija getstring(), atoi() i atof():

      /* str-cnv.c */
      #include <stdio.h>
      #include <stdlib.h>

      int getstring(char *, int);
      #define NCHARS 30

      int main()
      {
         int i;
         double d;
         char str [NCHARS];

          puts("Otipkajte cijeli broj");
          getstring(str, NCHARS);
          i = atoi(str);
          printf("Otipkali ste %dn", i);

          puts("Otipkajte realni broj");
          getstring(str, NCHARS);
          d = atof(str);
          printf("Otipkali ste %lgn", d);

          return 0;
      }


Drugi način da se iz stringa dobije numerička vrijednost je da se koristi funkcija sscanf().

      int sscanf (char *str, char *format, ... );

Ova funkcija ima isto djelovanje kao funkcija scanf(). Razlika je u tome da sscanf() prima
znakove iz stringa str, dok funkcija scanf() prima znakove sa standardnog ulaza.

Primjer: pretvorba stringa, koji sadrži znakovni zapis cijelog broja, u numeričku vrijednost
cjelobrojne varijable vrši se iskazom

      sscanf( str, "%d", &i);

Ponekad će biti potrebno izvršiti pretvorbu numeričke vrijednosti u string. U tom slučaju se
može koristiti standardna funkcija:

      int sprintf(char *str, char *format, ... );




                                                                                               154
koja ima isto djelovanje kao printf() funkcija, ali se ispis vrši u string, a ne na standardni
izlaz.


11.6 Nizovi stringova
    Nizovi stringova se jednostavno deklariraju i inicijaliziraju kao nizovi pokazivača na
char. Primjerice, za rad s igračkim kartama može se koristiti dva niza stringova: jedan za boju,
a drugi za lik karata. Deklariraju se na sljedeći način:

      char *boja[] = {"Srce", "Tref", "Karo", "Pik "};

      char *lik[]        = {"As", "2", "3", "4", "5",
                            "6", "7", "8", "9", "10",
                            "Jack", "Dama", "Kralj" };

Pojedinom stringu se pristupa pomoću indeksa, primjerice boja[2] označava string "Karo".
Pri deklaraciji se koristi pravilo da operator indirekcije ima niži prioritet od uglatih zagrada, tj.
da je deklaracija char *boja[] ekvivalentna deklaraciji: char *( boja []), pa se
iščitava: boja je niz pokazivača na char. Isto vrijedi i za upotrebu varijable lik. Primjerice,
*lik[i] se odnosi na prvi znak i-tog stringa (tj. lik[i][0]).
Primjer: Dijeljenje karata
Program karte.c služit će da se po slučajnom uzorku izmiješaju igrače karte. Za miješanje
karate koristi se proizvoljna metoda:
Postoje 52 karte. Njih se registrira u nizu cijelih brojeva int karte[52]. Svaka karta[i]
sadrži vrijednost iz intervala 0..51 koja označava kombinaciju boje i lika karte, prema pravilu :

      oznaka boje:        boja[karte[i] / 13];              /* string boja[0 do 3] */
      oznaka lika:        lik [karte[i] % 13];              /* string lik[0 do 12] */

jer ima 13 likova i 4 boje. To znači da ako se početni raspored inicijalizira s

      for (i = 0; i < BROJKARATA; i++)
           karte[i] = i;

karte će biti složene po bojama, od asa prema kralju (prva će biti as-srce a posljednja kralj-pik).
Za miješanje karata koristit će se standardna funkcija

      int rand();

koja je deklarirana u <stdlib.h>. Ova funkcija pri svakom pozivu vraća cijeli broj iz
intervala 0 do RAND_MAX (obično je to vrijednost 32767) koji se generira po slučajnom uzorku.
Kasnije će biti pokazano kako je implementirana ova funkcija.
    Miješanje karata se provodi na način da se položaj svake karte zamijeni s položajem koji se
dobije po slučajnom zakonu:

      for (i = 0; i < BROJKARATA; i++)
      {
            int k = rand() % BROJKARATA;
            swap (&karte[i], &karte[k]);
      }


      /* Datoteka: karte.c - dijeljenje karata*/




                                                                                                 155
#include <stdio.h>
      #include <stdlib.h>

      char *boja[] = {"Srce", "Tref", "Karo", "Pik "};

      char *lik[]    = {"As", "2", "3", "4", "5", "6", "7", "8",
                          "9", "10", "Jack", "Dama", "Kralj" };

      #define BROJKARATA 52

      void swap(int *x, int *y)
      {
        int t = *x;
           *x = *y;
           *y = t;
      }

      int main( void)
      {
      int i, karte[BROJKARATA];   /* ukupno 52 karte */

          for (i = 0; i <   BROJKARATA; i++)
            karte[i] = i;

          for (i = 0; i < BROJKARATA; i++)
          {
             int k = rand() % BROJKARATA;
             swap (&karte[i], &karte[k]);
          }

          for (i = 0; i < BROJKARATA; i++)
              printf("%s - %sn", boja[karte[i]/13], lik[karte[i]%13]);
          return 0;
      }



Dobije se ispis:

      Pik - 3
      Srce - 9
      Tref - 10
      Srce - 10
      Tref - 5
      Tref - 8
      Tref - Kralj
      . . . . .
      Pik - 7
      Srce - 3
      Tref - Dama
      Tref - Jack
      Tref - 6
      Karo - 8




                                                                      156
11.7 Generator slučajnih brojeva
     Znanstvenici su pokazali da se može realizirati niz slučajnih cijelih brojeva pomoću tzv.
linearne kongruencijske sekvence;

      xi = (a * xi-1 + b) % c

u kojoj se vrijednost i-tog broja dobije iz prethodno izračunate vrijednosti (xi-1) i tri konsatante
a,b,c. Vrijednost ovih konstanti se određuje iz uvjeta da brojevi iz ove sekvence imaju jednaku
vjerojatnost pojavljivanja, odnosno da imaju uniformnu razdiobu gustoće vjerojatnosti. U
ANSI/ISO standardnoj biblioteci implementiran je generator s konstantama a=1103515245,
b=12345 i c= 232, pomoću jedne globalne varijable seed i dvije funkcije rand() i
srand().

        static unsigned int seed = 1;

        int rand(void)
        {
         /* vraća pseudo-slučajni broj iz intervala 0..32767 */
            seed = seed * 1103515245 + 12345;
            return seed >> 16;
        }

        void srand(unsigned int start)
        {
         /* postavlja početnu vrijednost za rand() */
            seed = start;
        }

Slučajni broj generira funkcija rand() iskazom:

        seed = seed * 1103515245 + 12345;

Uočite da se ne vrši operacija mod 232, jer maksimalna veličina unsigned long je 232-1, pa
nije potrebno tražiti ostatak dijeljenja s tim brojem. Funkcija rand() vraća vrijednost koja je
sadržana u donjih 16 bita (seed>>16), jer se pokazalo da to daje najbolji statistički rezultat
(time je maksimalna veličina broja svedena na 32767). Postoje i kvalitetniji algoritmi za
generator slučajnih brojeva, ali ovaj način je odabran kao dobar kompromis između kvalitete i
brzine generiranja slučajnih brojeva.
     Početno, pri pokretanju programa, globalna varijabla seed ima vrijednost 1, a bilo bi
poželjno da to bude uvijek druga vrijednost. U tu svrhu se koristi funkcija srand(), pomoću
koje se početna vrijednost varijable seed postavlja na vrijednost argumenta ove funkcije.
Postavlja se pitanje kako osigurati da seed pri pokretanju programa uvijek ima drugačiju
vrijednost. Programeri obično koriste činjenicu da će startanje programa biti u nekom
jedinstvenom vremenu. Za očitanje vremena na raspolaganju je funkcija time() koja je
deklarirana u <time.h>:

      time_t time(time_t *tp)

Tip time_t je typedef za cjelobrojnu vrijednost . U većini implementacija funkcija time()
vraća vrijednost koja je jednaka vremenu u sekundama koje je proteklo od 01.10.1970. godine.
Ista se vrijednost postavlja i u dereferencirani pokazivač tp, ukoliko on nije NULL.
Sada se inicijalizacija generatora slučajnih brojeva može provesti sljedećim iskazom:




                                                                                                157
srand((unsigned int)time(NULL));

Zadatak: Uvrstite prethodni iskaz u program karte.c (također, dodajte direktivu
#include<time.h>). Pokrenite program nekoliko puta i provjerite da li se pri svakom
izvršenju programa dobije drugačija podjela karata.


11.8 Argumenti komandne linije operativnog sustava
     Funkcija main() također može imati argumente. Njoj argumente prosljeđuje operativni
sustav s komandne linije. Primjerice, pri pozivu kompilatora (c:>cl karte.c ) iz komandne linije
se kao argument prosljeđuje string koji sadrži ime datoteke koja se kompilira. Opći oblik
deklaracije funkcije main(), koja prima argumente s komandne linije, glasi:

          int main( int argc, char *argv[])

Parametri su:
     argc sadrži broj argumenata. Prvi argument (indeks nula) je uvijek ime samog programa,
     dakle broj argumenata je uvijek veći ili jednak 1.

          argv je niz pokazivača na char, svaki pokazuje na početni znak stringova koji su u
          komandnoj liniji odvojeni razmakom.

Primjer: Programom cmdline.c ispituje se sadržaj komandne linije

          /* Datoteka: cmdline.c */
          #include <stdio.h>
          int main( int argc, char *argv[])
          {
              int i;
              printf("Ime programa je: %sn", argv[0]);
              if (argc > 1)
              for (i = 1; i < argc; i++)
                  printf("%d. argument: %sn", i, argv[i]);
              return 0;
          }

Ako se otkuca:

          c:> cmdline Hello World

dobije su ispis:

          Ime programa: C:CMDLINE.EXE
          1. argument: Hello
          2. argument: World

jer je:

          argc = 3
          argv[0] = "C:CMDLINE.EXE"
          argv[1] = "Hello"
          argv[2] = "World"


Primjer: Program cmdsum.c računa sumu dva realna broja koji se unose u komandnoj liniji iza
imena izvršnog programa. Primjerice, ako se u komandnoj liniji otkuca:




                                                                                            158
C:>cmdsum 6.5 4e-2

program treba dati ispis

      Suma: 6.500000 + 0.040000 = 6.540000



      /* Datoteka: cmdsum.c */
      #include <stdio.h>
      #include <stdlib.h>

      int main( int argc, char *argv[])
      {
         float x,y;
         if (argc > 2) /* unose se dva argumenta */
         {
          x = atof(argv[1]);
          y = atof(argv[2]);
            printf ("Suma: %f + %f = %fn", x, y, x+y);
         }
         else
            printf("Otkucaj: cmdsum broj1 broj2 n");
         return 0;
      }




                                                          159
12 Dinamičko alociranje memorije


Naglasci:
   • slobodna memorija i alociranje memorije
   • malloc(), calloc(), realloc() i free()
   • dinamički nizovi
   • dinamičke matrice
   • brzi pristup memoriji

     Kada operativni sustav učita program, koji je formiran C-kompilatorom, on tom programu
dodijeli dio radne memorije koji čine tri cjeline: memorija izvršnog kôda programa, memorija
za statičke podatke (globalne i statičke varijable te literalne konstante), te memorija nazvana
stog (eng. stack) koja se koristi za privremeni smještaj automatskih varijabli (lokalne varijable i
formalni argumenti funkcija). Ostatak memorije se naziva slobodna memorija ili "heap".


    Operativni   Korisnički            Statički   Stog                   Slobodna memorija
                 program:
    sustav                             podaci     automatske varijable   "heap"
                 strojni kod

      Slika 12.1 Raspodjela memorije između operativnog sustava i korisničkog programa

     Nadzor nad korištenjem slobodne memorije vrši operativni sustav. U samom C-jeziku nije
implementirana mogućnost direktnog raspolaganja slobodnom memorijom, ali se, uz pomoć
pokazivača i funkcija biblioteke: malloc(), calloc(), realloc() i free(), može na
indirektan način koristiti slobodna memorija. Postupak kojim se dobiva na upotrebu slobodna
memorija naziva se dinamičko alociranje memorije. Pojam alociranje memorije asocira na
činjenicu da se dio slobodne memorije dodjeljuje korisničkom programu, dakle apstraktno se
mijenja njegova lokacija. Alociranje memorije se vrši tijekom izvršenja programa, pa se kaže da
je taj proces dinamički. Postupak kojim se alocirana memorija vraća na raspolaganje
operativnom sustavu naziva se oslobađanje ili dealociranje memorije.
     U ovom poglavlju biti će opisano kako se vrši dinamičko alociranje memorije i kako ta
programska tehnika omogućuje efikasno korištenje memorijskih resursa računala.


12.1 Funkcije za dinamičko alociranje memorije
Najprije će biti opisane funkcije za dinamičko alociranje memorije. One su deklarirane u
datoteci <stdlib.h>.

      void *malloc(size_t n)
      void *calloc(size_t n, size_t elsize)

Funkcijom malloc() alocira se n bajta slobodne memorije. Ako je alociranje uspješno funkcija
vraća pokazivač na tu memoriju, u suprotnom vraća NULL pokazivač. Primjerice, naredbom

      double *dp = malloc(10 * sizeof(double));




                                                                                               160
dobije se pokazivač dp, koji pokazuje na niz od 10 elemenata tipa double.

U deklaraciji funkcije malloc() označeno je da ona vraća je void *. To omogućuje da se
adresa, koju vraća funkcija malloc(), može pridijelili pokazivaču bilo kojeg tipa.
Funkcija calloc(n, elsize) je ekvivalentna malloc(n * elsize), uz dodatni uvjet da
calloc() inicijalizira sve bitove alocirane memorije na vrijednost nula.

Za dealociranje memorije poziva se funkcija:

      void free(void *p)

Funkcija free() prima kao argument pokazivač p. Uz pretpostavku da p pokazuje na memoriju
koja je prethodno alocirana fukcijom malloc(), calloc() ili realloc(), ova funkcija
dealocira (ili oslobađa) tu memoriju.

Promjenu veličine već alocirane memorije vrši se pozivom funkcije:

      void *realloc(void *ptr, size_t newsize)

Funkcija realloc() vrši promjenu veličine prethodno alocirane memorije, koja je pridijeljena
pokazivaču ptr, na veličinu newsize. Funkcija realloc() vraća pokazivač na tu memoriju.
Vrijednost toga pokazivača može biti ista kao i vrijednost od ptr, ako memorijski alokator
može prilagoditi veličinu zahtijevanog području slobodne memorije veličini newsize. Ukoliko
se to ne može ostvariti funkcija realloc() alocira novo područje memorije pa u njega kopira i
zatim oslobađa dio memorije na koju pokazuje ptr. Ukoliko se ne može izvršiti alokacija
memorije funkcija realloc() vraća NULL.

Napomena: poziv realloc(p, 0) je ekvivalentan pozivu free(p), a poziv realloc(0,
n) je ekivalentan pozivu malloc(n).
    Dobra je programerska praksa da se uvijek pri pozivu funkcije za alociranje memorije
provjeri da li ona vraća NULL. Ako se to dogodi u većini je slučajeva najbolje odmah prekinuti
program. U tu svrhu može se koristiti standardna funkcija exit() kojom se prekida korisnički
program, a operativnom se sustavu prosljeđuje argument ove funkcije. Primjerice,

      p = malloc(n);
      if(p == NULL)
      {
                printf("out of memoryn");
          exit(1);
      }
      /* … koristi pokazivač p */


Napomena: Prije uvođenja ANSI/ISO standarda prototip za malloc() je kod mnogih
kompilatora bio deklariran s: char *malloc(size_t n). Kod ovakvih kompilatora
potrebno je izvršiti eksplicitnu pretvorbu tipa vrijednosti koju vraća malloc(), primjerice

      double *p;
      p = (double *)malloc(n);

Ako se ne izvrši eksplicitna pretvorba tipa kompilator dojavljuje grešku o nekompatibilnosti
tipova.

Primjer: Dinamičko alociranje niza.




                                                                                          161
U programu allocarr.c alocira se niz od maxsize=5 cjelobrojnih elemenata. Korisnik
zatim unosi niz brojeva. Ako broj unesenih brojeva premaši maxsize, tada se povećava
alocirana memorija za dodatnih 5 elemenata. Unos se prekida kada korisnk otkuca slovo. Na
kraju se ispisuje niz i dealocira memorija.

     /*Datoteka: allocarr.c*/

     #include <stdio.h>
     #include <stdlib.h>

     void izlaz(char *string)
     {
         printf("%s", string);
         exit(1);
     }

     int main( void )
     {
         int *pa ;               /* pokazivač na niz cijelih brojeva */
         int maxsize = 5;        /* početna maksimalna veličina niza */
         int num = 0;            /* početno je u nizu 0 elemenata    */
         int j,data;

     /*
      * Početno alociraj memoriju za maxsize = 5 cijelih brojeva.
      * Koristi funkciju malloc koji vraća pokazivač pa.
      * Izvrši provjeru NULL vrijednosti pokazivača
      */

          pa = malloc(maxsize * sizeof( int ));
           if(pa == NULL )
              izlaz("Nema slobodne memorije za malloc.n") ;
     /*
     * Učitaj proizvoljan broj cijelih brojeva u niz pa[]
     * Ako broj premaši maxsize:
     * povećaj allocirani niz za 5 elemenata pomoću funkcije realloc
     * Kraj unosa je ako se umjesto broja pritisne neko slovo
     */
          while(scanf("%d", &data) == 1)
          {
               pa[num++] = data;   /* upisi podatak u memoriju  */

                 if(num >= maxsize) /* ako je memorija popunjena */
                 {                   /* povećaj alociranu memoriju */
                     maxsize += 5; /* za dodatnih 5 elemenata    */
                     pa = realloc( pa, maxsize * sizeof(int));
                     if(pa== NULL)
                        izlaz("Nema slobodne memorije za realloc.n") ;
                 }
          }
     /* Ispiši podatke iz dinamički alociranog niza */
          for(j = 0; j < num; j ++ )
               printf("%d n", pa[j]) ;

     /*    Konačno vrati memoriju na heap koristeći free() */
           free( pa ) ;
           return 0 ;
     }




                                                                                     162
Primjer: Pokazana je implementacija funkcije char * NumToString(int n), koja pretvara
cijeli broj n u string i vraća pokazivač na taj string.

      char *NumToString( int n)
      {
          char buf[100], *ptr;
          sprintf( buf, "%d", n);
          ptr = malloc( strlen( buf) + 1);
          strcpy( ptr, buf);
          return ptr;
      }

Primjer primjene funkcije NumToString:

      char *s;
      s = NumToString(56);
      printf("%sn", s);
      free(s);      /* !!! oslobodi memoriju kad više ne trebaš s */


Napomena: Ako se u nekoj funkciji alocira memorija i pridijeli pokazivaču koji je lokalna
varijabla, nakon izlaza iz funkcije taj pokazivač više ne postoji (zašto?). Alocirana memorija će
i dalje postojati ukoliko nije prije završetka funkcije eksplicitno dealocirana funkcijom
free(). Može se dogoditi da ta memorija bude "izgubljena" ako je se ne dealocira, ili ako se iz
funkcije ne vrati pokazivač na tu memoriju (kao u slučaju funkcije NumToString).

Primjer: Funkciji char * strdup(char *s) namjena je da stvori kopiju nekog stringa s, i
vrati pokazivač na novoformirani string.

      char *strdup( char * s)
      {
      char *buf, *ptr;
      int n = strlen(s);

      /* alociraj memoriju n+1 bajta za kopiju stringa s*/
      buf = malloc(n* sizeof(char)+1);

          /* kopiraj string u tu memoriju */
      if(buf != NULL)
             strcpy( buf, s);

           /* vrati pokazivač na string */
           return buf;
      }


Ova funkcija nije specificirana u standardnoj biblioteci C jezika, ali je implementirana u
biblioteci Visual C i gcc kompilatora.


12.2 Kako se vrši alociranje memorije
    Da bi se bolje shvatio proces alociranja memorije, bit će ilustrirano stanje slobodnje
memorije u slučaju kada se vrši višestruko alociranje/dealociranje memorije. Sivom bojom
označeno je područje slobodne memorije:




                                                                                             163
Neka najprije treba alocirati memoriju za string "Ivo". To se vrši iskazom:

      char *str1 = malloc(4);

Alocirana su 4 bajta, jer pored znakova 'I', 'v' i 'o', treba spremiti i znak '0'.

Ako se uspješno izvrši alokacija memorije, vrijedit će prikaz:

? ? ? ?


Upitnici označavaju da je sadržaj memorije, na koji str1 pokazuje, nedefiniran. Alocirana će
memorija biti inicijalizirana tek kada se izvrši funkcija:

      strcpy(str1, "Ivo");

Sada alocirana memorija ima definirani sadržaj:

I v o -
(crtica označava nul-znak)

Ako se nadalje izvrši alociranje memorije za string "Ivona",

      str2 = malloc(6);
      strcpy(str2, "Ivona");

vrijedi prikaz:

I v o -             I v o n a -

Između dva alocirana bloka nacrtano je manje područje slobodne memorije kako bi se istaklo
da memorijski alokator ne slaže alocirane blokove neposredno jedan do drugog. Zapravo,
memorijski alokator u alocirani sadržaj ispred svakog bloka upisuje i informacije o tom bloku,
primjerice veličinu samog bloka.

Realnu situaciju je bolje prikazati na sljedeći način:

4 I v o -              6 I v o n a -

           str1                    str2

Kada korisnik oslobodi memoriju na koju pokazuje str1, naredbom

      free(str1);

stanje slobodne memorije je takovo da postoji dio slobodne memorije ispred stringa "Ivona".

                       6 I v O n a -




                                                                                          164
Kaže se da je slobodna memorija fragmentirana. Pri višestrukim pozivima malloc/free može
doći do višestruke fragmentacije, čime se "gubi" jedan dio slobodne memorije.

Zapamtite:
   • O stanju slobodne memorije vodi računa posebni proces – memorijski alokator.
   • Nakon što se alocira memorija pomoću funkcije malloc(), treba je inicijalizirati.
   • Ako se želi inicijalizirati sadržaj memorije na vrijednost nula, koristi se funkcija
       calloc().
   • Ne smije se pretpostaviti da se sukcesivnim pozivima funkcije malloc() dobiva
       kontinuirani memorijski blok.


12.3 Alociranje višedimenzionalnih nizova
    Korištenje pokazivača i funkcija za dinamičko alociranja memorije omogućuje stvaranje
dinamičkih struktura podataka koje mogu imati promjenljivu veličinu tijekom izvršenja
programa. Nema posebnih pravila za stvaranje dinamičkih struktura podataka. U ovom i u
narednim poglavljima bit će pokazani različiti primjeri koji su korisni u programiranju i koji se
mogu koristiti kao obrazac za "dinamičko struktuiranje podataka" u C jeziku.
Najprije će biti opisano kako se dinamički alociraju višedimenzionalni nizovi i to nizovi
stringova i matrice promjenljivih dimenzija.

Dinamički nizovi stringova
Prije je pokazano da se u C jeziku pokazivači tipa char * mogu tretirati kao stringovi, uz uvjet
da pokazuju na niz znakova koji završava s nultim znakom. To omogućuje da se niz stringova
deklarira iskazom:

      #define N 100
      char *txt[N];

Ovaj niz stringova je statičan jer se njime može registrirati konačan broj stringova txt[i].
Uočite da je N konstanta.
    Niz stringova se može realizirati i dinamičkim alociranjem niza koji će sadržavati N
pokazivača tipa char *. To se postiže iskazima:

      int N = 100;               /* veličina niza je varijabla     */
      char **txt;                /* txt pokazuje na niz pokazivača */
      txt = calloc(N, sizeof(char*)); /* alociraj za N pokazivača */

U oba slučaja txt[i] predstavlja string. Razlika je pak od prethodne definicije niza stringova u
tome što se sada veličina niza može mijenjati tijekom izvršenja programa. Početno su svi
elementi niza jednaki NULL, što znači da niz nije inicijaliziran. Inicijalizacija niza se postiže
tako da se pokazivačima txt[i] pridijeli adresa nekog stringa. Primjerice, nakon sljedećih
operacija:

      txt[0]= strdup("Hello")
      txt[1]= strdup("World!")
      txt[2]= strdup("je prvi C program")

prva tri elementa niza txt[i] predstavljaju stringove, a ostali elementi niza su i dalje
neinicijalizirani.

Stanje u memoriji se može ilustrirati slikom 12.2.




                                                                                             165
Slika 12.2 Prikaz korištenja memorije za dinamički niz stringova


    Nakon upotrebe, potrebno je osloboditi memoriju koju zauzima niz stringova. To se postiže
tako da se najprije dealocira memorija koju zauzimaju stringovi.

      for(i=0; i<N; i++) {
           if(txt[i] != NULL)           /* oslobodi samo alocirane stringove */
           free(txt[i]);
      }

a zatim se dealocira memorija koju zauzima niz pokazivača txt;

      free(txt);

    Prikazana struktura je zgodna za unošenje proizvoljnog broja linija teksta, i može biti
temeljna struktura u editoru teksta. U ovu strukturu je lako unositi tekst , brisati tekst i
povećavati broj linija teksta.
Primjer: U programu alloctxt.c korisnik unosi proizvoljan broj linija teksta. Početno se
alocira memorija za 5 linija teksta. Ako korisnik unese više linija, tada se pomoću funkcije
realloc() povećava potrebna memorija za dodatnih 5 linija. Unos završava kada se otkuca
prazna linija. Nakon toga se vrši sortiranje teksta pomoću funkcije qsort(). Na kraju
programa ispisuje se sortirani tekst i dealocira memorija.

      /* Datoteka: alloctxt.c */
      #include <stdio.h>
      #include <stdlib.h>
      #include <string.h>

      int CmpString( const void *pstr1, const void *pstr2 );
      char *strdup( char * s);
      void memerror();

      int main( void )
      {
           char **txt;                     /* pokazivač na pokazivač stringa */
           int maxsize = 5;               /* početna maksimalna veličina niza */
           int numstrings = 0;            /* početno je u nizu 0 stringova */
           char str[256]={"0"};




                                                                                         166
int   i;

/*
 * Početno alociraj memoriju za maxsize=5 ,
 * Koristi malloc koji vraća pokazivač u txt.
 * Izvrši provjeru NULL vrijednosti pokazivača
 */
    txt = malloc(maxsize * sizeof(char *));
     if(txt == NULL )   memerror() ;

/*
 * Učitaj proizvoljan broj stringova u niz txt[]:
 * Prethodno alociraj memoriju sa svaki uneseni string
 * Ako broj stringova premaši maxsize:
 * povećaj allocirani niz za 5 elemenata pomoću funkcije realloc
 * Kraj unosa je ako se unese prazna linija.
 */

    while(gets(str) != NULL)
    {
        char *s = strdup( str) ;
        if(s== NULL) memerror();
        if(strlen(s)==0)
             break;
        txt[numstrings++] = s;
        if(numstrings >= maxsize)
         {
            maxsize += 5;
            txt = realloc( txt, maxsize * sizeof( int ));
            if(txt== NULL) memerror();
         }
    }

/* sortiraj niz stringova leksikografski */

    if(numstrings > 1)
    qsort((void *)txt, numstrings, sizeof(char*),   CmpString);

/* Ispiši podatke iz dinamički alociranog niza
 * i vrati memoriju na heap koristeći free()
 * za svaki pojedinačni string
 */

    for(i = 0; i < numstrings; i ++ )
    {
         puts(txt[i]) ;
         free( txt[i] );
    }

    free(txt);
    return 0 ;
}

int CmpString( const void *pstr1, const void *pstr2 )
{
/* Ova usporedna funkcija se koristi u funkciji qsort()
 * Prema dogovoru, u usporednu funkciju se šalju pokazivači
 * na element niza, u ovom slučaju
 * pokazivac na string, odnosno char **.




                                                                  167
* Za usporedbu stringova koristi se funkcija strcmp().
       * Pošto argument funkcije strcmp mora biti string (char *)
       * to znači da joj se mora poslati *pstr1 i *pstr2,
       * odnosno sadržaj argumenata pstr1 i pstr2,
       * koji su pokazivaci tipa char**
       */
         return strcmp( *(char **) pstr1, *(char **)pstr2 );
      }

      void memerror()
      {   /* funkcija za dojavu pogreške */
          printf("%s", "Greska pri alociranju memorije");
          exit(1);
      }

Dinamičke matrice
    Matrica je skup istovrsnih elemenata koji se obilježavaju s dva indeksa - mat[i][j]. Prvi
indeks predstavlja redak, a drugi indeks predstavlja stupac matrice. Matrica se može
programski realizirati kao dinamička struktura. Primjerice, matrica imena mat, koja će
sadržavati elemente tipa double, se sljedećim postupkom:




                 Slika 12.3 Prikaz korištenja memorije za dinamičke matrice

1. Identifikator matrice se deklarira kao pokazivač na pokazivač na double, a dvije varijable
koje označavaju broj redaka i stupaca matrice se iniciraju na neku početnu vrijednost.

      int brojredaka = 5, brojstupaca=10;
      double **mat;

2. Alocira se memorija za niz pokazivača na retke matrice (mat[i])

      mat = malloc( brojredaka * sizeof(double *));

3. Zatim se alocira memorija za svaki pojedini redak. To zauzeće je određeno brojem stupaca
matrice:

      for(k = 0; k < brojredaka; k++)
          mat[i] = malloc( brojstupaca * sizeof(double));

4. Ovime je postupak formiranja matrice završen. Nakon toga se mogu raditi operacije s
matricom. Primjerice, iskaz

      for(i = 0; i < brojredaka; i++)
           for(j = 0; j < brojstupaca; j++)
                mat[i][j] = 0;




                                                                                         168
postavlja sve elemente matrice na vrijednost nula.

5. Nakon završetka rada s matricom potrebno je osloboditi alociranu memoriju. To se postiže
tako da se najprije dealocira memorija koju zauzimaju elementi matrice;

      for(k=0; k < brojredaka; k++)
           free(mat[k]);

a zatim se dealocira memorija koju zauzima niz pokazivača retka;

      free(mat);

Iako ovaj postupak izgleda dosta kompliciran, on se često koristi jer se njime omogućuje rad s
matricama čije se dimenzije mogu mijenjati tijekom izvršenja programa. Također, pogodno je
pisati funkcije s ovakvim matricama, jer se mogu primijeniti na matrice proizvoljnih dimenzija.

Primjerice, funkcija:

      void    NulMat(double **mat, int brojredaka, int brojstupaca)
      {
             for(int i = 0; i < brojredaka; i++)
                  for(int j = 0; j < brojstupaca; j++)
                       mat[i][j] = 0;
      }

može poslužiti da se postavi vrijednost elemenata bilo koje matrice na vrijednost 0. Ovo nije
moguće kod korištenja statičkih nizova je se tada u definiciji funkcije, u parametru koji
označava dvodimenzionalni niz, uvijek mora označiti broj kolona.

Funkcija PrintMat() može poslužiti za ispis matrice u proizvoljnom formatu:

      void    PrintMat(double **mat, int brojredaka,
                       int brojstupaca, char *format)
      {
             int i,j;
             for( i = 0; i < brojredaka; i++)
             {
                  for(j = 0; j < brojstupaca; j++)
                       printf (format, mat[i][j]);
                  printf("n") ;
               }
      }


Sam postupak alociranja i dealociranja matrice se može formalizirati s dvije funkcije

      double **AlocirajMatricu (int brojredaka,int brojstupaca)
      {
      int k;
           double **mat = malloc( brojredaka * sizeof(double *));
           if(mat == NULL) return NULL;
           for(k = 0; k < brojredaka; k++)
                mat[k] = malloc( brojstupaca * sizeof(double));
           return mat;
      }

      void DealocirajMatricu(double **mat, int brojredaka)




                                                                                           169
{
            int k;
            for(k=0; k < brojredaka; k++)
                 free(mat[k]);
            free(mat);
      }


Primjer: U programu allocmat.c formira se matrica s 3x4 elementa, ispunja slučajnim
brojem i ispisuje vrijednost elemenata matrice. Zatim se ta matrica dealocira i ponovo alocira s
2x2 elementa.

      /* Datoteka: allocmat.c */
      #include <stdio.h>
      #include <stdlib.h>
      #include <math.h>

      double **AlocirajMatricu (int brojredaka,int brojstupaca);
      void DealocirajMatricu(double **mat, int brojredaka);
      void PrintMat(double **mat, int brojredaka, int brojstupaca, char
      *format);

      int main( void )
      {
         int i,j;
         double **mat;           /* matrica */
         int rd= 3;              /* broj redaka */
         int st= 4;             /* broj stupaca */

      /* formiraj matricu sa 3x4 elementa*/
         mat = AlocirajMatricu(rd, st);
         if(mat == NULL) exit(1);

      /* ispuni matricu sa slučajnim vrijednostima */
         for(i = 0; i < rd; i++)
            for(j = 0; j < st; j++)
                mat[i][j] = (double)rand();

      /* ispisi vrijednosti */
         PrintMat(mat, rd, st, "%12.2lf");

          DealocirajMatricu(mat, rd);

      /* sada formiraj drugu matricu sa 2x2 elementa*/
         rd = 2; st = 2;
         mat = AlocirajMatricu(rd, st);
         if(mat == NULL) exit(1);

          for(i = 0; i < rd; i++)
             for(j = 0; j < st; j++)
                 mat[i][j] = (double)rand();

          /* ispisi vrijednosti */

          PrintMat(mat, rd, st, "%12.2lf");
          DealocirajMatricu(mat, rd);

          return 0 ;




                                                                                            170
}


      /* definiraj potrebne funkcije */

      Dobije se ispis:

              41.00         18467.00         6334.00           26500.00
           19169.00         15724.00        11478.00           29358.00
           26962.00         24464.00         5705.00           28145.00

           23281.00         16827.00
            9961.00           491.00



12.4 Standardne funkcije za brzi pristup memoriji
U standardnoj biblioteci je definirano nekoliko funkcija koje su optimirane za brzi pristup
memoriji.
      void *memcpy(void *d, const void *s, size_t n)
Kopira n znakova s adrese s na adresu d, i vraća d.

      void *memmove(void *d, const void *s, size_t n)
Ima isto djelovanje kao memcpy, osim što vrijedi i za preklapajuća memorijska područja.

      int memcmp(const void *s, const void *t, size_t n)
uspoređuje prvih n znakova s i t; vraća rezultat kao strcmp.

      void *memchr(const void *m, int c, size_t n)
vraća pokazivač na prvu pojavu znaka c u memorijskom području m, ili NULL ako znak nije
prisutan među prvih n znakova.

      void *memset(void *m, int c, size_t n)
postavlja znak c u prvih n bajta od m , vraća m.


Primjer: prethodnu funkciju NulMat() može se napisati pomoću memset() funkcije:

      void    NulMat(double **mat, int brojredaka, int brojstupaca)
      {
             for(int i = 0; i < brojredaka; i++)
                  memset(mat[i], 0, brojstupaca*sizeof(double)
      }




                                                                                          171
13 Korisnički definirane strukture
podataka


Naglasci:
   • korisnički definirane strukture podataka
   • članovi strukture
   • prijenos struktura u funkcije
   • strukture i funkcije za očitanje vremena
   • unija podataka
   • bit polja
   • pobrojani tipovi


U C-jeziku je implementiran mehanizam za definiranje korisničkih tipova podataka. To su:

    1. Struktura - tip koji označava uređen skup varijabli, koje mogu biti različitog tipa
    2. Unija - tip kojim se jednom memorijskom objektu pridjeljuje skup tipova
    3. Pobrojani tip (ili enumeracija) - tip definiran skupom imenovanih cjelobrojnih
       konstanti


13.1 Struktura (struct)
    Struktura je skup od jedne ili više varijabli, koje mogu biti različitog tipa, grupiranih
zajedno pod jednim imenom radi lakšeg rukovanja. Varijable koje čine strukturu nazivaju se
članovi ili polja strukture. Član strukture može biti bilo koji tip koji je dozvoljen u C jeziku
(prosti tip, pokazivački tip, niz ili prethodno definirani korisnički tip).

     Za definiranje strukture koristi se ključna riječ struct. Tako nastaje složeni strukturni tip
podataka - struktura - pomoću koje se mogu deklarirati varijable odnosno memorijski objekti
strukturnog tipa.
U radu sa strukturnim tipovima potrebno je znati:
        1. Kako se deklarira struktura i njezini članovi,
        2. Kako se deklariraju varijable strukturnog tipa,
        3. Kako se operira sa strukturnim varijablama,
        4. Kako se može izvršiti inicijaliziranje početne vrijednosti varijable strukturnog tipa.

Strukturni tip se definira prema sljedećem sintaktičkom pravilu:

      struct oznaka_strukture
      {
              tip1 oznaka_člana1;
              tip2 oznaka_člana2;
              ....
      };




                                                                                                172
struct      oznaka_strukture        lista_varijabli;

      oznaka_varijable.oznaka_člana

Primjerice, zapisom

      struct _tocka
      {
         int x;
         int y;
      };

deklarirana je struktura imena _tocka, koja ima dva člana: x i y. Pomoću ove strukture zgodno
je deklarirati varijable koje će označavati položaj točke u pravokutnom koordinatnom sustavu.
Na slici 13.1 prikazan je pravokutnik kojeg određuju dvije točke T1 i T2. Deklarira ih se
iskazom

      struct _tocka T1, T2;

a vrijednost koordinata se postavlja u sljedećoj sekvenci:

      T1.x   =   2;
      T1.y   =   1;
      T2.x   =   6;
      T2.y   =   4;




                      Slika13. 1. Označavanje koordinata točke i pravokutnika

Ukoliko se želi izračunati površinu pravokutnika može se pisati:

      int povrsina;
      .....
      povrsina = (T2.x – T1.x)*(T2.y-T2.x)
      .......

Na ovaj način je dobiven programski zapis kojim se poboljšava apstrakcija. Dalje se razina
apstrakcije može povećati na način da se deklarira struktura _pravokutnik koja ima dva člana
koji predstavljaju točke dijagonala:

      struct _pravokutnik




                                                                                         173
{
          struct _tocka p1;
          struct _tocka p2;
      }

Dakle, članovi strukture mogu biti prethodno definirani strukturni tipovi.

Sada se prijašnji programski segment može zapisati sljedećom sekvencom:

      int povrsina;
      struct _pravokutnik pravokutnik;
      .....
      pravokutnik.p1.x = 2;
      pravokutnik.p1.y = 1;
      pravokutnik.p2.x = 6;
      pravokutnik.p2.y = 4;
      ......
      povrsina = ( pravokutnik.p2.x – pravokutnik.p1.x)
                *( pravokutnik.p2.y - pravokutnik.p2.x)


Uočite da se za pristup članovima strukture _tocka, koja je član strukture _pravokutnik,
pristupa tako da se dva puta koristi točka-operator.
Potrebno je navesti još neka sintaktička pravila za rad sa strukturama.

1. Može se istovremeno izvršiti deklariranje strukturnih varijabli i same strukture, primjerice

      struct _tocka {int x; int y;} T1,T2;

Ako kasnije neće biti potrebno deklarirati nove varijable ovog tipa, može se izostaviti oznaka
strukture, tj.

      struct {int x; int y;} T1,T2;

2. Pomoću typedef može se definirati sinonim za strukturu, primjerice

      typedef struct tocka _tocka;

      typedef struct _pravokutnik
      {
         _tocka p1;
         _tocka p2
      }
      pravokutnik_t;

Sada _tocka i pravokutnik_t predstavljaju oznake tipova pomoću kojih se može deklarirati
varijable (bez navođenja ključne riječi struct), primjerice

      _tocka T1, T2;
      pravokutnik_t pravokutnik;

3. Inicijaliziranje početnih vrijednosti se može izvršiti na sličan način kao kod nizova, tako da se
pripadajuća lista početnih vrijednosti navede unutar vitičastih zagrada.

      struct _tocka T1 = {2,1}, T2 = {6,4};
      struct _pravokutnik pravokutnik = { {2,1}, {6,4} };




                                                                                                  174
Ako se struktura inicijalizira djelomično, elementi koji nisu inicijalizirani se postavljaju na
vrijednost nula.
Strukture i nizovi
    Elementi strukture mogu biti nizovi. Također, može se formirati nizove struktura.
Primjerice, struktura za vođenje evidencije o ocjenama pojedinog studenta može imati oblik:


      typedef struct _studentinfo
      {
         char ime[30];
         int ocjena;
      } studentinfo_t;

Uz pretpostavku da nastavu pohađa 30 studenata, za vođenje evidencije o studentima može se
definirati niz:

      studentinfo_t student[30];

Pojedinom članu strukture, koji je element niza, pristupa se na način da se točka operator piše
iza uglatih zagrada, primjerice:

      student[0].ocjena = 2;
      student[1].ocjena = 5;

Struktura kao argument funkcije
     Strukture se, za razliku od nizova, tretiraju kao "tipovi prve klase". To znači da se može
pridijeliti vrijednost jedne strukturne varijable drugoj, da se vrijednost strukturne varijable može
prenositi kao argument funkcije i da funkcija može vratiti vrijednost strukturne varijable.
Program struct.c demonstrira upotrebu strukturnih varijabli u argumenatima funkcije.

      /* Datoteka: struct.c                         */
      /* Primjer definiraja strukture i koristenja */
      /* strukture kao argumenta funkcije          */

      #include <stdio.h>
      #include <string.h>

      typedef struct _student
      {
         char ime[30];
         int ocjena;
      }studentinfo_t;

      void display( studentinfo_t st );

      int main()
      {
         studentinfo_t          student[30];
         int i=0;

          strcpy( student[0].ime, "Marko Matic" );
          student[0].ocjena = 4;
          strcpy( student[1].ime, "Marko Katic" );
          student[1].ocjena = 2;
          strcpy( student[2].ime, "Ivo Runjanin" );
          student[2].ocjena = 1;




                                                                                                175
strcpy( student[3].ime, "" );
          student[3].ocjena = 0;

          while (student[i].ocjena != 0 )
             display( student[i++]);

          return 0;
      }

      void display(studentinfo_t st)
      {
         printf( "Ime: %s ", st.ime );
         printf( "tocjena: %dn", st.ocjena );
      }


Rezultat izvršenja programa je:

      Ime: Marko Matic                  ocjena: 4
      Ime: Marko Katic                  ocjena: 2
      Ime: Ivo Runjanin                 ocjena: 1

     U programu se prvo inicijalizira prva tri elementa niza koji su tipa studentinfo_t.
Četvrtom elementu se vrijednost člana ocjena postavlja na vrijednost nula. Ova nulta ocjena će
kasnije služiti kao oznaka elementa niza, do kojeg su uneseni potpuni podaci. Ispis se vrši
pomoću funkcije display(studentinfo_t).
     U prethodnom programu strukturna varijabla se prenosi u funkciju po vrijednosti. Ovaj
način prijenosa strukture u funkciju nije preporučljiv jer se za prijenos po vrijednosti na stog
mora kopirati cijeli sadržaj strukture. To zahtijeva veliku količinu memorije i procesorskog
vremena (u ovom slučaju veličina strukture je 34 bajta). Mnogo bolji način prijenosa strukture u
funkciju je da se koristi pokazivač na strukturu, a da se zatim u funkciji elementima strukture
pristupa indirekcijom.
Pokazivači na strukturne tipove i operator ->
Neka su deklarirani varijabla i pokazivač tipa studentinfo_t:

      studentinfo_t St, *pSt;

i neka je varijabla St inicijalizirana slijedećim iskazom:

      St.ime = strcpy("Ivo Boban");
      St.ocjena = 1;

Ako se pokazivač pSt inicijalizira na adresu varijable St, tj.

      pSt = &St;

tada se može pristupiti varijabli St i pomoću dereferenciranog pokazivača. Primjerice, ako se
želi promijeniti ocjenu na vrijednost 2 i ispisati novo stanje, može se koristiti iskaze:

      (*pSt).ocjena = 2;
      printf("Ime: %s, ocjena: %d", (*pSt).ime, (*pSt).ocjena)

Uočite da se zbog primjene točka operatora dereferencirani pokazivač mora napisati u
zagradama, jer bi inače točka operator imao prioritet. Ovakav način označavanja indirekcije je
dosta kompliciran, stoga je u C jeziku definiran operator ->, pomoću kojeg se prethodni iskazi
zapisuju u obliku:




                                                                                            176
pSt->ocjena = 2;
      printf("Ime: %s, ocjena: %d", pSt->ime, pSt->ocjena)

Vrijedi ekvivalentni zapis

      (*pStudent).ocjena       ⇔    pStudent->ocjena

      Operator -> označava pristup elementu strukture pomoću pokazivača i može ga se
      zvati operatorom indirekcije pokazivača strukture.

Primjer: U programu structp.c pokazano je kako se prethodni program može napisati
mnogo efikasnije korištenjem pokazivača. U ovom primjeru pokazivači će se koristiti i kao
članove strukture i pomoću njih će se vršiti prijenos strukturne varijable u funkciju.

      /* Datoteka: structp.c
       * Primjer definiraja strukture pomocu pokazivačkih članova i
       * korištenje pokazivača strukture kao argumenta funkcije
       */

      #include <stdio.h>
      #include <stdlib.h>       /* def. NULL */

      typedef struct _student
      {
         char *ime;
         int ocjena;
      }studentinfo_t;

      void display( studentinfo_t *pSt );

      int main()
      {

          int i=0;
          studentinfo_t *p;
          studentinfo_t student[30] = {
               { "Marko Matic", 4 },
               { "Marko Katic", 2 },
               { "Ivo Runjanin", 1 },
               { NULL, 0}
               };

          p=&student[0];
          while (p->ime != NULL )
             display( p++);

          return 0;
      }

      void display(studentinfo_t *pS)
      {
         printf( "Ime: %s ", pS->ime );
         printf( "tocjena: %dn", pS->ocjena );
      }




                                                                                       177
Prvo što se treba uočiti u ovom programu je način kako je deklarirana struktura _student . U
njoj sada nije rezerviran fiksni broj mjesta za član ime, već ime sada predstavlja pokazivač na
char (string).

      typedef struct _student
      {
         char *ime;
         int ocjena;
      }studentinfo_t;

Niz student je inicijaliziran pri samoj deklaraciji sa:

      studentinfo_t student[30] = {
              { "Marko Matic", 4 },
              { "Marko Katic", 2 },
              { "Ivo Runjanin", 1 },
              { NULL, 0}
              };

     Ovime se postiže ušteda memorije jer se za string ime rezervira točno onoliko mjesta koliko
je upisano inicijalizacijom (plus nula!). Pomoćnim pokazivačem p, kojeg se početno inicira na
adresu nultog elementa niza student, pretražuje se niz sve dok se ne dođe do elementa kojem
član ime ima vrijednost NULL pokazivača. Dok to nije ispunjeno vrši se ispis sadržaja niza
pomoću funkcije display(), kojoj se kao argument prenosi pokazivač na tip
studentinfo_t. Unutar funkcije display() elementima strukture se pristupa indirekcijom
pokazivača strukture (->).
     Na kraju, da se zaključiti, da prijenos strukture u funkciju u pravilu treba vršiti pomoću
pokazivača. Isto vrijedi za slučaj kada funkcija vraća vrijednost strukture. I u tom slučaju je
bolje raditi s pokazivačima na strukturu, jer se štedi memorija i vrijeme izvršenja programa.
Prenošenje strukture po vrijednosti se može tolerirati samo u slučajevima kada struktura ima
malo zauzeće memorije.
Memorijska slika strukturnih tipova
     U tablici 13.1 dana je usporedba karakteristika nizova i struktura. Važno je uočiti da
članovi struktura u memoriji nisu nužno poredani neposredno jedan do drugog. Razlog tome je
činjenica da je dozvoljeno da se članovi strukture smještaju u memoriju na način za koji se
ocjenjuje da će dati najbrže izvršenje programa (većina procesora brže pristupa parnim
adresama, nego neparnim adresama).


    karakteristika            niz                              struktura
    strukturalni sadržaj      kolekcija elemenata istog tipa   kolekcija imenovanih članova koji
                                                               mogu biti različitog tipa
    pristup     elementima    elementima    se    pristupa     članovima se pristupa pomoću imena
    složenog tipa             pomoću indeksa niza
    raspored elemenata u      u nizu jedan do drugog           u nizu, ali ne nužno jedan do drugog
    memoriji računala

                             Tablica 13.1. Usporedba nizova i struktura

    Kod kompilatora Microsoft Visual C može se posebnom direktivom (#pragma pack(1))
zadati da se članovi strukture "pakiraju" u memoriju jedan do drugog. To pokazuje program
pack.c.




                                                                                                      178
/* Datoteka: pack.c ***********************************/
      /* Primjer memorijski pakirane i nepakirane strukture */

      #include <stdio.h>

      struct s1
      {
         char c;
         short i;
         double d;
      } v1;

      #pragma pack(1)       /* forsiraj pakiranu strukturu */

      struct s2
      {
         char c;
         short i;
         double d;
      } v2;

      int main( void)
      {
        printf("Minimalno zauzece memorije: %d bajtan",
                sizeof(char)+sizeof(short)+ sizeof(double));
        printf("Zauzece memorije nepakiranom strukturom: %d bajtan",
                sizeof(struct s1));
        printf("Zauzece memorije pakiranom strukturom: %d bajtan",
                sizeof(struct s2));

          printf("nAdresa elementa          : nepakirano pakiranon");
          printf("Adresa od c     :          %p    %pn", &v1.c, &v2.c);
          printf("Adresa od i     :          %p    %pn", &v1.i, &v2.i);
          printf("Adresa od d     :          %p    %pn", &v1.d, &v2.d);

          return 0;
      }


Nakon izvršenja programa dobije se poruka:

      Minimalno zauzece memorije: 11 bajta
      Zauzece memorije nepakiranom strukturom: 16 bajta
      Zauzece memorije pakiranom strukturom: 11 bajta

      Adresa   elementa   :   nepakirano      pakirano
      Adresa   od c       :   00406B90        00406BA0
      Adresa   od i       :   00406B92        00406BA1
      Adresa   od d       :   00406B98        00406BA3


Uočite da je zauzeće memorije pakirane strukture minimalno i jednako zbroju veličine zauzeća
memorije pojedinog člana, dok kod nepakirane strukture ostaje neiskorišten jedan bajt između
char i short, te dva bajta između short i double.

      Ne smije se pretpostaviti veličina zauzeća memorije na temelju zauzeća memorije
      pojedinog člana strukture. Za određivanje veličine memorije koju zauzima
      strukturni tip uvijek treba koristiti operator sizeof.




                                                                                        179
13.2 Union – zajednički memorijski objekt za različite tipova podataka
    S ključnom riječi union definira se korisnički tip podataka pomoću kojeg se nekom
memorijskom objektu može pristupiti s različitim tipskim pristupom. Deklaracija unije sliči
deklaraciji strukture, primjerice,

      union skalar
      {
          char c;
          int i;
          double d;
      };

međutim, njeno značenje je bitno drukčije.

Ako se s unijom skalar deklarira objekt imena obj, tj.

      union skalar obj;

tom se objektu može pristupiti na više načina, jer obj može sadržavati vrijednost tipa char,
int ili double. Dozvoljeno je pisati:

      obj.c = 'A';
      obj.i = 1237;
      obj.d = 457.87

Sve ove vrijednosti se upisuju u istu memorijsku lokaciju. Veličina zauzeća memorije je
određena elementom unije koji zauzima najveći prostor memorije.
    U programiranju se često unija koristi unutar neke strukture kojoj posebni element služi za
označavanje tipa vrijednosti koji trenutno sadrži unija. Primjerice,

      #define char_type   0
      #define int_type    1
      #define double_type 2

      struct variant
      {
          int var_type;              /* oznaka tipa vrijednosti unije skalar */
          union skalar var;
      };

      struct variant obj;

      /* pored vrijednosti, zabilježimo i tip vrijednosti*/

      obj.var.c = 'A';         obj.var_type = char_type;
      obj.var.i =1237;         obj.var_type = int_type;
      obj.var.d =457.87        obj.var_type = double_type;
      ..........

      /* uniju koristimo tako da se najprije provjeri tip vrijednosti */

      switch(obj.val_type)
      {
         case char_type:   printf("%c", obj.var.c); break;
         case int_type:    printf("%d", obj.var.i); break;
         case double_type: printf("%f", obj.var.d); break;




                                                                                           180
}



13.3 Bit-polja
    Unutar strukture ili unije može se specificirati veličinu cjelobrojnih članova u bitovima .
To se izvodi tako da se iza člana strukture navede dvotočka i broj bitova koliko taj član
zauzima. Primjerice

      struct bitfield1
      {
          int i3b : 3;
          unsigned int i1b : 1;
          signed int i7b : 7;
      };


U ovoj deklaraciji je definirano da u strukturi bitfield1 član i3b je cijeli broj od 3-bita, član
i1b je 1-bitni kardinalni broj , a član i7b je 7-bitni cijeli broj. (uočite da 1-bitni član može
imati samo dvije vrijednosti 0 i 1)
    Ovakve strukture se koriste u cilju uštede memorijskog prostora, posebno u slučaju kad se
većina članova tretira kao logička vrijednost. Članovima strukture se pristupa kao da su
normalni cijeli brojevi, a kompilator vodi računa o tome da se maskiraju nepostojeći bitovi.

      struct bitfield1 a,b;
      a.i3b=3;
      b.i7b=65;

     Manipuliranje s ovakvim strukturama je potpuno pod kontrolom kompilatora. Zbog toga se
ne može koristiti pokazivače na članove strukture jer oni nisu direktno adresibilni. Iz istog
razloga nije moguće koristiti nizove bitnih-polja.
     Kada se želi kontrolirati kako se slažu bit-polja unutar jedne riječi, na raspolaganju su dva
mehanizma. Prvi je da se umetnu bezimeni članovi koji će predstavljati "prazne bitove". Drugi
je mehanizam da veličina bezimenog polja može biti 0. To je poruka kompilatoru da se na tom
mjestu završi pakiranje u jednu riječ, i da se od tog mjesta članovi strukture pakiraju u novu
riječ. Primjerice
      struct bitfield2
      {
          int i3b : 3;
          unsigned int i1b : 1;
          signed int i7b : 7;
          int : 2;
          int i2b: 2;
          int : 0;
          int i4b : 4, i5b : 5;
      };

opisuje strukturu koja se pakira u dvije riječi. Prva sadrži redom 3-, 1-, i 7-bitna polja, zatim 2-
bitnu prazninu, te 2-bitno polje i2b, a druga riječ sadrži 4-bitna i 5-bitna polja i4b i i5b.




                                                                                                181
13.4 Pobrojanji tip (enum)
     Treća klasa korisnički definiranih tipova C jezika je tzv. pobrojani tip (eng. enumeration).
Deklarira se pomoću ključne riječi enum. Služi definiranju integralnog cjelobrojnog tipa kojemu
se skup vrijednosti označava simboličkim (imenovanim ) konstantama u sljedećoj notaciji:
    pobrojani_tip:
           enum ime_tipa { lista_definicije_ konstanti };

     definicija_ konstanti:
              ime
             ime = konstanta .

Primjerice, deklaracijom
          enum dani_t {Nedjelja, Ponedjeljak, Utorak, Srijeda,
                       Cetvrtak, Petak, Subota};

definira se korisnički pobrojani tip dani_t kojem se vrijednost označava simboličkim
konstantama: Nedjelja, Ponedjeljak, Utorak, Srijeda, Cetvrtak, Petak,
Subota. Pri kompiliranju programa ovim se konstantama pridjeljuje numerička vrijednost
prema pravilu da prvo ime u listi ima numeričku vrijednost 0, drugo ime ima vrijednost 1, treće
ime ima vrijednost 2 itd.

Pomoću pobrojanog tipa mogu se deklarirati varijable, prema pravilu

      deklaracija_varijable: enum ime_tipa lista_varijabli;

primjerice,

      enum dani_t danas, sutra;
          .......
      danas = Srijeda;
      sutra = Cetvrtak;      ........

Vrijednost se neke ili svih simboličkih konstanti može inicijalizirati već pri samoj deklaraciji
pobrojanog tipa. Primjerice,

      enum karte_t {AS = 1, JACK = 11, DAMA,                  KRALJ};

Neinicijalizirane konstante imaju vrijednost za jedan veću od vrijednosti prethodne konstante,
tako DAMA ima vrijednost 12, a KRALJ ima vrijednost 13.
    Pobrojane tipove ne treba smatrati posebno “čvrstim” tipovima jer ih se može tretirati
ravnopravno s cjelobrojnim tipom. Primjerice, dozvoljeno je pisati:

      int i = Subota;            enum karte_t c = 10;
      c++;                        /* c postaje jack (11) */

Ponekad se enum koristi samo za definiranje simboličkih konstanti (poput #define direktive).
Umjesto korištenja leksičkih direktiva:

      #define ONE 1
      #define TWO 2
      #define THREE 3

može se koristiti enum deklaracija:




                                                                                             182
enum threenum {ONE=1, TWO, THREE};



13.5 Strukture i funkcije za očitanje vremena
U datoteci <time.h> definirano je nekoliko funkcija i struktura imena tm, za očitanje i
manipuliranje vremena i datuma.
Dosada je za očitanje vremena korištena funkcija

             time_t time(time_t *tp);

koja vraća vrijednost time_t tipa, tj. kardinalni broj koji predstavlja trenutno vrijeme (obično
je to broj sekundi od 1.1.1970.). Parametar tp, ako nije NULL, također prihvaća trenutno
vrijeme u *tp.
    Da bi se olakšalo pretvorbu ovog vremena u stvarno vrijeme i datum, definirana je struktura
tm sa sljedećim članovima:

      struct tm                        /*   opisuje vrijeme i datum */
      {      int     tm_sec;           /*   sekunde           0..61 */
             int     tm_min;           /*   minute            0..59 */
             int     tm_hour;          /*   sat               0..23 */
             int     tm_mday;          /*   dan               1..31 */
             int     tm_mon;           /*   mjesec            0..11 */
             int     tm_year;          /*   broj godina nakon 1900 */
             int     tm_wday;          /*   dan u sedmici      0..6 */
             int     tm_yday;          /*   dan u godini     0..365 */
             int     tm_isdst;         /*   da li je dan promjene sata 0..1 */
      };

Napomena: ako dan promjene sata nije implementiran tada tm_isdst ima negativnu
vrijednost.
    Broj sekundi može biti veći od 59 u slučaju prestupnog vremena. Mjeseci su kodiranu tako
da 0 označava siječanj, 1 veljaču itd. Dani u sedmici su kodirani tako da 0 označava nedjelju, 1
ponedjeljak itd. Stvarna godina se dobije tako da se članu tm_year doda vrijednost 1900
(primjerice u godini 2002. godini član tm_year sadrži vrijednost 102).
localtime, gmtime
    Pretvorbu vremena iz formata time_t u struct tm vrši se funkcijom localtime(),
kada se želi dobiti lokalno vrijeme, ili funkcijom gmtime() za dobiti univerzalno vrijeme u
nultom meridijanu.

      struct tm *localtime(const time_t *t);
      struct tm *gmtime(const time_t *t);

Obje funkcije primaju adresu varijable koja sadrži vrijeme u formatu time_t, a vraćaju
pokazivač na statičku strukturu tipa tm (sadržaj se obnavlja pri svakom pozivu ovih funkcija) .
ctime, asctime
Ako se želi dobiti zapis vremena u obliku stringa, mogu se koristiti funkcije

      char *ctime(const time_t *t);
      char *asctime(const struct tm *tp);




                                                                                            183
Funkcija ctime() za argument koristi adresu varijable koja sadrži vrijeme u formatu time_t,
a funkcija asctime()za argument koristi pokazivač na strukturu tm. Obje funkcije vraćaju
pokazivač statičkog stringa koji sadrži zapis vremena u standardnom formatu. Primjerice,
sekvenca naredbi

      time_t t = time(NULL);
      char *s = ctime(&t);
      puts(s);

generira ispis:

      Sat May 11 14:21:20 2002

Uočite da je rezultat poziva ctime(&t) ekvivalentan pozivu asctime(localtime(&t)) .

Standardna verzija je prilagođena američkim standardima. Ako se želi napisati vrijeme u
formatu

      11.05.2002        14:21

tada se može koristiti sljedeće iskaze:

      /* ispisuje datum i vrijeme u formatu                      11.05.2002 14:21 */
      time_t t = time(NULL);
      struct tm *p = localtime(&t);
      printf("%.2d.%.2d.%.2d %2d:%.2dn",
             p->tm_mday, p->tm_mon + 1,
              p->tm_year +1900,
             p->tm_hour, p->tm_min);

strftime
   Funkcija strftime() se koristi za formatirani ispis vremena. Format se zadaje kao kod
printf() funkcije. Prototip funkcije strftime()glasi:

      size_t strftime(char *buf, size_t bufsize,
              const char *fmt, const struct tm *tp);

Prvi argument je string str u koji se vrši formatirani zapis. Drugi argument (bufsize)
ograničava broj znakova stringa. Treći parametar je string u kojem se zapisuje format ispisa
nizom specifikatora oblika %x (kao kod printf() funkcije). Posljednji argument je pokazivač
strukture tm. Funkcija vraća broj znakova u stringu ili 0 ako nije moguće generirati formatirani
string. Specifikatori formata su
           %a     kratica od tri slova za ime dana u sedmici (eng. Sun, Mon, Tue,..)
           %A     puno ime dana u sedmici (eng...)
           %b     kratica od tri slova za ime mjeseca (eng. Jan, Feb, Mar,...)
           %B     puno ime mjeseca (eng....)
           %c     kompletni zapis vremena i datuma
           %d     dan u mjesecu (1..31)
           %H     sat u formatu (1..24)
           %I     sat u formatu (1..12)
           %j     dan u godini (1..365)
           %m     mjesec u godini (1..12)
           %M     minute
           %p     AM/PM (eng.) string koji označava jutro ili popodne
           %S     sekunde
           %U     broj za sedmicu u godini (1..52) - 1 određen prvom nedjeljom




                                                                                            184
%w       broj za dan u sedmici (0-nedjelja)
          %W       broj za sedmicu u godini (1..52) - 1 određen prvim ponedjeljkom
          %x       kompletni zapis datuma
          %X       kompletni zapis vremena
          %y       zadnje dvije znamenke godine
          %Y       godina u formatu s 4 znamenke
          %Z       ime vremenske zone (ako postoji )
          %%       znak %

Primjer: U programu vrijeme.c demonstrira se korištenje funkcija za datum i vrijeme.

      /* Datoteka: vrijeme.c */
      /* Prikazuje datum i vrijeme u tri formata*/

      #include <stdio.h>
      #include <time.h>

      int main()
      {
        time_t vrijeme = time(NULL);
        struct tm *ptr;
        char datum_str[20];

            /* ispisuje datum i vrijeme u standardnom formatu */
          puts(ctime(&vrijeme));

            /* ispisuje datum i vrijeme pomoću strftime funkcije */
          strftime(datum_str, sizeof(datum_str),
                   "%d.%m.%y %H:%Mn", localtime(&vrijeme));
          puts(datum_str);

            /* ispisuje datum i vrijeme u proizvoljnom formatu */
          ptr = localtime(&vrijeme);
          printf("%.2d.%.2d.%.2d %2d:%.2dn",
               ptr->tm_mday, ptr->tm_mon+1, ptr->tm_year +1900,
               ptr->tm_hour, ptr->tm_min);

          return 0;
      }


Dobije se ispis:

      Mon May 13 20:13:06 2002

      13.05.02        20:13

      13.05.2002         20:13


Za obradu vremena se koriste i sljedeće funkcije:
mktime
           time_t mktime(struct tm *tp)

Funkcija mktime() pretvara zapisa iz strukture tm u time_t format. Korisna je u tzv.
kalendarskim proračunima. Kada je potrebno dodati nekom datumu n dana, tada se može upisati




                                                                                       185
datum u tm strukturu, povećati član tm_mday za n, zatim pozivom mktime() se dobije
time_t vrijednost koja odgovara novom datumu.
difftime

             double difftime(time_t t1, time_t t2)

Funkcija difftime() vraća realnu vrijednost koja je jednaka razlici vremena t1 i t1 u
sekundama.
clock

             clock_t clock(void);

Funkcija clock() služi za preciznije mjerenje vremena nego je to moguće sa prethodnim
funkcijama. Ona vraća vrijednost procesorskog mjerača vremena, koji starta na početku
programa, u jedinicama koje su znatno manje od sekunde (nekoliko milisekundi). Koliko je tih
jedinica u jednoj sekundi određeno je konstantom CLOCKS_PER_SEC. To znači da izraz:

             (double)clock()/CLOCKS_PER_SEC

daje vrijednost koja je jednaka vremenu (u sekundama) od startanja programa.

Primjer: U programu brzina.c ispituje se vremenska rezolucija funkcije clock(), tj.
minimalno vrijeme koje se njome može mjeriti. Također, pomoću funkcije clock() mjeri se
koliko je potrebno vremena za izvršenje sinusne funkcije.

        /* Datoteka: brzina.c
         * Program određuje vremensku rezoluciju funkcije clock()
         * i mjeri brinu izvršenja sinus funkcije
         */

        #include <stdio.h>
        #include <math.h>
        #include <time.h>

        int main()
        {
          double start, stop;
          double n ;
          double rezolucija;

           start =(double)clock()/CLOCKS_PER_SEC;
           do {
                stop=(double)clock()/CLOCKS_PER_SEC;
           }
           while (stop == start);

           rezolucija = stop-start;

           printf("Rezolucija CLOCK-a je %g sekundin" , rezolucija);

           start =(double)clock()/CLOCKS_PER_SEC;
           stop = start + 10*rezolucija;

           do {
              n += 1.0;
              sin(n);




                                                                                        186
}
          while (stop > (double)clock()/CLOCKS_PER_SEC);

        printf("Funkcija sin se izvršava %g sekundin" ,
      10*rezolucija/n);

          return 0;
      }


Dobije se ispis:

      Rezolucija CLOCK-a je 0.015 sekundi
      Funkcija sin se izvrsava u 2.3543e-007 sekundi

Rezolucija funkcije clock() je 15 ms. U drugoj petlji, u kojoj se računa funkcija sin(),
odabrano je da se ona ponavlja za vrijeme koje je 10 puta veće od rezolucije clock()-a. Na taj
način mjerenje vremena izvršenja sinus funkcije vrši se s greškom koja je manja ili jednaka
10%.




                                                                                          187
14 Leksički pretprocesor


Naglasci:
   • leksičke direktive
   • makro-supstitucije
   • leksički string-operatori
   • direktive za uvjetno kompiliranje


    Pojam "pretprocesor'' se koristi za oznaku prve faze obrade izvornog koda. Kod nekih C
kompilatora pretprocesor je izveden kao zasebni program. Pretprocesor ne analizira sintaksu
zapisanog koda, već koristeći leksičke direktive, vrši leksičku obradu izvornog kôda na tri
načina:
    1. Umeće u izvorni kôd datoteke koje su navedene u direktivi #include
    2. Vrši supstituciju teksta prema direktivi #define
    3. Vrši selekciju kôda, koji će se kompilirati, pomoću direktiva: #if, #elif, #else i
        #endif



14.1 Direktiva #include
Koriste se dvije varijante direktive #include:

      #include <ime_datoteke>
      #include "ime_datoteke"

pomoću kojih se vrši umetanje neke datoteke u izvorni kôd, i to na mjestu gdje ja zapisana
direktiva. Ako je ime_datoteke zapisano unutar navodnih znakova, traženje datoteke počinje u
direktoriju gdje se nalazi izvorni kod, a ako nije tamo pronađena, ili ako je ime zapisano unutar
< >, traženje datoteke se vrši u direktoriju u kojem su smještene datoteke s deklaracijama
standardnih funkcija.
    Ime datoteke može sadržavati potpuni ili djelomični opis staze do datoteke. Zapis ovisi o
pravilima operativnog sustava. Primjerice, na Unix-u može biti oblik

      #include "/usr/src/include/classZ.h"
      #include "../include/classZ.h"

ili na Windowsima

      #include "C:srcincludeclassZ.h"
      #include "..includeclassZ.h"



14.2 Direktiva #define za makro-supstitucije
    Leksička supstitucija teksta se definira pomoću direktive #define na dva načina. Prvi
način zapisa, koji se vrši prema pravilu:




                                                                                             188
#define identifikator supstitucijski-tekst

je već korišten za definiranje simboličkih konstante ili dijelova izvornog koda, primjerice

      #define EPSILON 1.0e-6
      #define PRINT_LINE printf("----------n");
      #define LONG_SUBSTITION if(a>b){ printf("a>b"); } 
                                      else { prinf("a<=b"); }

Značenje ove direktive je da se u izvornom kodu, na mjestima gdje se je identifikator, umetne
supstitucijski-tekst. Supstitucijski tekst se zapisuje u jednoj ili više linija. Znak nove linije
označava kraj zapisa, ali ako je posljednji znak obrnuta kosa crta tada supstitucijskom tekstu
pripada i zapis u narednoj liniji.

    Direktiva za supstituciju teksta se često naziva makro supstitucija ili još kraće –
    makro.


     Drugi način zapisa direktive #define omogućuje da se uz identifikator navede jedan ili
više makro argumenata, prema pravilu:

      #define identifikator(argument, ... , argument) supstitucijski-tekst-s-argumentima

tako da supstitucijski tekst može biti različit za različita pozivanja makroa. Primjerice, makroi

      #define KVADRAT(x)    x*x
      #define POSTOTAK(x,y) x*100.0/y

se u izvornom kodu, ovisno o argumentima, supstituiraju na sljedeći način:

             poziv makroa                    rezultira kodom
             y   =   KVADRAT(z);             y    =   z*z;
             y   =   POSTOTAK(a,b);          y    =   a*100.0/b;
             y   =   KVADRAT(a+b);           y    =   a+b*a+b;
             y   =   POSTOTAK(a+b,c+d);      y    =   a+b*100.0/c+d;


    Dva zadnja primjera pokazuju najčešću grešku kod primjene makroa. Dobiveni kod ne
odgovara namjeri da se dobije kvadrat vrijednosti (a/b) jer se zbog prioriteta operatora izraz
(a+b*a+b) tretira kao (a+(b*a)+b), a ne kako je bila intencija programera, tj. kao ((a+b)*(a+b)).
Zbog toga se preporučuje da se argumenti makroa u supstitucijskom tekstu uvijek pišu u
zagradama, primjerice:

      #define KVADRAT(x)    ((x)*(x))
      #define POSTOTAK(x,y) ((x)*100.0/(y))

Tada se ekspanzija makroa uvijek vrši s istim značajem, primjerice

           poziv makroa                          rezultira kodom
           Y = KVADRAT(a+b)                      y = ((a+b)*(a+b))
           Y = POSTOTAK(a+b,c+d)                 y = ((a+b)*100.0/(c+d))




                                                                                               189
U definiciji makro naredbe prvi argument mora biti napisan neposredno iza zagrada,
     bez razmaka.

       Makro argumente u supstitucijskom tekstu treba uvijek pisati unutar zagrada. Time
       se osigurava da će se ekspanzija makroa izvršiti s istim efektom za bilo koji oblik
       argumenata. Također, poželjno je i da se cijeli supstitucijski tekst zapiše u
       zagradama.

       Poziv makroa izgleda kao poziv funkcije, ali nema značaj poziva funkcije (ne vrši
       se prijenos parametara funkcije), već se radi o supstituciji teksta prije procesa
       kompiliranja.

Simboli unutar supstitucijskog teksta mogu biti prethodno definirani makroi, primjerice

       #define JEDAN 1
       #define DVA   (JEDAN +1)
       #define TRI   (DVA +1)
       ....

Ekspanzija koju pretprocesor radi s izrazom JEDAN+DVA+TRI je 1+(1+1)+((1+1)+1), što
daje vrijednost 6. Ovaj proračun se provodi tijekom kompiliranja.


14.3 String operatori # i ##
     Ekspanzija makro argumenata se ne vrši ako je argument u supstitucijskom tekstu zapisan u
navodnim znakovima, odnosno unutar stringa. Ponekad je potrebno da se neki argument u
ekspanziji makroa pojavljuje literalno kao string. To se postiže tako da se u supstitucujskom
tekstu nepesredno ispred imena argumenta napiše znak # (tzv. string operator). U ekspanziji
makroa se tada taj argument pojavljuje zapisan unutar navodnih znakova. Primjerice, za
definiciju

       #define PRINT(x)           printf(#x "=%g", x)

vrši se ekspanzija PRINT(max) u printf("max" "=%g", max), a pošto se dva uzastopna
navodnika poništavaju, efekt je da se kompilira naredba printf("max=%g", max).

Zadatak: Provjerite ispis sljedećeg programa:

       /* program fun.c */
       #include <stdio.h>
       #include <math.h>

       #define PRINT_FUN(fun, x)              printf(#fun " = %gn", fun ((x)))

       int main()
       {
          PRINT_FUN(sin, 3);
          PRINT_FUN(cos, 3);
          return 0;
       }


Ispis je:




                                                                                             190
sin = 0.14112
       cos = -0.989992

     Operator ## djeluje na dva susjedna simbola na način da preprocesor izbaci taj operator i
sva prazna mjesta, pa se dobije jedan novi simbol s identifikatorom sastavljenim od ta dva
susjedna simbola. Primjerice, za definiciju

       #define NIZ(ime, tip, n)              tip    ime ## _array_ ## tip[N]

vrši se ekspanzija

            NIZ(x, int, 100)                         int x_array_int [100]
            NIZ(vektor, double, N)                   double vektor_array_double [N]



Zadatak: Provjerite ispis sljedećeg programa:

       /* program strop.c */
       #include <stdio.h>

       #define N 10
       #define PRINT(x)    printf(#x "=%dn", x)
       #define NIZ(ime, tip, n)    tip ime ## _array [n]

       int main()
       {
          int i, y=7;
          NIZ(x, int, N);

            for(i=0; i<N;i++) x_array[i] = 10;

            PRINT(y);
            PRINT(x_array[y]);
            return 0;
       }


Ispis je:

       y=7
       x_array[y]=10



14.4 Direktiva #undef
     Ponekad je potrebno redefinirati značaj nekog identifikatora. U tu svrhu koristi se direktiva
#undef:
       #undef identifikator

Ovom se direktivom poništava prethodni značaj identifikatora. Primjerice,

       #define SIMBOL_X VALUE_FOR_X
       ....
       ....     /* područje gdje se koristi SIMBOL_X */
       ....




                                                                                              191
#undef SIMBOL_X
      ....
      ....     /* ovdje        se ne može koristiti SIMBOL_X */
      ....
      #define SIMBOL_X         NEW_VALUE_FOR_X
      ....
      ....     /* ovdje        ponovo može koristiti SIMBOL_X */
      ....     /* ali s        nekim drugim značenjem */

Ako se pokuša redefinirati neki simbol bez prethodne #undef direktive, C kompilator će
dojaviti grešku poput:

      "Attempt to redefine macro SIMBOL_X"



14.5 Direktive za uvjetno kompiliranje
     Moguće je pomoću posebnih direktiva kontrolirati i sam proces pretprocesiranja. U tu svrhu
se koriste direktive: #if, #ifdef, #ifndef, #elif, #else i #endif.
     Iza direktiva #if i #elif (else-if) mogu se koristiti prosti izrazi koji rezultiraju
cjelobrojnom vrijednošću (ne smiju se koristiti sizeof i cast operatori te enum konstante).
Ako je vrijednost tih izraza različita od nule, tada se u proces kompiliranja uključuje tekst koji
slijedi iza ovih direktiva sve do slijedeće direktive (#elif, #else ili #endif).
     U sljedećem primjeru pokazano je kako se pomoću ovih direktiva određuje koja će datoteka
biti uključena u proces kompiliranja, ovisno o tipu operativnog sustava:

          #if SISTEM == LINUX
              #define HDR "linx.h"
          #elif SISTEM == MSDOS
              #define HDR "msdos.h"
          #elif SISTEM == WIN32
              #define HDR "win32.h"
          #else
              #define HDR "default.h"
          #endif

          #include HDR

Pretpostavljeno je da je prethodno definirana vrijednost simbola: LINUX, WIN32, MSDOS i
SISTEM.

Direktiva #elif ima značenje else if.

Iza #if i #elif može se koristiti izraz

             defined(identifikator)

koji daje 1 ako je identifikator prethodno registriran kao neki simbol (uključujući i makro
simbole), inače je 0. Primjerice, da bi se osiguralo da se sadržaj neke datoteke može uključiti u
izvorni kod samo jedan put, često se koriste direktive po sljedećem obrascu :


          /* datoteka header.h */
          #if !defined(HEADER_H)
          #define HEADER_H




                                                                                              192
/* ovdje se zapisuje sadržaj od header.h */

          #endif

Pri prvom uključenju ove datoteke definira se simbol HEADER_H i sadržaj datoteke se uljučuje
u izvorni kod. Ako se ova datoteka uključi po drugi put, neće se koristiti njen sadržaj jer tada
postoji definiran simbol HEADER_H.
    Direktive #ifdef i #ifndef su specijalizirane za ispitivanje da li je neki identifikator
definiran (ili nije definiran). Prethodni se primjer može zapisati i u obliku:

          #ifndef _HEADER_H
          #define _HEADER_H

              /* ovdje se zapisuje sadržaj od header.h */

          #endif


    #ifdef identifikator je ekvivalentno #if defined(identifikator).
    #ifndef identifikator je ekvivalentno #if !defined(identifikator).

Iza svake #if, #ifdef ili #ifndef direktive mora biti #endif direktiva.




                                                                                            193
15 Rad s datotekama i tokovima


Naglasci:
   • ulazno-izlazni tokovi
   • standardni tokovi
   • tekstualne i binarne datoteke
   • otvaranje i zatvaranje datotečnih tokova
   • formatirani tokovi
   • binarni tokovi
   • sekvencijalni i proizvoljni pristup datotekama
   • kopiranje, brisanje i promjena imena datoteka


15.1 Ulazno-izlazni tokovi
     U standardnoj je biblioteci implementiran niz funkcija koje na jedinstven način tretiraju sve
ulazno izlazne operacije: unos s tipkovnice, ispis na ekran te čitanje i pisanje informacija koje se
pohranjuju na magnetskim i optičkim medijima. Komuniciranje s uređajima koji obavljaju ove
operacije vrši se sekvencijalno bajt po bajt, a programski mehanizam kojim se vrši ovakvi
prijenos informacije naziva se tok (eng. stream).
     U jednom se programu može raditi s više tokova. Svakom toku se pridjeljuje jedna
struktura podataka imena FILE, koja je definirana u <stdio.h>. Temeljna namjena te
strukture je da služi kao memorijski ulazno/izlazni međuspremnik (eng. I/O buffer) pri prijenosu
podataka. Važno je znati da se pri izlaznim operacijama podaci ne šalju direktno vanjskim
uređajima, već se najprije upisuju o ovaj međuspremnik. Kada se on ispuni, tada se sadržaj
cijelog međuspremnika šalje vanjskom uređaju. Na ovaj način se smanjuje broj pristupa disku i
znatno ubrzava rad s datotekama. Sličnu namjenu ovaj međuspremnik ima i pri ulaznim
operacijama.
Tokovi se dijele u četiri grupe:
   • standardni ulaz (vrši dobavu znakove tipkovnice)
   • standardni izlaz (vrši ispis na ekran)
   • standardna dojava greške (obično se vrši ispis na ekran)
   • datotečni tok (vrši čitanje ili pisanje podataka u datoteku)
    Standardni ulaz, standardni izlaz i standardni tok dojave greške se samoinicijaliziraju pri
pokretanju programa, a njihov pokazivač na strukturu FILE je u globalnim varijablama:


      FILE *stdin;              /* pokazivač toka standardnog ulaza */
      FILE *stdout;             /* pokazivač toka standardnog izlaza */
      FILE *stderr;             /* pokazivač toka dojave greške */


Ovi pokazivači su deklarirani u datoteci <stdio.h>. Iniciranje pokazivača datotečnih tokova
mora obaviti sam programer. Kako se to radi bit će objašnjeno kasnije.




                                                                                                194
Programer ne mora voditi računa o detaljima kako se izvršava ulazno/izlazni prijenos
podataka. Ono što on mora znati je pokazivač toka, s kojim se komunicira, i funkcije pomoću
kojih se ta komunikacija realizira. Pokazivača toka (FILE *) se mora navesti kao argument
svake funkcije s kojom se vrše ulazno/izlazne operacije. Primjerice, za formatirani ispis
podataka koristi se funkcija fprintf kojoj prototip glasi:

      int fprintf(FILE *pTok, const char *format, ...);

gdje je pTok pokazivač toka, a tri točkice označavaju da se funkcija može koristiti s
promjenjljivim brojem argumenata. Formatirani ispis se vrši prema obrascu koji se zapisuje u
stringu format, na isti način kako se zapisuje format ispisa u printf()funkciji. Primjerice,
za ispis stringa "Hello World" na standardnom izlazu može se koristiti naredba:

      fprintf(stdout, "Hello World");

koja ima isti učinak kao naredba:

      printf("Hello World");

Zapravo, printf() funkcija je interno realizirana kao fprintf() funkcija kojoj je pokazivač
toka jednak stdout.


15.2 Binarne i tekstualne datoteke
     Informacije se u datoteke zapisuju u kodiranom obliku. Temeljna su dva načina kodiranog
zapisa: binarni i tekstualni (ili formatirani). Kaže se da je zapis izvršen u binarnu datoteku kada
se informacije na disk zapisuju u istom binarnom obliku kako su kodirane u memoriji računala.
U tekstualne datoteke se zapis vrši formatirano pomoću slijeda ASCII znakova, na isti način
kako se vrši tekstualni ispis na video monitoru, primjerice printf() funkcijom. Sadržaj
tekstualnih datoteka se može pregledati bilo kojim editorom teksta, dok sadržaj binarnih
datoteka obično može razumjeti samo program koji ih je formirao.
     Treba imati na umu da kada se u tekstualnu datoteku formatirano upisuje C-string, tada se
ne zapisuje završni znak '0'. Uobičajeno se zapis u tekstualne datoteke vrši u redovima teksta,
na način da se za oznaku kraja linije koristi znak 'n'. Takovi zapis zovemo linija. Poželjno je da
se ne unose linije koje sadrže više od 256 znakova, jer se time osigurava da će tekstualna
datoteka biti ispravno očitana s gotovo svim programima koji manipuliraju s tekstualnim
zapisom. Potrebno je napomenuti da se na MS-DOS računalima tekstualne datoteke zapisuju
tako da se svaki znak 'n' (CR) pretvara u dva znaka "rn" (CR-LF), a kada se vrši očitavanje s
diska tada se ""rn" prevodi u jedan znak 'n'. Ova se operacija obavlja na razini operativnog
sustava. Programer o njoj ne mora voditi računa ukoliko koristi funkcije koje su u C jeziku
predviđene za rad s tekstualnim datotekama.. Iznimka je slučaj kada se datoteka, koja je
zapisana u tekst modu, tretira kao binarna datoteka. Na UNIX sustavima se ne vrši ova
pretvorba.
     Prema ANSI/ISO standardu u datoteka <stdio.h> sadrži prototipove funkcija za rad s
datotekama. Neke od ovih funkcija predviđene su za rad s binarnim datotekama, a neke za rad s
tekstualnim datotekama. Prije nego se opiše te funkcije najprije će biti pokazano kako se
pristupa datotekama.


15.3 Pristup datotekama
    Svaka datoteka ima ime. Ime datoteke je spremljeno na disku kao tekstualni zapis u
posebnoj sekciji kataloga diska. Uz ime su zabilježeni i podaci o datoteci: vrijeme kada je




                                                                                               195
spremljena, broj bajta koje datoteka zauzima, mjesto na disku gdje je spremljen sadržaj datoteke
i atributi pristupa datoteci (read, write, hidden).
      Da bi se moglo koristiti neku datoteku potrebno je od operativnog sustava zatražiti dozvolu
pristupa toj datoteci. Taj proces se zove otvaranje datoteke. Isto tako se za kreiranje nove
datoteke mora zatražiti dozvola od operativnog sustava. Tu funkciju obavlja standardna funkcija
fopen(). Ona pored komunikacije s operativnim sustavom kreira datotečni tok koji sadrži
memorijski međuspremnik za efikasno čitanje ili spremanje podataka na disk.
Prototip funkcije fopen() je:


              FILE *fopen(const char *staza, const char *mod);


staza je string koji sadrži ime datoteke, s potpunim opisom staze direktorija, primjerice string :
          char *filename = "c:datalist.txt"; bi koristili na Windows
          računalima za otvoriti datoteku imena list.txt koja se nalazi na disku c: u
          direktoriju imena data. Napomenimo da se u imenu datoteke ne smiju koristiti
          znakovi : /, , :, *, ?, ", <, > i |. Ako se zapiše samo ime datoteke, podrazumijeva se da
          se datoteka nalazi u tekućem direktoriju.

mod je string koji se opisuje način otvaranja datoteke. Zapisuje s jednim ili više znakova: r, w,
         a i +, čije značenje je dano u tablici 15.1.


     mod                                                 značenje
   "r"   Otvori datoteku za čitanje (eng. read). Ako datoteka ne postoji fopen() vraća NULL.
             Otvori datoteku za pisanje (eng. write). Ako ne postoji datoteka zadanog imena kreira se nova datoteka.
   "w"       Ako postoji datoteka zadanog imena njen se sadržaj briše i kreira prazna datoteka (novi podaci će se
             zapisivati počevši od početka datoteke).
             Otvori datoteku za dopunu sadržaja (eng. append) . Ako ne postoji datoteka zadanog imena kreira se
   "a"
             nova datoteka. Ako postoji datoteka zadanog imena, novi podaci će se dodavati na kraj datoteke.
             Otvori datoteku za čitanje i pisanje . Ako ne postoji datoteka zadanog imena kreira se nova datoteka.
   "r+"      Ako postoji datoteka zadanog imena njen se sadržaj briše i kreira prazna datoteka (novi podaci će se
             zapisivati od početka datoteke)..
   "w+"      Isto kao r+
             Otvori datoteku za čitanje i dopunu . Ako ne postoji datoteka zadanog imena kreira se nova datoteka.
   "a+"
             Ako postoji datoteka zadanog imena u nju se vrši upis na kraju datoteke..
             Ako se iza slova w, r ili a još zapiše slovo 'b' to označava da se datoteku treba otvoriti u binarnom
   "b"
             modu, inače se datoteka otvara u tekstualnom modu.

                    Tablica 15.1. Značaj znakova u stringu mod funkcije fopen()


    Funkcija fopen() vraća pokazivač toka (FILE *). U slučaju greške vrijednost toga
pokazivača je NULL. Najčešći uzrok greške pri otvaranju datoteke je:
    •     Neispravan zapis imena datoteke.
    •     Neispravan zapis direktorija ili oznake diska.
    •     Ne postoji direktorij zadana imena
    •     Zahtjev da se otvori nepostojeća datoteka u modu čitanja – "r".




                                                                                                     196
Dobra je praksa da se uvijek provjeri da li je datoteka otvorena bez greške. Primjerice, za
otvoriti datoteku imena "hello.txt" u modu pisanja, pogodan je slijed iskaza

      FILE * fp;
      fp = fopen("hello.txt", "w");
      if( fp == NULL)
         printf("Greska pri otvaranju datoteke");

Kada je datoteka otvorena, koristi je se kao tok. Primjerice, iskazima

      fprintf( fp, "Hello World!n");
      fprintf( fp, "%sn" "Hello World drugi put!");

u prethodno otvorenoj datoteci "hello.txt" biti će zapisane dvije linije teksta:

      Hello World!
      Hello World drugi put;

      Kada se završi rad s datotekom, treba zatvoriti datoteku. Što je to zatvaranje datoteke?
Zatvaranje datoteke je postupak koji je nužno provesti kako bi svi podaci iz računala, koji se
jednim dijelom nalaze u međuspremniku strukture FILE, bili spremljeni na disk, te da bi se
ispravno zapisao podatak o veličini datoteke. Zatvaranje datoteke se vrši funkcijom fclose()
čiji je prototip:

      int fclose(FILE *fp);

Funkcija fclose() prima argument koji je pokazivač toka prethodno otvorene datoteke, a
vraća vrijednost nula ako je proces zatvaranja uspješan ili EOF ako pri zatvaranju nastane
pogreška.
Ukoliko se ne zatvori datoteka pomoću ove funkcije, ona će biti prisilno zatvorena po završetku
programa. Ipak se preporučuje da se uvijek zatvori datoteka čim se s njome završi rad, jer se
time štede resursi operativnog sustava i osigurava od mogućeg gubitka podataka (primjerice, pri
resetiranju računala dok je program aktivan, pri nestanku električnog napajanja, ili ako nastupi
blokada programa).
    U nekim će programima biti potrebno da datoteke budu otvorene cijelo vrijeme. U tom
slučaju je zgodno koristiti funkciju fflush() kojom se forsira pražnjenje datotečnog
međuspremnika i ažurira stanje datoteke na disku, bez zatvaranja datoteke. Prototip funkcije
fflush() je

      int fflush(FILE *fp);

funkcija prima argument koji je pokazivač toka prethodno otvorene datoteke, a vraća vrijednost
nula ako je proces pražnjenja međuspremnika uspješan ili EOF ako pri zapisu podataka iz
međuspremnika nastane greška.
Funkcija fflush(stdin) s također često koristi za odstranjivanje viška znakova iz
standardnog ulaza.


15.4 Formatirano pisanje podataka u datoteku
    Formatirano pisanje se vrši pomoći fprintf() funkcije.                 Pokažimo to sljedećim
programom:

      /* Datoteka: txtfile-write.c */
      /* Demonstrira se upis u tekstualnu datotke */




                                                                                            197
/* 5 realnih brojeva, koje unosi korisnik */

      #include <stdlib.h>
      #include <stdio.h>

      int    main()
      {
             FILE *fp;
             float data[5];
             int i;
             char filename[20];

             puts("Otipkaj 5 realnih brojeva");

             for (i = 0; i < 5; i++)
                 scanf("%f", &data[i]);

             /* Dobavi ime datoteke, ali prethodno */
             /* isprazni moguci višak znakova iz međuspremnika ulaza */

             fflush(stdin);

             puts("Otipkaj ime datoteka:");
             gets(filename);

             if ( (fp = fopen(filename, "w")) == NULL)
             {
                 fprintf(stderr, "Greska pri otvaranju datoteke %s.",
                                  filename);
                 exit(1);
             }

             /*Ispisi vrijednosti u datoteku i na standardni izlaz */

             for (i = 0; i < 5; i++)
             {
                 fprintf(fp, "ndata[%d] = %f", i, data[i]);
                 fprintf(stdout, "ndata[%d] = %f", i, data[i]);
             }
             fclose(fp);
             printf("nSada procitaj datoteku: %s, nekim editorom",
                    filename);
             return(0);
      }

Izlaz iz programa je:

      Otipkaj 5 realnih brojeva
      3.14159
      9.99
      1.50
      3.
      1000.01
      Otipkaj ime datoteke
      brojevi.txt
      data[0] = 3.141590
      data[1] = 9.990000
      data[2] = 1.500000
      data[3] = 3.000000




                                                                          198
data[4] = 1000.010
       Sada procitaj datoteku: brojevi.txt, nekim editorom



15.5 Formatirano čitanje podataka iz datoteke
    Za formatirano čitanje sadržaja datoteke koristi se fscanf() funkcija, koja je poopćeni
oblik scanf() funkcije za dobavu podataka iz ulaznih tokova. Prototip fscanf() funkcije je:

       int fscanf(FILE *fp, const char *fmt, ...);

Parametar fp je pokazivač ulaznog toka, koji može biti stdout ili datotečni tok koji se dobije
kada se datoteka otvori s atributom "r", "r+" ili "w+". String fmt služi za specifikaciju formata
po kojem se učitava vrijednost varijabli, čije adrese se koriste kao argumenti funkcije. Tri točke
označavaju proizvoljan broj argumenata, uz uvjet da svakom argumentu mora pripadati po jedan
specifikator formata u stringu fmt.

Primjer: Program txtfile-read.c čita sadržaj datoteke "input.txt", a zatim ga ispisuje na
standardni izlaz. Prethodno je potrebno nekim editorom teksta formirati datoteka imena
"input.txt", sa sljedećim sadržajem:
            6.8 5.89
            67
            1.099010
            67.001


       /* Datoteka: txtfile-read.c */
       /* Demonstrira se upis u tekstualnu datotke */
       /* 5 realnih brojeva, koje unosu korisnik */

        #include <stdio.h>

        int main()
        {
          float f1, f2, f3, f4, f5;
          FILE *fp;

             if ( (fp = fopen("INPUT.TXT", "r")) == NULL)
             {
                   fprintf(stderr, "Greska pri otvaranju datoteke.n");
                   exit(1);
             }

             fscanf(fp, "%f %f %f %f %f", &f1, &f2, &f3, &f4, &f5);
             fprintf(stdout, "Vrijednosti u datoteci su:n");
             fprintf(stdout, "%f, %f, %f, %f, %fn.", f1, f2, f3, f4, f5);

             fclose(fp);
             return(0);
        }


Ispis je:
             Vrijednosti u datoteci su:
             6.800000, 5.890000, 67.000000, 1.099010, 67.000999




                                                                                              199
Uočite da je posljednji broj u datoteci 67.0001 pročitan kao 67.000999. Očito je da se u
pretvorbi koja se vrši pri formatiranom unosu gubi na točnosti.
     Funkcija fscanf() je pogodna za formatirani unos brojeva, ali nije pogodna za unos
stringova i znakova. To je pokazano u prethodnim poglavljima, kada je analizirana upotreba
funkcije scanf().
Dalje će biti opisane funkcije koje omogućuju dobavu znakova i linija teksta.


15.6 Znakovni ulaz/izlaz
      U pristupu datotekama pojam znakovni ulaz/izlaz se koristi za transfer pojedinačnog znaka
ili jedne linije (linija je nula ili više znakova zaključenih s znakom nove linije). Znakovni
ulaz/izlaz se uglavnom koristi s tekstualnim datotekama.
Znakovni ulaz – getc, ungetc, fgetc, fgets
    Za dobavu znakova koriste se funkcije getc() i fgetc(), a za dobavu linije koristi se
funkcija fgets(). Deklarirani su u <stdio.h>. Prototip funkcija za dobavu znaka je:

      int getc(FILE *fp);
      int fgetc(FILE *fp);

Obje funkcije obavljaju s tokom fp iste operaciju kao funkcija fgetchar() sa standardnim
ulazom, na način da vraćaju trenutni ulazni znak, ili EOF ako je greška ili kraj datoteke.

Prototip funkcije za dobavu linije je:

      char *fgets(char *str, int n, FILE *fp);

Parametar str je pokazivač gdje će biti prihvaćen string, n je maksimalni broj znakova koji
string prihvaća (uključujući i nul znak), a fp je pokazivač toka. Znakovi se uzimaju iz toka sve
do pojave znaka nove linije ili dok se ne prenese n-1 znakova (fgets() postavlja n-ti znak
'0'). Ako je transfer uspješan fgets() vraća pokazivač str, a ako je transfer neuspješan ili
ako je detektiran EOF vraća NULL. Ukoliko greška ili EOF nastupi nakon transfera prvog znaka,
str više nije pogodan za upotrebu jer nije ispravno zaključen s nul znaka. Obično se ova
funkcija koristi za dobavu linije teksta. Preporuka je da se uvijek alocira dovoljno memorije za
string, kako be se učitala čitava linija.
    Ponekad je pri dobavi znaka potrebno vratiti taj znak u tok. U tu svrhu se može koristiti
funkcija

      int ungetc(int c , FILE *fp);

ungetc() je korisna pri leksičkoj analizi. Primjerice, ako se iz toka dobavlja niz znamenki može
ih se pridijeliti nekom broju sve dok se ne pojavi znak koji nije znamenka. Tada je zgodno
vratiti taj znak u tok kako bi bio na raspolaganju u nekoj drugoj operaciji. Primjerice, sljedeći
iskazi rezultiraju dobavom cijelog broja iz tekstualnog ulaznog toka:

      int n = 0;
      int c;

      while((c = getc(fp)) >= '0' && c <= '9')
             n = 10 * n + (c - '0');

      ungetc(c, fp);         /* nije znamenka – vrati znak u ulazni tok */




                                                                                             200
printf("%d", n);


Ovim se mehanizmom ne smije vraćati više znakova u tok. Garantira se uspješno vraćanje samo
jednog znaka. Ukoliko se ne može izvršiti ova operacija funkcija ungetc() vraća EOF, a ako je
operacija uspješna funkcija ungetc() vraća znak c.
Znakovni izlaz – putc, fputs
     Za znakovni izlaz se mogu koristiti dvije funkcije; putc() i fputs(). Funkcija putc()
je ekvivalentna funkciju putchar() kada se komunicira sa standardnim izlazom. Prototip te
funkcije je:

      int putc(int ch, FILE *fp);

Funkcija koristi dva argumenta; ch je znak koji se zapisuje, a fp je pokazivač izlaznog toka.
Iako je ch deklariran kao int uzima se samo donji bajt. Funkcija vraća znak koji je zapisan u
tok ili EOF ako nastupi pogreška.

Za zapis stringa u izlazni tok koristi se funkcija fputs(), kojoj prototip glasi:

      int fputs(char *str, FILE *fp);

Koriste se dva argumenta: str je pokazivač na string, a fp je pokazivač izlaznog toka. Ova
funkcija zapisuje string bez zaključnog nul znaka. Ako je transfer uspješan vraća pozitivnu
vrijednost, a ako je transfer neuspješan vraća EOF.
Za ispis stringa na standardni izlaz do sada je korištena funkcija

      int puts(char *str);

koja je uvije dodavala znak nove linije. To je različito od djelovanja fputs(str, stdout),
koja ne dodaje znak nove linije.
Znakovni prijenos kod binarnih datoteka
    Za direktan upis u binarne datoteke, bez ikakvog posrednog formatiranja, također se mogu
koristiti funkcije putc() i fgetc() ali se tada ne vrijedi da se vrijednost EOF (-1) koristi za
dojavu greške, jer je kod binarnih datoteka to regularni simbol.
Za detektiranje kraja datoteke predviđena je posebna funkcija

             int feof(FILE *fp);

koja vraća vrijednost različitu od nule ako je dosegnut kraj datoteke. Nakon toga više nije
moguće čitanje iz datoteke.
Primjer: kopiranje datoteka
Kopiranje sadržaja jedne datoteke u drugu datoteku provodi se prema sljedećem algoritmu:
   1. Otvori izvornu datoteku u binarnom modu (tj. datoteku iz koje se kopira)
   2. Otvori odredišnu datoteku u binarnom modu (tj. datoteku u koju se kopira)
   3. Učitaj znak iz izvorne datoteke
   4. Ako funkcija feof() vrati vrijednost različitu od nule (nije dosegnut kraj datoteke)
       tada upiši znak u odredišnu datoteku i vrati se na korak 3.
   5. Ako funkcija feof() vrati 0 to znači da je dosegnut kraj datoteke. U tom slučaju
       zatvori obje datoteke.

Ovaj algoritam je implementiran u funkciji




                                                                                             201
int kopiraj_datoteke( char *ime_izvora, char *ime_odredista );

Funkcija prima dva argumenta koji označavaju imena izvorne i odredišne datoteke. Vraća 0 ako
nije izvršeno kopiranje ili 1 ako je kopiranje uspješno. Implementacija funkcije je:

      int kopiraj_datoteke( char *ime_izvora, char *ime_odredista )
      {
           FILE *fpi, *fpo;

             /* Otvori datoteku za čitanje u binarnom modu */
             if (fpi = fopen( ime_izvora, "rb" ) == NULL )
                 return 0;

             /* Otvori datoteku za pisanje u binarnom modu */
             if ( fpo = fopen( ime_odredista, "wb" ) == NULL                 )
             {
                 fclose ( fpi );
                 return 0;
             }

             /* Učitaj 1 bajt iz fpi. Ako nije eof, upiši bajt u fpo */

             while (1)
             {
                 char c = fgetc( fpi );
                 if ( !feof( fpi ) )
                     fputc( c, fpo );
                 else
                     break;
             }
             fclose ( fpi);
             fclose ( fpo);
             return 1;
       }


Testiranje ove funkcije se provodi programom kopiraj.c u kojem se ime izvorne i odredišne
datoteke dobavlja s komandne linije.

      /* Datoteka: kopiraj.c
       * kopira datoteku ime1 u datoteku ime2, komandom
       * c:> kopiraj ime1 ime2
       */
      #include <stdio.h>

      int kopiraj_datoteke( char *ime_izvora, char *ime_odredista );

      int    main(int argc, char **argv)
      {
          char *ime1, *ime2;
      if (argc <3) /* moraju biti dva argumenta komandne linije */
      {
         printf("Uputstvo: kopiraj ime1 ime2n");
         return 1;
      }
          ime1 = argv[1];
          ime1 = argv[2];
          if( kopiraj_datoteke(ime1, ime1 ) )




                                                                                        202
puts("Kopiranje zavrseno uspjesnon");
              else
                  puts("Kopiranje neuspjesno!n");
              return(0);
          }




         U radu s binarnim datotekama za detekciju kraja datoteke isključivo se koristi
         funkcija feof(), dok se u radu s tekstualnim datotekama kraj datoteke može
         detektirati i kada funkcije vrate EOF (obično je to vrijednost -1).


15.7 Direktni ulaz/izlaz za memorijske objekte
    Najefikasniji i najbrži način pohrane bilo kojeg složenog memorijskog objekta je da se
zapisuje u binarne datoteke. Na taj način datoteka sadrži sliku memorijskog objekta, pa se isti
može na najbrži mogući način prenijeti iz datoteke u memoriju.
Za ovakvi tip ulazno izlaznih operacija predviđene su dvije funkcije: fwrite() i fread().
fwrite
Funkcija fwrite() služi za zapis u datoteku proizvoljnog broja bajta s neke memorijske
lokacije. Prototip funkcije je:

         int fwrite(void *buf, int size, int count, FILE *fp);

Argument buf je pokazivač na memorijsku lokaciju s koje se podaci zapisuju u tok fp.
Argument size označava veličinu u bajtima pojedinog elementa koji se upisuje, a argument
count označava ukupni broj takovih elemenata koji se zapisuju. Primjerice, ako je potrebno
zapisati 100 elemenata niza cijelih brojeva, tada je size jednak 4 (sizeof int), a count je
jednako 100. Dakle, ukupno se zapisuje 400 bajta.
Funkcija vraća vrijednost koja je jednaka broju elemenata koji su uspješno zapisani. Ako je ta
vrijednost različita od count, to znači da je nastala pogreška. Uobičajeno je da se pri svakom
transferu vrši ispitivanje ispravnog transfera iskazom:

         if( (fwrite(buf, size, count, fp)) != count)
             fprintf(stderr, "Error writing to file.");

Primjeri korištenja funkcije fwrite():

1. zapis skalarne varijable x, koja je tipa float, vrši se sa:

         fwrite(&x, sizeof(float), 1, fp);

2. zapis niza od 50 elemenata strukturnog tipa, primjerice

         struct tocka {int x, int y;} niz[50];

vrši se iskazom:

         fwrite(niz, sizeof(struct tocka), 50, fp);

         ili fwrite(niz, sizeof(niz), 1, fp);

U drugom slučaju čitav se niz tretira kao jedan element, a učinak je isti kao u prvom iskazu.




                                                                                                203
fread
    Funkcija fread() služi za učitavanje proizvoljnog broja bajta na neku memorijsku
lokaciju. Prototip funkcije je:

        int fread(void *buf, int size, int count, FILE *fp);

Argument buf je pokazivač na memorijsku lokaciju u koju se upisuju podaci iz toka fp.
Argument size označava veličinu u bajtima pojedinog elementa koji se učitava, a argument
count označava ukupni broj takovih elemenata koji se učitavaju u memoriju.
Funkcija vraća vrijednost koja je jednaka broju elemenata koji su uspješno učitani. Ako je ta
vrijednost različita od count, to znači da je nastala greška ili je dosegnut kraj datoteke.

Primjer: U programu binio.c niz od 10 cijelih brojeva prvo se zapisuje u binarnu datoteku
imena "podaci", a zatim se taj niz ponovo učitava u binarnom obliku.
         /*Datoteka: binio.c */
         #include <stdlib.h>
         #include <stdio.h>

         #define SIZE 10

         void prekini(char *s)
         {
              fprintf(stderr, s);
              exit(1);
         }

         int main()
         {
            int i, niz1[SIZE], niz2[SIZE];
            FILE *fp;

            for (i = 0; i < SIZE; i++)
                niz1[i] = 7 * i;

            /* otvori datoteku za pisanje u binarnom modu*/
            if ( (fp = fopen("podaci", "wb")) == NULL)
                 prekini("Greška pri otvaranju datoteke");

            /* Spremi niz1 u datoteku */
            if (fwrite(niz1, sizeof(int), SIZE, fp) != SIZE)
                 prekini("Greška pri pisanju u datoteku");

            /* Zatvori datoteku */
            fclose(fp);

            /* Ponovo otvori datoteku za čitanje*/
            if ( (fp = fopen("podaci", "rb")) == NULL)
                  prekini("Greška pri otvaranju datoteke");

            /* Čitaj iz datoteke u niz2 */
            if (fread(niz2, sizeof(int), SIZE, fp) != SIZE)
                  prekini("Greška pri citanju datoteke");

            fclose(fp);

            /* Sada ispisi oba niza i provjeri da li su jednaka*/




                                                                                         204
for (i = 0; i < SIZE; i++)
                printf("%dt%dn", niz1[i], niz2[i]);
            return(0);
        }

Dobije se ispis:

      0      0
      7      7
      14     14
      21     21
      28     28
      35     35
      42     42
      49     49
      56     56
      63     63


      Prednost zapisa u binarnom modu u odnosu na formatirani zapis nije samu u
      efikasnom korištenju resursa i brzini rada. U binarnom modu nema gubitka
      informacije (smanjenja točnosti numeričkih zapisa) koje se javljaju u formatiranom
      zapisu.

      Nije preporučljivo direktno zapisivanje u datoteku struktura koji sadrže
      pokazivačke članove, jer pri ponovnom učitavanju takovih struktura pokazivači
      neće sadržavati ispravne adrese.


Funkcije rewind() i ftell()
    U prethodnom programu bilo je potrebno dva puta otvoriti i zatvoriti datoteku istog imena.
U oba slučaja se čitanje/pisanje vršilo od početka datoteke. U slučaju kada se s nekom
datotekom vrši čitanje i pisanje ona se može otvoriti u modu "w+b". Da bi ovi procesi startali od
početka datoteke tada je potrebno koristiti funkciju rewind() kojom se mjesto pristupa
datoteci postavlja na početak datoteke. Prototip ove funkcije glasi:

      void rewind(FILE *fp);

Uvijek se može odrediti mjesto na kojem će biti izvršen slijedeći pristup datoteci. To se postiže
funkcijom ftell() kojoj je prototip:

      long ftell(FILE *fp);

Funkcija ftell() vraća cjelobrojnu vrijednost koja odgovara poziciji (u bajtima) slijedećeg
pristupa datoteci. U slučaju pogreške funkcija vraća vrijednost -1L.

Primjer: Program binio1.c ima isti učinak kao i program binio.c, ali se koristi mod "w+b"
i funkcija rewind(). Također, demonstrira se upotreba funkcije ftell().

      /*Datoteka: binio1.c */
      #include <stdlib.h>
      #include <stdio.h>

      #define SIZE 10




                                                                                             205
void prekini(char *s)
        {
           fprintf(stderr, s);
             exit(1);
        }

        int main()
        {
           int i, niz1[SIZE], niz2[SIZE];
           FILE *fp;

             for (i = 0; i < SIZE; i++)
                 niz1[i] = 7 * i;
            /* Otvori datoteku za čitanje i pisanje*/
             if ( (fp = fopen("podaci", "w+b")) == NULL)
                  prekini("Greška pri otvaranju datoteke");

            /* Spremi niz1 u datoteku */
            if (fwrite(niz1, sizeof(int), SIZE, fp) != SIZE)
                 prekini("Greška pri pisanju u datoteku");
            /* pomoću ftell() izvijesti o broju bajta u datoteci */
            printf("U datoteci je zapisano %d bajtan", ftell(fp));

            /* vrati poziciju pristupa datoteci na pocetak */
            rewind(fp);

            /* Čitaj iz datoteke u niz2 */
            if (fread(niz2, sizeof(int), SIZE, fp) != SIZE)
                  prekini("Greška pri citanju datoteke");

            fclose(fp);

            /* Sada ispisi oba niza i provjeri da li su jednaka*/
            for (i = 0; i < SIZE; i++)
                printf("%dt%dn", niz1[i], niz2[i]);
            return(0);
        }



15.8 Sekvencijani i proizvoljni pristup datotekama
     Sekvencijalni pristup datoteci označava operacije s datotekama u kojima se čitanje ili
pisanje uvijek vrši na kraju datoteke. Proizvoljni ili slučajni pristup datoteci (random access)
označava operacije s datotekama u kojima se čitanje ili pisanje može usmjeriti na proizvoljno
mjesto u datoteci.
     Svakoj se otvorenoj datoteci dodjeljuje jedan pozicijski indikator koji označava poziciju (u
bajtima) od početka datoteke na kojoj će biti izvršeno čitanje ili pisanje. U svim dosadašnjim
primjerima korišten je sekvencijalni pristup datoteci. U tom slučaju, nakon otvaranja datoteke
pozicijski indikator ima vrijednost 0, a kada se datoteka zatvori pozicijski indikator ima
vrijednost koja je jednaka broju bajta koji su zapisani u datoteci, ukoliko je posljednja operacija
bila pisanje u datoteku.
     Proizvoljni pristup datoteci ima smisla samo kod binarnih datoteka, kod kojih se čitanje i
pisanje vrši kontrolirano bajt po bajt. On se ostvaruje pomoću funkcije fseek().
fseek
Funkcija fseek() služi za proizvoljno postavljanje pozicijskog indikatora datoteke.
Deklarirana je u datoteci <stdio.h> prototipom:




                                                                                               206
int fseek(FILE *fp, long pomak, int seek_start);

Prvi argument je pokazivač toka. Drugi argument određuje pomak pozicijskog indikatora, a
treći argument određuje od koje startne pozicije se vrši pomak pozicijskog indikatora. Ova
startna pozicija se određuje pomoću tri konstante koje su definirane u <stdio.h>, a njihov
značaj je opisan u tablici:

    Konstanta        Vrijednost Značaj vrijednosti seek_start
   SEEK_SET              0      Pomak se vrši od početka datoteke prema kraju datoteke.

   SEEK_CUR              1      Pomak se vrši od trenutne pozicije prema kraju datoteke.

   SEEK_END              2      Pomak se vrši od kraja datoteke prema početku datoteke.


Funkcija fseek() vraća vrijednost 0 ako je operacija uspješna, a ako je operacija neuspješna
vraća vrijednost različitu od nule.
Uočite:
      fseek(fp,0, SEEK_SET)) je jednako rewind(fp).

Veličinu datoteke u bajtima se može dobiti naredbama:

      fseek(fp, 0, SEEK_END);
      size = ftell(fp);

Primjer: U programu seek.c generira se datoteka "random.dat" s 50 slučajnih cijelih brojeva.
Zatim se po proizvoljnom redoslijedu čita vrijednosti iz te datoteke. Redoslijed bira korisnik
tako da unosi indeks elementa. Program završava kada korisnik unese negativnu vrijednost.

      /* Program fseek.c */
      #include <stdlib.h>
      #include <stdio.h>

      #define MAX 50

      int main()
      {
          FILE *fp;
          int data, i, niz[MAX];
          long pomak;

           /* Inicijaliziraj niz od 50 elemenata po slucajnom uzorku */
           for (i = 0; i < MAX; i++)
               niz[i] = rand();

           /* Otvori binarnu datoteku RANDOM.DAT za čitanje i pisanje. */
           if ( (fp = fopen("RANDOM.DAT", "w+b")) == NULL)
           {
               fprintf(stderr, "nGreska pri otvaranje datoteke");
               exit(1);
           }
            /* upiši niz */
           if ( (fwrite(niz, sizeof(int), MAX, fp)) != MAX)
           {
              fprintf(stderr, "nGreska pisanja u datoteku");
            exit(1);




                                                                                           207
}

           /* Pitaj korisnika koji element zeli ucitati, */
           /* zavrsi ako se unese negativna vrijednost */
           while (1)
           {
               printf("nIzaberi element datoteke: 0-%d ili -1 za kraj:",
                       MAX-1);
               scanf("%ld", &pomak);
               if (pomak < 0)
                   break;
               else if (pomak > MAX-1)
                   continue;
               /* Postavi pozicijski indikator datoteke */
               if ( (fseek(fp, (pomak*sizeof(int)), SEEK_SET)) != 0)
               {
                   fprintf(stderr, "nGreska fseek().");
                   exit(1);
                }

                 /* Zatim učitaj element i prikazi njegovu vrijednost. */
                fread(&data, sizeof(int), 1, fp);
                printf("nElement %ld ima vrijednost %d.", pomak, data);
           }
           /* zatvori datoteku */
           fclose(fp);
           return(0);
      }



15.9 Funkcije za održavanje datoteka
Temeljne operacije za održavanje datoteka su brisanje datoteka, promjena imena datoteka i
kopiranje datoteka. Prije je definirana funkciju za kopiranje datoteke. Za brisanje datoteke
koristi se funkcija remove(), a za promjenu imena datoteke koristi se funkcija rename().
Ove funkcije su deklarirane u datoteci <stdio.h>.
remove
Prototipe funkcije remove(), kojom se briše datoteka glasi:

      int remove( const char *imedatoteke);

Funkcija kao argument prima pokazivač stringa koji sadrži ime datoteke (uključujući i stazu)
koju treba izbrisati. Operacija se može izvesti samo ako ta datoteka nije otvorena. Funkcija
vraća vrijednost 0 ako je operacija brisanja uspješna, a ako je operacija neuspješna vraća
vrijednost -1. Razlog neuspješnog brisanja može biti kada datoteka ne postoji ili kada je
spremljena s atributom read-only.
rename
Prototipe funkcije rename(), kojom se mijenja ime datoteke glasi:

      int rename( const char *ime, const char *novo_ime );

Funkcija prima dva argumenta – pokazivače na string - prvi je ime datoteke, a drugi novo ime
za datoteku. Operacija se može izvesti samo ako ta datoteka nije otvorena. Funkcija vraća




                                                                                        208
vrijednost 0 ako je operacija uspješna, a ako je operacija neuspješna vraća vrijednost -1.
Razlog neuspješne operacije može biti:
     • ne postoji datoteka ime
     • već postoji datoteka s imenom novo_ime
     • zadano je novo_ime s drugom oznakom diska

Privremene datoteke
Ponekad je potrebno formirati tzv. privremenu datoteku koja će poslužiti za smještaj podataka
koji se vrši samo za vrijeme izvršenja programa. Tada nije bitno kako se datoteka zove, jer se
nju treba izbrisati prije nego završi program. Za formiranje imena privremene datoteke može se
koristiti funkcija tmpname(), koja je deklarirana u <stdio.h>. Prototip joj je:

             char *tmpnam(char *s);

Argument funkcije je pokazivač stringa koji pokazuje na memoriju koja je dovoljna za smještaj
imena datoteke. Ako je taj pokazivač jednak NULL tada funkcija tmpname() koristi vlastiti
statički spremnik u kojem generira neko ime. Funkcija tada vraća pokazivač na taj spremnik.

Način formiranja imena privremene datoteke je određen na način koji osigurava da se u jednom
programu ne mogu pojaviti dva ista imena za privremenu datoteku.

Primjer: u programu tmpname.c demonstrira se korištenje tzv. privremenih datoteka

      /* Datoteka: tmpname.c
       * Formiranje privremenih datoteka             */

      #include <stdio.h>

      int        main()
      {
             char ime[80];
             FILE *tmp;

             /* Formiraj privremenu datorteku */
             tmpnam(ime);
             tmp = fopen(ime, "w");
             if(tmp != NULL)
             {
                /* koristi datoteku */
                printf("Formirana datoteka imena: %sn", ime);

                   /* nakon koristenja zatvori datoteku **/
                   fclose(tmp);

                   /* i izbrisi je s diska */
                   remove(ime);
             }
      }


Dobije se ispis:

      Formirana datoteka imena: s3a4.




                                                                                          209
16 Apstraktni tipovi podataka - ADT


Naglasci:
   • Apstraktni dinamički tip podataka -ADT
   • Model, specifikacija, implementacija i aplikacija ADT-a
   • ADT STACK za rad sa stogom podataka
   • Proračun aritmetičkog izraza postfiksne notacije
   • ADT QUEUE za rad s redom a čekanje



16.1 Koncept apstraktnog dinamičkog tipa podataka
Pomoću struktura, pokazivača i funkcija mogu se realizirati apstraktni dinamički tipovi
podataka (ADT – eng. abstract data type). Mi smo, na neki način, već do sada koristili
apstraktne tipove. Primjerice, int, char ili float apstraktno označavaju karakteristike nekog
memorijskog objekta i operacije koje se mogu izvršavati s tim objektom. Pošto smo se na te
tipove navikli, oni su u našem mentalnom sklopu postali "konkretni" primitivni tipovi C jezika.
Sada će ideja tipa biti proširena i na druge objekte apstrakcije, tako da apstraktni tip predstavlja
oznaku za skup objekata koji se ponašaju u skladu s definiranim operacijama.

Na slici 16.1 prikazan je konceptualni model za rad s apstraktnim tipom podataka. Čini ga:
1. Model
Prvi stupanj definiranja ADT-a je izrada modela podataka i operacija kojima se opisuje objekt
apstrakcije, neovisno o načinu kako će biti implementiran u C jeziku. Model se opisuje
algoritamskim zapisima i matematičkom aksiomatikom operacija.
2. Specifikacija
Na temelju tog modela izrađuje se specifikacija u C jeziku koja mora sadržavati:
    • identifikator tipa kojim se označava ADT,
    •   prototip funkcija kojima se realizira model operacija s apstraktnim objektom,
    •   uz funkcije treba jasno dokumentirati koji su uvjeti za primjenu funkcije (eng.
        precondition) i stanje objekta nakon djelovanja funkcije (eng. postcondition).
Specifikacija se zapisuje u "*.h" datoteci. Ona predstavlja sučelje prema aplikaciji.
3. Implementacija
Na temelju specifikacije vrši se implementacija modela, odnosno definiranje potrebnih C
funkcija i struktura podataka. Implementacija se zapisuje u jednoj ili više datoteka kao
samostalni modul koji se može kompilirati neovisno od programa u kojem se koristi.
4. Aplikacija
Korisnik upotrebljava ADT modul na temelju opisa koji je dan specifikacijom, i ne zanima ga
kako je izvršena programska implementacija modela.




                                                                                                210
Objekt
              apstrakcije

                                   ADT - model podataka i operacija kojima se
                                   opisuje objekt apstrakcije




     Izrada programa             Sučelje s aplikacijom:               Implementacija
                                 - oznaka tipa ADT-a                  funkcija i struktura
                                 - specifikacija funkcija i           podataka potrebnih
     Izvršni program               uvjeta za primjenu ADT-a           za realizaciju ADT-a




                            Slika 16.1. Konceptualni model ADT-a


    Kako se realizira apstraktni tip bit će najprije pokazano na primjeru apstraktnog objekta
brojača. Primjer programske realizacije objekta brojača već je prije opisan u lekciji o
modularnom programiranju. Tada je model rada brojača bio sljedeći - stanje objekta brojača
opisivale su dvije statičke varijable: count (koja pokazuje izbroj) i mod (koja određuje modul
brojača), a operacije s brojačem bile su reset_counter(), incr_count(), get_count(),
get_modul(). Nedostatak te - statičke - realizacije brojača je u tome što u jednom programu
omogućuje postojanje samo jednog apstraktnog objekta brojača. Sada će biti pokazano kako se
programski može omogućiti višestruka pojavnost objekta brojača.
Stanje apstraktnog objekta brojača bit će opisano strukturom _counter, pomoću koje se
definira i tip COUNTER, koji označava pokazivač na ovu strukturu;

      struct _counter {
          int count;
          int mod;
      };
      typedef struct _counter *COUNTER;

    Zapis velikim slovima je izvršen iz razloga da podsjeti kako se radi o pokazivačkom tipu.
Ime COUNTER se dalje koristi kao oznaku tipa ADT-a brojača. Pojavnost (instancu) objekta tipa
COUNTER, određuju dvije funkcije: new_counter(), koja vrši dinamičko alociranje objekta
brojača i inicira varijable tipa COUNTER, te delete_counter(), koja dealocira objekt brojača.

    Pošto je model brojača poznat, sada slijedi opis specifikacije ADT-a COUNTER (u datoteci
"counter-adt.h"). Uz svaki prototip funkcije u komentaru su opisani: namjena funkcije,
argumenti i vrijednost koju funkcija vraća, uvjeti koji moraju biti zadovoljeni za primjenu
funkcije (PRE) i stanje nakon primjene funkcije (POST).




                                                                                          211
/* Datoteka: counter-adt.h
         Specifikacija ADT brojača po modulu: mod
       */

       typedef struct _counter *COUNTER;

       COUNTER new_counter(int mod);
       /* Funkcija: alocira i inicijalizira novi objekt tipa COUNTER
        *           vraca pokazivač tipa COUNTER
        * Argumenti: mod je modul brojača
        * POST: brojač na nuli, a modul brojaca na vrijednost mod.
        * Ako je mod<=1, modul se postavlja na vrijednost INT_MAX
        */

       void delete_counter(COUNTER pc);
       /* Funkcija: dealocira objekt ADT brojaca
        * PRE: pc != NULL
        * POST: pc==NULL
        */

       void reset_counter(COUNTER pc, int mod);
       /* Funkcija: resetira stanje brojača
        * PRE: pc != NULL
        * POST: brojač na nuli, a modul brojača ima vrijednost mod.
        * Ako je mod<=1, modul se postavlja na vrijednost INT_MAX
        */

       int incr_count(COUNTER pc);
       /* Funkcija: inkrementira brojac u intervalu 0..mod-1
        * PRE: pc != NULL
        * POST: vrijednost brojača inkrementirana (u intervalu 0..mod-1)
        */

       int get_count(COUNTER pc);
       /* Funkcija: vraca trenutnu vrijednost brojača
        * PRE: pc != NULL
        */

       int get_modul(COUNTER pc);
       /* Funkcija: vraca vrijednost modula brojača
        * PRE: pc != NULL
        */

Na temelju specifikacije vrši se implementacija modula ADT-a. Uočimo da se u specifikaciji
funkcija pojavljuje preduvjet PRE: pc != NULL. Ovaj će preduvjet biti uvijek ispunjen ako se
pri inicijalizaciji objekta brojača ispita vrijednost pokazivača na objekt, primjerice

      COUNTER pc = new_counter(0); /* inicijalizirara brojač pc s
                                      mod=INT_MAX*/
      if(pc == NULL) exit(1);      /* ako je p==NULL prekini program */


Tjekom razvoja modula poželjno je provjeravati ovaj preduvjet u svakoj funkciji. U C jeziku,
prema ANSI standardu, u datoteci <assert.h> definirana je makro naredba:

       assert( uvjet )




                                                                                        212
kojom se može provjeravati da li je neki uvjet ispunjen. Ako uvjet nije ispunjen, prekida se
program i izvještava u kojoj datoteci i u kojem retku izvornog koda je došlo do greške. Pošto
ovo ispitivanje usporava program, predviđeno je da se ova provjera može isključiti. Ako ne
želimo da se vrši ova provjera tada se u komandnoj liniji kompilatora treba definirati simbol
NDEBUG, primjerice:

      c:> cl /D"NDEBUG" ime_datoteke.c

ili u izvornom kodu ispred direktive #include <assert.h> treba definirati simbol NDEBUG,
tj.

      #define NDEBUG 1
      #include<assert.h>

U implementaciji brojača koristit će se makro naredba assert(pc != NULL), i to u svim
funkcijama koje kao argument imaju pokazivač pc. Slijedi opis implementacije:

      /* Datoteka: counter-adt.c
       * Implementacija ADT brojača po modulu mod
       */

       #include    <limits.h>            /* zbog definicija INT_MAX*/
       #include    <stdlib.h>            /* zbog definicija malloc i free*/
       #include    <assert.h>
       #include    "counter-adt.h"

      typedef struct _counter {
          int count;
          int mod;
       } counter;

      /* typedef struct _counter *COUNTER;              definiran u counter-adt.h */

       COUNTER new_counter(int mod)
       {
           COUNTER pc = malloc(sizeof(counter));
           if(pc != NULL) {
              if(mod <= 1) mod = INT_MAX;
              pc->mod = mod;
              pc->count=0;
           }
           return pc;
       }

       void delete_counter(COUNTER pc) {              assert(pc != NULL);
           free(pc);
       }

       void reset_counter(COUNTER pc, int mod) {                  assert(pc != NULL);
            if(mod <= 1) mod = INT_MAX;
            pc->mod = mod;
            pc->count=0;
       }

       int incr_count(COUNTER pc) { assert(pc != NULL);
          pc->count++;
          if(pc->count >= pc->mod) pc->count = 0;
          return pc->count;




                                                                                         213
}

       int get_count(COUNTER pc)           {     assert(pc != NULL);
          return pc->count;
       }

       int get_modul(COUNTER pc)           {     assert(pc != NULL);
          return pc->mod;
       }


Modul ADT-a se može testirati programom testcount-adt.c u kojem se inicijaliziraju dva
neovisna objekta brojača pc1 i pc2. Prvi na mod=5, a drugi na mod=INT_MAX.

      /* Datoteka: testcount-adt.c */
      #include <stdio.h>
      #include "counter-adt.h"

      int main(void)
      {
         int i;
         COUNTER pc1 = new_counter(5);
         COUNTER pc2 = new_counter(0);

           if(pc1 == NULL || pc2 == NULL) exit(1);

           printf("brojac(mod=%d), brojac(mod=%d)n",
                  get_modul(pc1),get_modul(pc2));

           for(i=0; i<=10; i++) {
              incr_count(pc1);
              incr_count(pc2);
              printf("%dtt %dn", get_count(pc1),get_count(pc2));
           }
           printf("itd........n");
           delete_counter(pc1);
           delete_counter(pc2);
           return 0;
      }


Nakon izvršenja ovog programa dobije se ispis:

      brojac(mod=5), brojac(mod=2147483647)
      1                1
      2                2
      3                3
      4                4
      0                5
      1                6
      2                7
      3                8
      4                9
      0                10
      1                11
      itd........




                                                                                  214
Na kraju razmatranja važno je uočiti da u specifikaciji (counter-adt.h) nije navedena
struktura podataka koja služi za implementaciji objekta brojača. Deklariran je samo pokazivač
na "neku" strukturu - COUNTER. Kako izgleda ta struktura važno je implementatoru ADT-a, a
ne onome tko ga koristi. Na ovaj način se ostvaruje princip enkapsulacije – skrivanja detalja
implementacije od korisnika modula. Početnicima ovaj princip nema posebno značenje, ali
iskusnim programerima on znači jednu od temeljnih paradigmi modernog programiranja.
Enkapsulacija olakšava timski rad i doradu modula ADT-a, bez obzira u kojoj će aplikaciji biti
primijenjen.


16.2 Stog i STACK ADT
     Stog je naziv za kolekciju podataka kojoj se pristupa po principu LIFO – last in first out.
Primjerice, kada slažemo tanjure tada stavljamo jedan tanjur poviše drugog – tu operaciju
zovemo push(), a kada uzimamo tanjur tada uvijek uzimamo onaj tanjur kojeg smo posljednjeg
stavili u stog – tu operaciju nazivamo pop(). Pored ove dvije temeljne operacije, obično se u
pristupu stogu koriste još dvije operacije: top() – vraća vrijednost elementa koji je na vrhu stoga
i empty() – vraća 1 ako je stog prazan, inače vraća 0.
Stog se može realizirati pomoću ADT STACK, koji ima sljedeću specifikaciju:

#ifndef STACK_ADT
#define STACK_ADT

typedef int stackElemT;
typedef struct stack *STACK;

STACK stack_new(void);
/* alocira memoriju za stog */
/* vraća pokazivač na stog */

void stack_free(STACK S);
/* dealocira memoriju koju zauzima stog */

int stack_empty(STACK S);
/* vraca 1 ako je stog prazan */

unsigned stack_count(STACK S);
/* vraca broj elemenata na stogu */

stackElemT Top(STACK S);
/* dobavlja vrijednost elementa na vrha stoga */

stackElemT Pop(STACK S);
/* dobavlja vrijednost elementa na vrha stoga */
/* i odstranjuje ga sa stoga */
/* PRE: stog postoji */
/* POST: na stogu je jedan element manje */

void Push(STACK S, stackElemT x);
/* postavlja element na vrh stoga */                                   Slika 16.2. Stog - operacije
/* PRE: stog postoji */
/* POST: na stogu je jedan element vise */

#endif




                                                                                               215
Implementacija se može izvršiti na više načina. Sada će biti opisana implementacija u kojoj
se za spremanje elemenata stoga koristi podatkovna struktura tipa dinamičkog niza, a u
poglavlju 18 bit će pokazana implementacija pomoću strukture podataka tipa vezane liste.
Implementacija ADT STACK pomoću dinamičkog niza je opisana u datoteci "stack-arr.c".

      /* Datoteka: stack-arr.c:
       * Implementacija ADT STACK pomoću niza
       */
      #include <stdlib.h>
      #include "stack.h"

      #define STACK_GROW 10U
      #define STACK_SIZE 100U

      /* typedef int stackElemT;                     definirano in stack.h*/
      /* typedef struct stack *STACK;                definirano in stack.h*/

      struct stack {
           stackElemT *A;
           unsigned top;           /* indeks poviše stvarnog vrha stoga*/
           unsigned size;          /* veličina niza*/
      };

      static void stack_error(char *s)
      {
           printf("nGreska: %sn", s); exit(1);
      }

      STACK stack_new(void)
      {
         STACK S = malloc(sizeof(struct stack));
         if(S != NULL) {
            S->size = STACK_SIZE;
            S -> top = 0;
            S->A = malloc(sizeof(stackElemT) * S->size);
            if(S->A == NULL) {free(S); S=NULL;}
         }
         return S;
      }

      void stack_free(STACK S)
      {
         if(S->A != NULL) free(S->A);
         if(S != NULL)    free(S);
      }

      int stack_empty(STACK S) { return (S->top <= 0);                   }

      stackElemT stack_pop(STACK S)
      {
          if(stack_empty(S)) stack_error("Stog prazan");
             return S->A[--(S->top)];
      }

      void stack_push(STACK S, stackElemT x)
      {
           if (S->top >= S->size) {
                S->size += STACK_GROW;




                                                                                           216
S->A = realloc(S->A, sizeof(stackElemT) * S->size);
                 if(S->A == NULL)
                      stack_error("Nema slobodne memorije");
             }
             S->A[(S->top)++] = x;
     }

     stackElemT stack_top(STACK S)
     {
         if(stack_empty(S)) stack_error("Stog prazan");
            return S->A[S->top-1];
     }


Testiranje ADT STACK provodi se programom stack-test.c.

     /* Datoteka: stack-test.c */
     #include <stdio.h>
     #include <stdlib.h>
     #include "stack-arr.c"

     void upute(void) /* Upute za korisnika */
     {
        printf("Otipkaj:n"
               "1 - push - gurni vrijednost na stogn"
               "2 - pop - skini vrijednost sa stogan"
               "0 - kraj programan");
     }

     int main(void)
     {
        int izbor, val;
        STACK stog = stack_new();

         upute(); printf("? ");
         scanf("%d", &izbor);
         while (izbor != 0) {
            switch (izbor) {
               case 1:      /* push */
                  printf("Unesi integer: ");
                  scanf("%d", &val);
                  stack_push(stog, val);
                  break;

                 case 2:      /* pop */
                    if (!stack_empty(stog))
                       printf("Podignuta je vrijednost %d.n",
                               stack_pop(stog));
                    else
                       printf("Stog je prazan.n");
                    break;
                 default:
                    printf("Pogresan odabir opcije. Ponovi!nn");
                    upute();
                    break;
              }
              printf("? ");
              scanf("%d", &izbor);
         }




                                                                       217
printf("nStog:");
          while (!stack_empty(stog))
              printf(" %d", stack_pop(stog));
          stack_free(stog);
          return 0;
      }



16.3 Primjena stoga za proračun izraza postfiksne notacije
     Korištenjem programski simuliranog stoga jednostavno se provodi računanje matematičkih
izraza u postfiksnoj notaciji. Postfiksna notacija izraza se piše tako da se najprije napišu
operandi, a iza njih operator koji na njih djeluje, primjerice


      infiksna notacija izraza                  postfiksna notacija izraza
      A + B * C                                 A B C * +
      (A + B) * C                               A B + C *
      (a + b)/(c – d)                           a b + c d - /
      a * b / c                                 a b * C /


Ovaj tip notacije se naziva i obrnuta poljska notacije, prema autoru Lukasiewiczu. Svojstva
postfiksne notacije su:

    1. Svaka formula se može napisati bez zagrada.
    2. Infiksni operatori moraju uvažavati pravila prioriteta što nije potrebno kod postfiksne
       notacije.
    3. Za proračun postfiksne notacije prikladna je upotreba stoga.

Pretvorba infiksne u postfiksnu notaciju se izvodi slijedećim algoritmom:

    1. Kompletno ispiši zagrade između svih operanada, tako da zagrade potpuno odrede
       redoslijed izvršenja operacija.
    2. Pomakni svaki operator na mjesto desne zagrade
    3. Odstrani zagrade


Pretvorba izraza (8+2*5)/(1+3*2-4), prema gornjem pravilu, je sljedeća:


      prema 1.              ( ( 8 + ( 2 * 5 ) ) / ( 1 + ( ( 3 * 2 ) - 4 ) ) )

      prema 2. i 3.         ( ( 8 + ( 2 * 5 ) )           ( 1 + ( ( 3 * 2 ) - 4 )          ) /
                            ( 8   ( 2 * 5 ) +             1   ( ( 3 * 2 ) - 4 ) +          /
                              8     2   5 * +             1   ( ( 3 * 2 )   4 - +          /
                              8     2   5 * +             1       3   2 *   4 - +          /

      daje notaciju:           8 2 5 * + 1 3 2 * + 4 - /




                                                                                             218
Za izračun postfiksnog izraza, koji ima n simbola, vrijedi algoritam:

1. Neka je k =1 indeks prvog simbola
2. Dok je k <= n ponavljaj
        Ako je k-ti simbol operand, stavi ga na stog.
        Ako je k-ti simbol operator, dobavi dvije vrijednosti sa stoga (najprije drugi, pa prvi
        operand), izvrši naznačenu operaciju i rezultat vrati na stog.
        Uvećaj k za 1
3. Algoritam završava s rezultatom na stogu.

Primjer: Izraz zapisan infiksnoj notaciji:

      (8+2*5) / (1+3*2-4).

ima postfiks notaciju:

      8 2 5 * + 1 3 2 * + 4 - /

Proračun ovog izraza pomoću stoga ilustriran je na slici 16.3:


      Neobrađeni ulazni      niz           Operacija                          Sadržaj stoga
1     8 2 5 * + 1 3 2 *      + 4   -   /   push 8                             8
2       2 5 * + 1 3 2 *      + 4   -   /   push 2                             8 2
3         5 * + 1 3 2 *      + 4   -   /   push 5                             8 2 5
4           * + 1 3 2 *      + 4   -   /   pop(b), pop(a),push (a*b)          8 10
5             + 1 3 2 *      + 4   -   /   pop(b), pop(a),push(a+b)           18
6               1 3 2 *      + 4   -   /   push 1                             18 1
7                 3 2 *      + 4   -   /   push 3                             18 1 3
8                   2 *      + 4   -   /   push 2                             18 1 3 2
9                     *      + 4   -   /   pop(b), pop(a),push (a*b)          18 1 6
10                           + 4   -   /   pop(b), pop(a),push(a+b)           18 7
11                             4   -   /   push 4                             18 7 4
12                                 -   /   pop(b), pop(a),push(a-b)           18 3
13                                     /   pop(b), pop(a),push(a/b)           6



      Slika 16.3. Korištenje stoga za proračun izraza koji je zapisan u postfiksnoj notaciji


     Proračun izraza pomoću postfiksne notacije je jedan od uobičajenih načina kako
interpreteri izračunavaju izraze zapisane u višim programskim jezicima - najprije se vrši
pretvorba infiksnog zapisa izraza u postfiksni zapis, a zatim se proračun izraza vrši pomoću
stoga. U ovom slučaju ne koristi se stog kojim upravlja procesor već se rad stoga simulira
programski.

Primjer: Datoteka "polish.c" sadrži jednostavni interpreter aritmetičkih izraza. Izraz treba
zapisati u komandnoj liniji unutar navodnika, primjerice

      c:> polish         "8 2 5 * + 1 3 2 * + 4 - /"

U izrazu se smiju koristiti cijeli brojevi i operatori zbrajanja, oduzimanja, množenja i dijeljenja.

Kada se program izvrši dobije se ispis:

      Rezultat: 8 2 5 * + 1 3 2 * + 4 - / = 6




                                                                                                 219
/* Datoteka polish.c:
 * Proracun izraza postfiksne notacije
 */
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <ctype.h>
#include "stack.h"
#include "stack-arr.c"

#define Pop()  stack_pop(stack)
#define Top()  stack_top(stack)
#define Push(x) stack_push(stack, (x))

main(int argc, char *argv[])
{
    char *str;
    int i, len, tmp;
    STACK stack;

     if (argc < 2) {
       printf("Za proracun (30/(5-2))*10 otkucaj:n");
       printf("c:> polish "30 5 2 - / 10 *"n");
       exit(1);
     }
     str = argv[1];
     len = strlen(str);

     stack = stack_new();

     for (i = 0; i < len; i++)
     {
         if (str[i] == '+')    Push(Pop()+ Pop());
         if (str[i] == '*')    Push(Pop()* Pop());
         if (str[i] == '-') { tmp = Pop(); Push(Pop()- tmp); }
         if (str[i] == '/') {
           int tmp = Pop();
           if (tmp==0) {printf("Djeljenje s nulomn"); exit(1);}
           Push(Pop() / tmp);
         }
         if (isdigit(str[i])) /* konverzija niza znamenki u broj */
         {
           Push(0);
           do {
              Push(10*Pop() + (int)str[i]-'0');
              i++;
           } while (isdigit(str[i]));
           i--;
         }
     }

     printf("Rezultat: %s = %d n", str, Pop( ));
     stack_free(stack);
 }




                                                                220
16.4 Red i QUEUE ADT
    Red (eng. queue) je struktura koja podsjeća na red za čekanje. Iz reda izlazi onaj koji je prvi
u red ušao. Ovaj princip pristupa podacima se naziva FIFO – first in first out. Temeljne su
operacije:

           ADT QUEUE
           get(Q )            - dobavi element iz reda Q.
           put( Q , el)       - stavi element el u red Q.
           empty( Q )         - vraća 1 ako je red Q prazan, inače vraća 0.
           full( Q )          - vraća 1 ako je red Q popunjen, inače vraća 0.



Ovakvi se redovi mogu realizirati kao ADT QUEUE prema sljedećoj specifikaciji:

      /* Datoteka: queue.h */

      #ifndef _QUEUE_ADT
      #define _QUEUE_ADT

      typedef int queueElemT;
      typedef struct queue *QUEUE;

      QUEUE queue_new(void);
      /* formira novi objekt tipa QUEUE */

      void queue_free(QUEUE Q);
      /* dealocira objekt tipa QUEUE */

      int queue_empty(QUEUE Q);
      /* vraća 1 ako je red prazan */

      int queue_full(QUEUE Q);
      /* vraća 1 ako je red popunjen */

      void queue_put(QUEUE Q, queueElemT el);
      /* stavlja element u red */

      queueElemT queue_get(QUEUE Q);
      /* vraća element iz reda */

      void queue_print(QUEUE Q);

      #endif


Implementacija se može provesti na više načina. Za smještaj elemenata reda najčešće se koristi
niz ili linearna lista. Najprije ćemo upoznati implementaciju pomoću niza, i to implementaciju
koja koristi tzv. cirkularni spremnik. On se realizira pomoću niza, kojem dva indeksa: back i
front, označavaju mjesta unosa (back) i dobave (front) iz reda. Niz je veličine
QUEUEARRAYSIZE. Operacije put() i get() se mogu ilustrirati na sljedeći način:




                                                                                               221
Početno je red prazan              -      -          -       -        -      -
                                  Front=back                           (red prazan)
Front = back;
Nakon operacija put() povećava put('a'); put('b')
se indeks back za 1             a      b      -              -        -          -
                                   front            back
Operacija get() dobavlja element x = get() /* x sadrži vrijednost a */
kojem je indeks jednak front, a   a      b     -      -      -      -
zatim se front povećava za               front  back
jedan.
Što napraviti kada, nakon         put('c'); put('d'); put('e');
višestrukog unosa, back postane    -        b      c         d        E          -
jednak krajnjem indeksu niza?                front                                back
Ideja je da se back ponovo put('f')
postavi na početak niza (takovi -         b         C   d      e      f
spremnik se nazivaju cirkularni    back     front          (red popunjen)
spremnik). Time se maksimalno
iskorištava prostor spremnika za Red popunjen ako je:
smještaj elemenata reda.               (back+1) % QUEUEARRAYSIZE == front


U datoteci "queue_arr.c" realiziran je ADT QUEUE pomoću cirkularnog spremnika.

      /* Datoteka: queue-arr.c
       * QUEUE realiziran kao cirkularni spremnik
       */

      #include    <stdio.h>
      #include    <stdlib.h>
      #include    <assert.h>
      #include    "queue.h"

      #define QUEUESIZE 100             /* maksimalni broj elemenata */
      #define QUEUEARRAYSIZE (QUEUESIZE +1) /* veličina niza */

      /* typedef int queueElemT; */
      /* typedef struct queue *QUEUE;          definirani u queue.h*/

      struct queue {
          queueElemT A[QUEUEARRAYSIZE];
          int front;
          int back;
      };

      QUEUE queue_new(void)
      {
         QUEUE Q = malloc(sizeof(struct queue));
         if(Q != NULL) Q->front = Q->back = 0;
         return Q;
      }

      void queue_free(QUEUE Q)
      {
         assert(Q != NULL);
         if(Q != NULL) free(Q);




                                                                                     222
}

     int queue_empty(QUEUE Q)
     {
        assert(Q != NULL);
        return (Q->front == Q->back);
     }

     int queue_full(QUEUE Q)
     {
        assert(Q != NULL);
        return ((Q->back + 1) % QUEUEARRAYSIZE == Q->front);
     }

     void queue_put(QUEUE Q, queueElemT x)
     {
        assert(Q != NULL);
        Q->A[Q->back] = x;
        Q->back = (Q->back + 1) % QUEUEARRAYSIZE;
     }

     queueElemT queue_get(QUEUE Q)
     {
        queueElemT x;
        assert(Q != NULL);
        x = Q->A[Q->front];
        Q->front = (Q->front +1) % QUEUEARRAYSIZE;
        return x;
     }

     void queue_print(QUEUE Q)
     {
        int i;
        assert(Q != NULL);
        printf("Red: ");
        for(i = Q->front % QUEUEARRAYSIZE; i < Q->back;
            i=(i+1)% QUEUEARRAYSIZE )
            printf("%d, ", Q->A[i]);
        printf("n");
     }


Testiranje ADT QUEUE provodi se programom queue-test.c.

     /* Datoteka: queue-test.c */
     #include <stdio.h>
     #include <stdlib.h>
     #include <assert.h>
     #include "queue.h"
     #include "queue-arr.c"

     void upute(void)
     {   printf ("Izbornik:n"
                "   1 Umetni broj u Redn"
                "   2 Odstrani broj iz Redan"
                "   0 Krajn");
     }

     int main()




                                                               223
{
          int izbor, elem;

          QUEUE Q = queue_new();
          upute();
          printf("? ");
          scanf("%d", &izbor);

          while (izbor != 0) {
             switch(izbor) {
                case 1:
                   printf("Otkucaj broj: ");
                   scanf("n%d", &elem);
                   if (!queue_full(Q)) {
                      queue_put(Q, elem);
                      printf("%d ubacen u red.n", elem);
                   }
                   queue_print(Q);
                   break;
                case 2:
                   if (!queue_empty(Q)) {
                      elem = queue_get(Q);
                      printf("%d odstranjen iz reda.n", elem);
                   }
                   queue_print(Q);
                   break;

                  default:
                     printf("Pogresan Izbor.nn");
                     upute();
                     break;
              }
              printf("? ");
              scanf("%d", &izbor);
          }
          return 0;
      }



16.5 Zaključak
Opisana je metoda programiranja, pomoću koje se sustavno analizira, specificira i implementira
programske objekte kao apstraktne dinamičke tipove podataka – ADT.

Izrada specifikacije operacija s apstraktnim objektima sve više postaje temeljni element
programiranja. To osigurava da se maksimalna pažnja posveti onome što treba programirati.

Neovisnost specifikacije od implementacije ADT, osigurava fleksibilan i pouzdan razvoj
programa.

U specifikaciji ADT-a dva su temeljna elementa: ime ADT-a i operacije koje se mogu izvršiti.

To je karakteristka tipova, pa se s ADT-om kreiraju novi apstrakni tipovi podataka.

Rad s ADT predstavlja objetno temeljeno programiranje.




                                                                                          224
17 Rekurzija i složenost algoritama


Naglasci:
   • rekurzija
   • podijeli pa vladaj
   • kompleksnost algoritama
   • binarno pretraživanje niza
   • sortiranje


17.1 Rekurzivne funkcije
     U programiranju i matematici često se koriste rekurzivne definicije funkcija. Direktna
rekurzija nastaje kada se u definiciji funkcije poziva ta ista funkcija, a indirektna rekurzija
nastaje kada jedna funkcija poziva drugu funkciju, a ova ponovo poziva funkciju iz koje je
pozvana.
     Definicija rekurzivne funkcije u pravilu se sastoji od dva dijela: temeljnog slučaja i pravila
rekurzije. Primjerice, u matematici se se može rekurzivno definirati funkcija n! (n-faktorijela)
na sljedeći način:
      Definicija n! (n-faktorijela):

        1. Temeljni slučaj:      0! = 1             za n=0
        2. Pravilo rekurzije:    n! = n * (n-1)!    za n>0

Sve vrijednosti od n! se mogu izračunati pomoću gornjeg rekurzivnog pravila, tj.

      1!   =   1 * 0! = 1 * 1 = 1
      2!   =   2 * 1! = 2 * 1 * 0! = 2 * 1 * 1 = 2
      3!   =   3 * 2! = 3 * 2 * 1! = 3* 2 * 1 * 0! = 3 * 2 * 1 * 1 = 6
      4!   =   ....

Uočite da rekurzivno pravilo znači ponavljanje nekog odnosa, a temeljni slučaj označava prostu
radnju nakon koje prestaje rekurzija.
    Programski se funkcija n! realizira kao funkcija fact(), koja prima argument tipa int i
vraća vrijednost tipa int. Koristeći prethodnu matematičku definiciju , funkcija fact() se
implementira na sljedeći način:
        int fact(int n)
        {
            if (n == 0)
                return 1;
            else
                return fact(n-1) * n;
        }

Uočite da se u tijelu funkcije fact() poziva ta ista funkcija.
     Kako se izvršava ova funkcija? Da bi to shvatili, potrebno je znati kako se na razini
strojnog koda vrši poziv funkcije. Većina kompilatora to vrši na sljedeći način:




                                                                                               225
1. Pri pozivu funkcije najprije se argumenti funkcije postavljaju u dio memorije koji je
       predviđen za lokalne varijable. Ta memorija se naziva izvršni stog, jer se podacima
       pristupa s pus() i pop() opearacijama. Vrijednost, koja je posljednja stavljena na stog,
       predstavlja vrh stoga.
    2. Zatim se izvršava kôd tijela pozvane funkcije. U njoj se argumenti funkcije tretiraju kao
       lokalne varijable čija je vrijednost na stogu.
    3. Povrat iz funkcije se vrši tako da se stanje izvršnog stoga vrati na stanje prije poziva
       funkcije, a povratna se vrijednost postavlja u registar procesora, tzv. povratni registar.
    4. Izvršenje programa se nastavlja naredbom u pozivnoj funkciji koja slijedi iza pozvane
       funkcije.

Na slici 1 prikazano je izvršenje funkcije fact(4) i stanje izvršnog stoga.

                                                                   stanje stoga koji se
  izvršenje funkcije fact(4)                                       koristi za prijenos
                                                                   argumenata funkcije
  fact(4)=                                                               ... 4
        4 * fact(3)=                                                     ... 4 3
                 3 * fact(2)                                             ... 4 3 2
                          2 * fact(1)                                    ... 4 3 2 1
                                   1 * fact(0)                           ... 4 3 2 1 0
                                       return 1                          ... 4 3 2 1
                              return 1*1                                 ... 4 3 2
                     return 2*1                                          ... 4 3
             return 3*2                                                  ... 4
  return 4*6                                                             ...
  => 24
                  Slika 17.1. Redoslijed izvršenja rekurzivne funkcije fact(4)


     Poziv funkcije fact(4) počinje tako da se argument vrijednosti 4 stavlja na vrh izvršnog
stoga, a zatim se izvršavaju naredbe iz tijela funkcije. Pošto je argument različit od nule
izvršava se naredba return fact(n-1) * n;. Da bi se ona mogla izvršiti, najprije se vrši
poziv funkcije fact(n-1). Zbog toga se na izvršni stog stavlja vrijednost argumenta 3 i poziva
funkcija fact(3). Ovaj proces se ponavlja sve dok argument funkcije ne postane jednak nuli.
Nakon toga se u tijelu funkcije izvršava naredba return 1;. To znači da se odstranjuje
vrijednost s vrha stoga (0), a u povratni registar se upisuje vrijednost 1. Program nastavlja
izvršenje naredbom koja slijedi iza pozivne funkcije, a to je zapravo onaj dio naredbe return
fact(n-1) * n; u kojoj se vrši množenje povratne vrijednosti od fact(n-1) i argumenta
n, čija se vrijednost nalazi na vrhu stoga (u ovom slučaju to je vrijednost 1). Zatim se u povratni
registar stavlja vrijednost 1*1 i skida argument s vrha stoga, pa na vrhu stoga ostaje vrijednost
2. Program se ponovo vraća na izvršenje u naredbu return 1 * 2;. U povratni registar se
sada upisuje vrijednost 2, na vrhu stoga ostaje vrijednost 3 i izvršenje se vraća u naredbu
return 2 * 3; jer je iz nje vršen poziv fact(3). Nakon toga se izvršava naredba return
6 * 4;. Ovo je posljednja naredba koja će se izvršiti. Nakon nje je vrh stoga prazan, a
izvršenje programa se vraća na naredbu iz pozivne funkcije koja slijedi iza poziva fact(4). U
povratnom registru je vrijednost 24, koja predstavlja vrijednost koju vraća fact(4).
     Moglo bi se slikovito reći da se rekurzivne funkcije izvršavaju tako da se najprije
višestrukim pozivom funkcije vrši "traženje" temeljnog slučaja, pri čemu se pamte sva stanja
procesa, a zatim se problem rješava "izvlačenjem" iz rekurzije.
     Kod funkcija koje imaju veliki broj rekurzivnih poziva može doći do značajnog ispunjenja
izvršnog stoga. Kod MSDOS operativnog sustava može se maksimalno koristiti 64Kbajta za




                                                                                               226
stog, pa treba biti oprezan pri korištenju rekurzivnih funkcija. Kod WIN32 sustava za stog je
predviđeno koristiti do 1Mbajta memorije.

Zadatak: Napišite funkciju

      unsigned suma( unsigned n);

kojoj je argument kardinalni broj n, a funkcija vraća vrijednost koja je jednaka sumi svih
kardinalnih brojeva 0,1,2...,n. Koristite rekurzivnu definiciju:

    1. trivijalni slučaj:       ako je n=0, suma(n) = 0
    2. rekurzivno pravilo:      ako je n>0, suma(n) = suma(n-1)+n


17.2 Matematička indukcija
     Rekurzija se koristi i pri dokazivanju teorema indukcijom. Princip matematičke indukcija
se koristi kod problema čija se zakonitost može označiti cijelim brojem n, kao S(n). Definira se
na sljedeći način.

      Da bi dokazali da vrijedi zakonitost S(n), za bilo koju vrijednost n:
            1. Dokaži da zakonitost S(n) vrijedi u trivijalnom slučaju za n=0
            2. Zatim dokaži da vrijedi S(n), ako se pretpostavi da vrijedi S(n-1).

Primjer: Suma od n prirodnih brojeva se može izračunati prema izrazu:

      1 + 2 + 3 + … + n = n (n +1) / 2

Dokaz:
1. Trivijalni slučaj: za n = 1, suma je jednaka 1

      Pošto je 1(1+1)/2) = 1 dokazano je da vrijedi trivijalni slučaj.

2. Pretpostavimo da vrijedi za 1 + … +( n -1),
   pa ispitajmo da li vrijedi za 1 + … +( n -1) + n ?

 Pošto je
     1 + … +( n -1) + n = (n -1)( n -1+1) / 2 + n = n (n +1) / 2
 dokaz je izvršen.


Zadatak: Dokažite da ova formula vrijedi i za proračun sume svih kardinalnih brojeva (0,1,2,..)
koji su manji ili jednaki n.

Zadatak: Napišite funkciju za proračun sume svih kardinalnih brojeva koji su manji ili jednaki
n, koristeći prethodno izvedenu formulu.


17.3 Kule Hanoja
    Čovjek nije sposoban razmišljati i rješavati probleme na rekurzivan način. U programiranju
se pak rekurziju može koristiti u mnogo slučajeva, posebno kada je njome prirodno definiran




                                                                                            227
neki problem. Jedan od najpoznatijih rekurzivnih problema u kompjuterskoj literaturi je bez
sumnje rješenje inteligentne igre koja se naziva Kule Hanoja. Problem je predstavljen na slici 2.




                                     Slika 17.2. Kule Hanoia

     Postoje tri štapa označena s A, B i C. Na prvom štapu su nataknuti cilindrični diskovi
promjenljive veličine, koji imaju rupu u sredini. Zadatak je premjestiti sve diskove s štapa A na
štap B u redoslijedu kako se nalaze na štapu A. Pri prebacivanju diskova treba poštovati
sljedeće pravila:
        Odjednom se smije pomicati samo jedan disk.
        Ne smije se stavljati veći disk povrh manjeg diska.
        Može se koristiti štap C za privremeni smještaj diskova, ali uz poštovanje prethodna
        dva pravila.

Problem: pomakni N diskova sa štapa A na štap B, može se riješiti rekurzivnim postupkom.

Temeljni slučaj i pravilo rekurzije su:

Temeljni slučaj - Najjednostavniji slučaj kojeg svatko može riješiti je kada kula sadrži samo
jedan disk. Tada je rješenje jednostavno; prebaci se taj disk na ciljni štap B.

Rekurzivno pravilo - Ako kula sadrži N diskova, pomicanje diskova se može izvesti u tri koraka
   1. Pomakni gornjih N-1 diskova sa štapa A na pomoćni štap C.
   2. Preostali donji disk s štapa A pomakni na ciljni štap B.
   3. Zatim pomakni kulu od N-1 diskova s pomoćnog štapa C na ciljni štap B.

    Teško je na prvi pogled prihvatiti da ovo rekurzivno pravilo poštuje pravilo da se uvijek
pomiče samo jedan disk, ali ako se prisjetimo da se rekurzivni problemi počinju rješavati tek
kad je pronađen temeljni slučaj, u kojem se pomiče samo jedan disk, i da su prije toga
zapamćena sva moguća stanja procesa, onda je jasno da se uvijek pomiče samo jedan disk.
    Kako napisati funkciju pomakni_kulu() koja izvršava gornje pravilo. Potrebni argumente
funkcije su: broj diskova koje treba pomaknuti, ime početnog štapa , ime ciljnog štapa, ime
pomoćnog štapa.

      void pomakni_kulu(int n, char A, char B, char C);

Također, potrebno je definirati funkciju kojom će se na prikladan način označiti prebacivanje
jednog diska. Nju se može odmah definirati u obliku:




                                                                                             228
void pomakni_disk(char sa_kule, char na_kulu)
      {
      printf("%c -> %cn", sa_kule, na_kulu);
      }

Korištenjem ove funkcije i pravila rekurzije, funkciju pomakni_kulu() se može napisati na
sljedeći način:

      void pomakni_kulu(int n, char A, char B, char C)
      {
          if (n == 1) {                 /* temeljni slučaj                   */
              pomakni_disk(A, B);
          }
          else {
             pomakni_kulu (n - 1, A, C, B); /* 1. pravilo                    */
             pomakni_disk (A, B);            /* 2. pravilo                   */
             pomakni_kulu (n - 1, C, B, A); /* 3. pravilo                    */
          }
      }


Ili još jednostavnije:

      void pomakni_kulu(int n, char A, char B, char C)
      {
      if (n > 0)
      {
              pomakni_kulu (n - 1, A, C, B);
              pomakni_disk (A, B);
              pomakni_kulu (n - 1, C, B, A);
          }
      }

jer se u slučaju kada je n=1 u funkciji pomakni_kulu(0, ....) ne izvršava ništa, pa se u
tom slučaju izvršava funkcija pomakni_disk(A,B), što je pravilo temeljnog slučaja.

Za testiranje funkcije pomakni_kulu(), koristi se program hanoi.c:

      /* Datoteka: hanoi.c */
      #include <stdio.h>

      void pomakni_kulu(int n, char A, char B, char C);
      void pomakni_disk(char sa_kule, char na_kulu)

      int main()
      {
         int n = 3; /* za slučaj 3 diska*/
         pomakni_kulu(n, 'A','B','C');
         return 0;
      }

Nakon izvršenja ovog programa dobije se izvještaj o pomaku diskova oblika:

      A   ->   B
      A   ->   C
      B   ->   C
      A   ->   B
      C   ->   A




                                                                                     229
C -> B
      A -> B

U ovom primjeru je očito da se pomoću rekurzije dobije fascinantno jednostavno rješenje
problema. Teško da postoji neka druga metoda kojom bi se ovaj problem mogao riješiti na
jednako efikasan način.

Zadatak: Provjerite izvršenje programa za slučaj da broj diskova iznosi: 2, 3, 4 i 5. Pokazat će
se da broj operacija iznosi 2n-1, što se može i logično zaključiti, jer se povećanjem broja
diskova za jedan udvostručuje broj rekurzivnih poziva funkcije pomakni_kulu(), a u
temeljnom slučaju se vrši samo jedna operacija.
Procijenite koliko bi trajalo izvršenje programa pod uvjetom da izvršenje jedne operacije traje
1us i da se koristi 64 diska. Da li izvršenje tog program traje dulje od životog vijeka čovjeka?


17.4 Metoda - podijeli pa vladaj (Divide and Conquer)
     U analizi programskih metoda često se spominje metoda "podijeli pa vladaj". Kod nje se
rekurzija nameće kao prirodni način rješenja problema. Opći princip metode je da se problem
logično podijeli u više manjih problema, tako da se rješenje dalje može odrediti rješavanjem
jednog od tih "manjih" problema.

17.4.1 Drugi korijen broja
Metodu podijeli pa vladaj primijenit ćemo za približan proračun drugog korijena broja n.

Metoda: Numerički se proračuni mogu provesti samo s ograničenom točnošću. Zbog toga
zadovoljava postupak u kojem se određuje da vrijednost x predstavlja drugi korijen od n, ako je
razlika (n – x2) približno jednaka nuli, odnosno manja od po volji odabrane vrijednosti epsilon.
Točno rješenje se nalazi unutar nekog intervala [d,g]. Primjerice, sigurno je da se rješenje nalazi
u intervalu [0,n] ako je n>1, odnosno u intervalu [0,1] ako je n<1. Interval može biti i uži ako
smo sigurni da obuhvaća točno rješenje.

Do rješenja se dolazi rekurzivno:

Temeljni slučaj:
         Ukoliko se uzme da je vrijednost od x u sredini intervala [d,g], tj. x=(d+g)/2, može se
         prihvatiti da je to zadovoljavajuće rješenje, ako je širina intervala manja od neke po
         volji odabrane vrijednosti epsilon (pr. 0,000001) , tj. ako je je g-d < epsilon.

          Ako je širina intervala veća od epsilon, do rješenje se dolazi koristeći rekurziju
          prema pravilu (2).

Pravilo rekurzije:
          Ako je n < x2 rješenje se traži u intervalu [d, x],
          inače, rješenje se traži u intervalu [x, g].

Implementacija: U programu korijen.c implementirana je i testrirana funkcija
DrugiKorijen(n), koja vraća drugi korijen od n. U toj funkciji se prvo određuje donja i
gornja granica intervala unutar kojega se nalazi rješenje, a zatim se poziva funkcija
korijen_rek(n, d, g) koja obavlja proračun prema prethodnom rekurzivnom algoritmu.
Točnost proračuna se ispituje usporedbom s vrijednošću kojeg vraća standardna funkcija
sqrt(n).




                                                                                               230
/* Program korijen.c */
      #include <stdio.h>
      #include <math.h>

      #define EPSILON 0.000001          /* proizvoljni kriterij točnosti */

      double korijen_rek(double n, double d, double g)
      {
         double x = (d + g)/ 2.0;
         if (g - d < EPSILON)               /* temeljni slučaj   */
            return x;
         else if (n < x*x )                 /* pravilo rekurzije */
            return korijen_rek (n, d, x);
         else
            return korijen_rek (n, x, g);
      }

      double DrugiKorijen(double n)
      {
         double g, d=0.0;
         /* početne granice d=0.0, g=n ili 1 ako je n<1*/
         if(n < 0) n= -n; /* samo za pozitivne vrijednosti */
         if(n>1)   g=n;
         else      g=1.0;
         return korijen_rek(n, d, g);
      }
      int main( int argc, char *argv[])
      {
         int i;
         double n;
         for (i = 1; i < argc; i++)
         {
            sscanf( argv[i], "%lf", &n);
            printf("Drugi korijen(%f) = %f (treba biti %f)n",
                          n, DrugiKorijen(n), sqrt(n));
         }
         return 0;
      }

Nakon poziva programa:

      c:>korijen 5 7.6 3.14

Dobije se ispis:

      Drugi korijen (5.000000) = 2.236068 (treba biti 2.236068)
      Drugi korijen (7.600000) = 2.756810 (treba biti 2.756810)
      Drugi korijen (3.140000) = 1.772004 (treba biti 1.772005)

17.4.2 Binarno pretraživanje niza
    Drugi primjer primjene metode podijeli pa vladaj je traženje elementa sortiranog niza
metodom koja se naziva binarno pretraživanje niza.
Zadatak: Zadan je niz cijelih brojeva a[n] kojem su elementi sortirani od manje prema većoj
vrijednosti, tj.

      a[i-1] < a[i],      za i=1,..n-1




                                                                                       231
Potrebno je odrediti da li se u ovom nizu nalazi element vrijednosti x, i to pomoću funkcije

      int binSearch( int a[],int x, int d, int g);

koja vraća indeks elementa a[i], kojem je vrijednost jednaka traženoj vrijednosti x. Ako
vrijednost od x nije jednaka ni jednom elementu niza, funkcija binSearch() vraća negativnu
vrijednost -1. Cjelobrojne vrijednosti d i g predstavljaju indekse niza koji određuju interval
[d,g] unutar kojeg se vrši traženje.

Metoda: Problem se može riješiti rekurzivno na sljedeći način: Ako u nizu a[i] postoji element
jednak traženoj vrijednosti x, njegov indeks je iz intervala [d,g], gdje mora biti istinito g >= d.
Trivijalni slučaj je za d=0, g=n-1, koji obuhvaća cijeli niz.

Temeljni slučaj:
     Razmatra se element niza indeksa i = (g+d)/2 (dijelimo niz na dva podniza).
     Ako je a[i] jednak x, pronađen je traženi element niza, a funkcija vraća indeks i.

Pravilo rekurzije:
      Ako je a[i] <x rješenje se traži u intervalu [i+1, g], inače je u intervalu [d, i-1].

Implementacija:

      int binSearch( int a[],int x, int d, int g)
      {
         int i;
         if (d > g) return –1;
         i = (d + g)/ 2;
         if (a[i] == x) return i;
         if (a[i] < x) return binSearch( a, x, i + 1, g);
         else           return binSearch( a, x, d, i - 1);
      }

Proces traženja vrijednosti x=23 u nizu od 14 elemenata, ilustriran je na slici 17.3.

             0     1     2     3      4     5     6     7     8     9      10    11    12     13
             1     2     3     5      6     8     9     12    23    26     27    31    34     42
             D                                    i                                           g
             1     2     3     5      6     8     9     12    23    26     27    31    34     42
                                                        d                  i                  g
             1     2     3     5      6     8     9     12    23    26     27    31    34     42
                                                        d     i     g


                         1.korak: d=0,    g=13,    i=6, a[6]<23
                         2.korak: d=i+1=7,g=14,    i=10, a[10]>23
                         3.korak: d=7,    g=i-1=9, i=8, a[8]==23

                               Slika 17.3. Binarno pretraživanje niza


17.5 Pretvorba rekurzije u iteraciju
     Neke rekurzivne funkcije se mogu transformirati u funkcije koje umjesto rekurzije koriste
iteraciju. To su funkcije u kojima je rekurzivni poziv funkcije posljednja naredba u tijelu
funkcije, primjerice




                                                                                                   232
void petlja()
      {
          iskaz ...
          if (e) petlja();
      }

U ovom slučaju, rekurzivni poziv funkcije petlja(), znači da se izvršenje vraća na početak
tijela funkcije, zbog toga se ekvivalentna verzija funkcije može napisati tako da se umjesto
poziva funkcije koristi goto naredba s odredištem na prvu naredbu tijela funkcije, tj.

      void petlja()
      {
         start: iskaz ...
                if (e) goto start;
      }

     Rekurzivni poziv, koji se vrši na kraju (ili na repu) tijela funkcije, često se naziva "repna
rekurzija". (eng. tail recursion). Neki optimizirajući kompilatori mogu prepoznati ovakav oblik
rekurzivne funkcije i transformirati rekurzivno tijelo funkcije u iterativnu petlju. Na taj način
se dobije efikasnija funkcija, jer se ne gubi vrijeme i prostor na izvršnom stogu koji su potrebni
za poziv funkcije.
     Kod većine se rekurzivnih funkcija ne može izvršiti ova transformacije. Primjerice,
funkcija fact() nije repno rekurzivna jer se u posljednjoj naredbi (return fact(n-1)*n;)
najprije vrši poziv funkcije fact(), a zatim naredba množenja. Funkcija binSearch(), koja
je opisana u prethodnom odjeljku, može se transformirati u funkciju s repnom rekurzijom, na
sljedeći način:

      int binSearch( int a[],int x, int d, int g)
      {
         int i;
         if (d > g) return –1;
         i = (d + g)/ 2;
         if (a[i] == x) return i;
         if (a[i] < x) d=i+1;
         else g=i-1
         return binSearch( a, x, d, g);
      }

Dalje se može provesti transformacija u iterativnu funkciju

      int binSearch( int a[],int x, int d, int g)
      {
       int i;
       start: if (d > g) return –1;
              i = (d + g)/ 2;
              if (a[i] == x) return i;
              if (a[i] < x) d=i+1;
              else g=i-1
              goto start;
      }

Može se napisati iterativno tijelo funkcije i u obliku while petlje:

      int binSearch( int a[],int x, int d, int g)
      {
         int i;
         while (d <= g) {




                                                                                              233
i = (d + g)/2;
              if (a[i] == x) return i;
              if (a[i] < x) d=i+1;
              else g=i-1;
          }
          return –1;
      }

    Iterativna verzija se može pojednostaviti u slučaju kada se pretražuje cijeli niz. Tada kao
argument funkcije nije potrebna donja granica indeksa, jer je ona jednaka nuli, a umjesto gornje
granice indeksa, argument je broj elemenata niza n.

      int binSearch( int a[],int x, int n)
      {
         int i, d=0, g=n-1;
         while (d <= g) {
            i = (d + g)/2;
            if (a[i] == x) return i;
            if (a[i] < x) d=i+1;
            else g=i-1;
        }
        return –1;
      }



17.6 Standardna bsearch() funkcija
U standardnoj biblioteci implementirana je polimorfna funkcija bsearch(). Služi za binarno
pretraživanje nizova s proizvoljnim tipom elemenata niza. Deklarirana je na sljedeći način:

      void *bsearch( const void *px, const void *niz,
         size_t n, size_t el_size,
         int (*pCmpFun)(const void *, const void *) );

Prvi parametar funkcije je pokazivač na objekt kojeg se traži. Drugi parametar je pokazivač na
prvi element niza od n elemenata koji zauzimaju el_size bajta. Posljednji parametar je
pokazivač na usporednu funkciju koja je ista kao kod qsort() funkcije. Funkcija bsearch()
vraća pokazivač na element niza ili NULL ako niz ne sadrži traženi objekt.

Realizacija te funkcije, ali pod imenom binsearch(), dana je i            testirana programom
binsearch.c, i to za slučaj da se vrši pretraživanje niza stringova.

      /* Datoteka: binsearch.c */

      #include <stdio.h>
      #include <stdlib.h>
      #include <string.h>

      typedef char *string;

      void * binsearch( const void *px, const void *niz,
                        size_t n, size_t el_size,
                        int (*pCmpFun)(const void *, const void *)                        )
      {
         int i, cmp, d=0, g=n-1;
         char * adr;




                                                                                              234
while (d <= g)
    {
       i = (d + g)/2;

       /* adresa i-tog elementa niza*/
       adr = (char *)niz + i*el_size;

       /*el. niza na toj adresi usporedi s objektom     *px */
       cmp = (*pCmpFun)((void *)adr, (void *)px);

       if (cmp == 0)    return (void *) adr;   /* objekt pronađen */
       if (cmp < 0)     d=i+1;
       else             g=i-1;
    }
    return NULL;
}

int   UsporediStringove( const void *pstr1, const void *pstr2 )
{
       /* Argumenti funkcije su pokazivači na objekte koje
        * usporedjujemo, u ovom slučaju objekt je string (char *).
        * Funkcija strcmp() prima argumente tipa string, stoga
        * treba izvršiti pretvorbu tipa i indirekciju da bi dobili
        * string,kao argument za strcmp()
        * ( str = *(string *) pstr))
        */

      return strcmp( *(string*) pstr1, *(string*)pstr2 );
}

int main( void )
{
     string txt[] = {"Ante", "Ivo", "Marko", "Jure", "Bozo"};
     int numstrings = sizeof(txt)/sizeof(txt[0]);
     string key="Ivo";
     int i, idx;
     string * rez;

      /* sortiraj niz stringova leksikografski */

      qsort((void *)txt, numstrings, sizeof(char*),
      UsporediStringove);

      for(i = 0; i < numstrings; i++ ) puts(txt[i]) ;

      /* pronađi string key */
      rez =(string*) binsearch(&key, txt,
                               numstrings, sizeof(char*),
      UsporediStringove);

      if(rez != NULL)
           printf("Pronadjen string "%s" na adresi %Fpn",
                  *pstr, pstr);
      else
           printf("Nije pronadjen string "%s" n", key);
      return 0 ;
}




                                                                  235
Dobije se ispis:

      Ante Ivo Marko Jure Bozo
      Nakon sortiranja:
      Ante Bozo Ivo Jure Marko
      Pronadjen string "Ivo" na adresi 0022FF48



17.7 Složenost algoritama - "Veliki - O" notacija
     Pri analizi složenosti algoritama uvodi se mjera "dimenzije problema". Primjerice, kada se
obrađuju nizovi, onda dimenziju problema predstavlja duljina niza n, a kada su u pitanju
kvadratne matrice, onda je dimenzija matrice istovremeno i dimenzija problema. Ako je n
dimenzija problema, onda se efikasnost korištenja memorije može opisati funkcijom M(n), a
vrijeme izvršenja algoritma funkcijom T(n).
     Funkcija T(n) se često naziva vremenska složenost algoritma (engl. time complexity), dok
je M(n) prostorna složenost. Funkciji T(n) se pridaje znatno više značaja nego veličini M(n), pa
se pod skraćenim pojmom "složenost" (eng. complexity) obično podrazumijeva vremenska
složenost. Razlog tome je iskustvo u razvoju algoritama, koje je pokazalo da je zauzeće
memorije znatno manji problem od postizanja prihvatljivog vremena obrade.
     Analiza brzine izvršenja programa se radi tako da se odredi ukupan broj operacija koje
dominantno troše procesorsko vrijeme. Primjerice, u programu za zbrajanje elemenata
kvadratne matrice, dimenzija nxn, označen je broj ponavljanja naredbi u kojima se vrši
zbrajanje.

            NAREDBE PROGRAMA                           BROJ PONAVLJANJA
            S = 0 ;                                    .
            for(i=0; i<n; i++)                         n
                for(j=0; j<n; j++)                     n*n
                      S = S + M[i][j]                  n*n

Ako se uzme da prosječno vrijeme izvršenja jedne naredbe iznosi t0, dobije se da ukupno
vrijeme izvršenja algoritma iznosi:

                   T(n) = (2n² + n)*t0

Veličina T(n) je posebno važna za velike vrijednosti od n. U tom slučaju je:

                   T(n) ≤ konst * n²

Ovaj se zaključak u računarskoj znanosti piše u obliku tzv. "veliki-O" notacije:

                   T(n) = O(n²)

i kaže se da je T(n) "veliki O od n²". Funkcija f(n) = n² predstavlja red složenosti algoritma.
Uočite da vrijednost konstantnog faktora ne određuje složenost algoritma već samo faktor koji
ovisi o veličini problema.

   Definicija: Složenost algoritma u "veliki-O" notaciji definira se na sljedeći način:

      T(n) = O( f(n) ),           (čita se: T je veliki-O od f)




                                                                                            236
ako postoje pozitivne konstante C i n0, takove da je 0 ≤ T(n) ≤ C * f(n), za sve vrijednosti n ≥
   n0.

     U prethodnom je primjeru funkcija složenosti određena razmatrajući broj operacija u
kojima se vrši zbrajanje. To ne treba uzeti kao pravilo, već za pojedini problem treba sagledati
koje su operacije dominantne po zauzeću procesorskog vremena. Primjerice, kod metoda
pretraživanja niza to će biti naredbe usporedbe i pridjele vrijednosti.
     Klasifikacija algoritama prema redu funkcije složenosti za najpoznatije klase algoritama
prikazana je u tablici 17.1. Ova je tablica uređena po kriteriju rastuće složenosti algoritama.

                    Tip algoritma                                  f(n)
                    Konstantan                                   const.
                    Logaritamski                                 log2n
                    Linearan                                        N
                    Linearno-logaritamski                        nlog2n
                    Kvadratni                                       n2
                                                                 k
                    Stupanjski                                  n (k>2)
                    Eksponencijalni                             kn (k>1)
                    Faktorijelni                                    n!
                            Tablica 17.1. Red složenosti algoritama

    Pri izradi algoritama cilj je nalaženje rješenja koje je manjeg reda složenosti, a posebno je
značajan rezultat kada se pronađe polinomsko ili logaritamsko rješenje nekog od
eksponencijalnih problema. U nastavku je opisana svaka od navedenih tipova algoritama.
Konstantni algoritmi
    Konstantni algoritmi su klasa algoritama kod kojih je vrijeme rada približno konstantno i ne
ovisi od veličine problema. Primjerice, program koji računa kvadratni korijen realnog broja do
rezultata po pravilu dolazi poslije približno istog vremena rada bez obzira kolika je veličina
broja koji je uzet za ulazni podatak.
Logaritamski algoritmi
    Kod logaritamskog algoritma vrijeme izvršenja programa proporcionalno je (najčešće
binarnom) logaritmu veličine problema. Imajući u vidu sporost porasta logaritamske funkcije
vidi se da se ovdje radi o najefikasnijim i stoga najpopularnijim algoritmima. Tipičan
predstavnik logaritamskih algoritama je binarno pretraživanje sortiranog niza prikazano u
prethodnom odjeljku.
Opća koncepcija logaritamskih algoritama je sljedeća:
       1. Obaviti postupak kojim se veličina problema prepolovi.
       2. Nastaviti razlaganje problema dok se ne dođe do veličine 1.
       3. Obaviti završnu obradu s problemom jedinične veličine.

    Ukupan broj razlaganja k dobije se iz uvjeta: n / 2k = 1, odnosno k=log2 n. Kako je po
pretpostavci broj operacija koji se obavlja pri svakom razlaganju približno isti, to je i vrijeme
rada približno proporcionalno broju razlaganja, odnosno binarnom logaritmu od n.




                                                                                               237
Linearni algoritmi
    Linearni algoritmi se javljaju u svim slučajevima gdje je obradom obuhvaćeno n istovjetnih
podataka i gdje udvostručenje količine radnji ima za posljedicu udvostručenje vremena obrade.
Opći oblik linearnog algoritma može se prikazati u vidu jedne for petlje:

             for (i=0; i < n; i++)
                  {obrada koja traje vrijeme t}

Zanemarujući vrijeme opsluživanja for petlje, u ovom slučaju funkcija složenosti je T(n)=nt,
pa je T(n) = O(n).
    U slučajevima kada nije moguće deterministički odrediti vrijeme izvršavanja sadržaja petlje
može se umjesto vremena rada T(n) statistički odrediti srednji broj naredbi I(n) koje program
obavlja. Tada se podrazumijeva da je T(n)=t1*I(n), gdje je vrijeme izvršavanja prosječne
naredbe t1 = konst.
Budući da vrijedi

             T(n) = O( f(n) ) , I(n) = O( f(n) ),

slijedi da se traženi red funkcije složenosti f(n) može odrediti kako iz T(n), tako i iz I(n).

Primjerice, pronalaženje maksimalne vrijednosti elementa niza ima sljedeću analizu broja
naredbi:

                NAREDBE PROGRAMA                     BROJ PONAVLJANJA
                max = a[0]                           1
                for(i=1; i<n; i++)                   (n-1)
                   if (a[i] > max)                   (n-1)
                       max = a[i]                    (n-1)p (0<=p<=1)



Ovdje p označava vjerojatnost da dođe do ispunjenja uvjeta a[i]>max ( p ne ovisi od n).
Sumirajući broj ponavljanja pojedinih naredbi dobije se da je ukupan broj izvršenih naredbi

             I(n) = 1 + (n-1)(3+p) = (3+ p) n - 1 - p.

U ovom je izrazu dominantan samo prvi član polinoma, pa je f(n) = n, odnosno T(n) = O(n).

Linearno-logaritamski algoritmi
     Linearno-logaritamski algoritmi, složenosti O(n log n) ), spadaju u klasu veoma efikasnih
algoritama jer im složenost, za veliki n, raste sporije od kvadratne funkcije. Primjer za ovakav
algoritam je Quicksort, koji će biti detaljno opisan kasnije. Bitna osobina ovih algoritama je
sljedeća:
 (1) Obaviti pojedinačnu obradu kojom se veličina problema prepolovi.
 (2) Unutar svake polovine sekvencijalno obraditi sve postojeće podatke.
 (3) Nastaviti razlaganje problema dok se ne dođe do veličine 1.

    Slično kao i kod logaritamskih algoritama i ovdje je ukupan broj dijeljenja log2n, ali kako
se nakon svakog dijeljenja sekvencijalno obrade svi podaci, to je ukupan broj elementarnih
obrada jednak nlog2n, i to predstavlja red funkcije složenosti.




                                                                                                 238
Kvadratni i stupanjski algoritmi
    Kvadratni algoritmi, složenosti O(n2), najčešće se dobivaju kada se koriste dvije for petlje
jedna unutar druge. Primjer je dat na početku ovog poglavlja. Stupanjski algoritmi nastaju kod
algoritama koji imaju k umetnutih petlji, pa je složenost O(nk).
Eksponencijalni algoritmi
    Eksponencijalni algoritmi O(kn) spadaju u kategoriju problema za koje se suvremena
računala ne mogu koristiti, izuzev u slučajevima kada su dimenzije takvog problema veoma
male. Jedan od takovih primjera je algoritam koji rekurzivno rješava igru "Kule Hanoja",
opisan u prethodnom poglavlju.
Faktorijelni algoritmi
     Kao primjer faktorijelnih algoritama najčešće se uzima problem trgovačkog putnika.
Problem je formuliran na sljedeći način: zadano je n+1 točaka u prostoru i poznata je udaljenost
između svake dvije točke j i k. Polazeći od jedne točke potrebno je formirati putanju kojom se
obilaze sve točke i vraća u polaznu točku, tako da je ukupni prijeđeni put minimalan.
     Trivijalni algoritam za rješavanje ovog problema mogao bi se temeljiti na uspoređivanju
duljina svih mogućih putanja. Broj mogućih putanja iznosi n!. Polazeći iz početne točke postoji
n putanja do n preostalih točaka. Kada odaberemo jednu od njih i dođemo u prvu točku onda
preostaje n-1 mogućih putanja do druge točke, n-2 putanja do treće točke, itd., n+1-k putanja do
k-te točke, i na kraju samo jedna putanja do n-te točke i natrag u polaznu točku. Naravno da
postoje efikasnije varijante algoritma za rješavanje navedenog problema, ali u opisanom slučaju,
s jednostavnim nabrajanjem i uspoređivanjem duljina n! različitih zatvorenih putanja, algoritam
ima složenost O(n!).


17.8 Sortiranje
     Sortiranje je postupak kojim se neka kolekcija elemenata uređuje tako da se elementi
poredaju po nekom kriteriju. Kod numeričkih nizova red elemenata se obično uređuje od
manjeg prema većim elementima. Kod nizova stringova red se određuje prema leksikografskom
rasporedu. Kod kolekcija strukturnog tipa obično se odabire jedan član strukture kao ključni
element sortiranja. Sortiranje je važno u analizi algoritama, jer su analizom različitih metoda
sortiranja postavljeni neki od temeljnih kompjuterskih algoritama.

    Za analizu metoda sortiranja postavlja se sljedeći problem: odredite algoritam pomoću
kojeg se ulazni niz A[0..n-1], od n elemenata, transformira u niz kojem su elementi poredani u
redu od manjeg prema većem elementu, tj. na izlazu treba biti ispunjeno:

        A[0] ≤ A[1] ≤ A[2] ≤ ... ≤ A[n-2] ≤ A[n-1].
    Najprije će biti analizirane dvije jednostavne metode, selekcijsko sortiranje i sortiranje
umetanjem, kojima je složenost O(n2). Zatim će biti analizirane dvije napredne metode,
quicksort i mergesort, koje koriste metodu "podijeli pa vladaj". Njihova je složenost O(n log2
n). U analizi će biti korištena oznaka A[d..g] za označavanje da se analizira neki niz od indeksa
d do indeksa g. Ovaj način označavanja ne vrijedi u C jeziku.

17.8.1 Selekcijsko sortiranje
Ideja algoritma je:
    1. Pronađi najmanji element i njegov indeks k.
    2. Taj element postavi na početno mjesto u nizu (A[0]), a element koji je dotad postojao
         na indeksu 0 postavi na indeks k.




                                                                                             239
3. Zatim pronađi najmanji element, počevši od indeksa 1. Kada ga pronađeš, zamijeni ga
       s elementom indeksa 1.
    4. Ponovi postupak (3) za sve indekse (2..n-2).

Primjerice za sortirati niz od 6 elemenata: 6, 4, 1, 5, 3 i 2 , treba izvršiti sljedeće operacije:


      6   4   1   5   3   2   ->   min   od   A[0..5] zamijeni     sa   A[0]
      1   4   6   5   3   2   ->   min   od   A[1..5] zamijeni     sa   A[1]
      1   2   6   5   3   4   ->   min   od   A[2..5] zamijeni     sa   A[2]
      1   2   3   5   6   4   ->   min   od   A[3..5] zamijeni     sa   A[3]
      1   2   3   4   6   5   ->   min   od   A[4..5] zamijeni     sa   A[4]
      1   2   3   4   5   6   ->   niz   je   sortiran


Ovaj algoritam se može implementirati pomoću for petlje:

      for (i = 0; i < n-1; i++)
      {
          imin = indeks najmanjeg elementa u A[i..n-1];
          Zamijeni A[i] sa A[imin];
      }

Uočite da se petlja izvršava dok je i < n-1, a ne za i < n, jer ako A[0..n-2] sadrži n-1 najmanjih
elemenata, tada posljednji element mora biti najveći, i on se nalazi na ispravnom položaju , tj.
A[n-1].
Indeks najmanjeg elementa u A[i..n-1] pronalazi se sa:

      imin = i;
      for (j = i+1; j < n; j++)
         if (A[j] < A[min]) imin=j;

Zamjenu vrijednosti vrši funkcijom swap();

      /* Zamjena vrijednosti dva int */
      void swap(int *a, int *b)
      {
        int t = *a;
        *a = *b;
        *b = t;
      }

Sada se može napisati funkcija za selekcijsko sortiranje, niza A koji ima n elemenata, u obliku:

      void selectionSort(int *A, int n)
      {
        int i, j, imin;   /* indeks najmanjeg elementa u A[i..n-1] */

          for (i = 0; i < n-1; i++) {
            /* Odredi najmanji element u               A[i..n-1]. */
            imin = i;                 /*               pretpostavi da je to A[i] */
            for (j = i+1; j < n; j++)
              if (A[j] < A[imin])     /*               ako je A[j] najmanji */
                imin = j;             /*               zapamti njegov indeks */

              /* Sada je A[imin] najmanji element od A[i..n-1], */
              /* njega zamjenjujemo sa A[i]. */




                                                                                                     240
swap(&A[i], &A[imin]);
          }
      }

Analiza selekcijskog sortiranja
Svaka iteracija vanjske petlje (indeks i) traje konstantno vrijeme t1 plus vrijeme izvršenja
unutarnje petlje (indeks j). Svaka iteracija u unutarnjoj petlji traje konstantno vrijeme t2 .
Broj iteracija unutarnje petlje ovisi u kojoj se iteraciji nalazi vanjska petlja:

                                               Broj operacija u
                                   i
                                               unutarnjoj petlji
                                   0            n-1
                                   1            n-2
                                   2            n-3
                                   ...          ...
                                   n-2          1

Ukupno vrijeme je:

      T(n) = [t1 + (n-1) t2] + [t1 + (n-2) t2] + [t1 + (n-3) t2] + ... + [t1 + (1) t2]

odnosno, grupirajući članove u oblik t1 ( …) + (...) t2 dobije se

      T(n) = (n-1) t1 + [ (n-1) + (n-2) + (n-3) + ... + 1 ] t2

Izraz u uglatim zagradama predstavlja sumu aritmetičkog niza

      1 + 2 + 3 + ... + (n-1) = (n-1)n/2 = (n2-n)/2,

pa je ukupno vrijeme jednako:

      T(n) = (n-1) t1 + [(n2-n)/2] t2 = - t1 + t1 n - t2 n/2 + t2 n2/2

Očito je da dominira član sa n2 , pa je složenost selekcijskog sortiranja jednaka O(n2) .

17.8.2 Sortiranje umetanjem
Algoritam sortiranja umetanjem (eng. insertion sort) se temelji na postupku koji je sličan načinu
kako se slažu igraće karte. Algoritam se vrši u u n-1 korak. U svakom koraku se umeće i-ti
element u dio niza koji mu prethodi (A[0..i-1]), tako taj niz bude sortiran. Primjerice, sortiranje
niza od n=6 brojeva izgleda ovako:


      6   4   1   5   3   2   ->   ako   je   A[1]< A[0],     umetni     A[1]    u   A[0..0]
      4   6   1   5   3   2   ->   ako   je   A[2]< A[1],     umetni     A[2]    u   A[0..1]
      1   4   6   5   3   2   ->   ako   je   A[3]< A[2],     umetni     A[3]    u   A[0..2]
      1   4   5   6   3   2   ->   ako   je   A[4]< A[3],     umetni     A[4]    u   A[0..3]
      1   3   4   5   6   2   ->   ako   je   A[5]< A[4],     umetni     A[5]    u   A[0..4]
      1   2   3   4   5   6   ->   niz   je   sortiran


Algoritam se može zapisati pseudokôdom:

          for (i = 1; i < n; i++)
          {




                                                                                               241
x = A[i];
              analiziraj elemente A[0 .. i-1]
              počevši od indeksa j=i-1, do indeka j=0
                 ako je x < A[j] tada
                     pomakni element A[j] na mjesto A[j+1]
                 inače prekini
              zatim umetni x na mjesto A[j+1]
          }


pa se dobije funkcija:

      void insertionSort(int *A, int n)
      {
       int i, j, x;

          for (i = 1; i < n; i++) {
             x = A[i];
             for(j = i-1; j >= 0; j--) {
                 if(x < A[j])
                     A[j+1] = A[j];
                else
                    break;
             }
             A[j+1] = x;
          }
      }


Analiza složenosti metode sortiranja umetanjem
Sortiranje umetanjem je primjer algoritma u kojem prosječno vrijeme izvršenja nije puno kraće
od vremena izvršenja koje se postiže u najgorem slučaju.

Najgori slučaj
Vanjska petlja se izvršava u n-1 iteracija, što daje O(n) iteracija. U unutarnjoj petlji se vrši od
0 do i<n iteracija, u najgorem slučaju vrši se također O(n) iteracija. To znači da se u najgorem
slučaju vrši O(n) ⋅O(n) operacija, dakle složenost je O(n2).

Najbolji slučaj
U najboljem slučaju u unutarnjoj petlji se vrši 0 iteracija. To nastupa kada je niz sortiran. Tada
je složenost O(n). Može se uzeti da to vrijedi i kada je niz "većim dijelom sortiran" jer se tada
rijetko izvršava unutarnja petlja.

Prosječni slučaj
U prosječnom se slučaju vrši n/2 iteracija u unutarnjoj petlji. To također daje složenost
unutarnje petlje O(n), pa je ukupna složenost: O(n2).

17.8.3 Sortiranje spajanjem sortiranih podnizova (merge sort)
Sortiranje metodom spajanja sortiranih podnizova (eng. merge sort) temelji se na ideji da se niz
rekurzivno dijeli na dva sortirana niza, te da se zatim izvrši spajanje tih sortiranih nizova.

Problem će biti riješen za slučaj da se sortira niz A[d..g], tj. od donjeg indeksa d do gornjeg
indeksa g, funkcijom

      void mergeSort(int *A, int d, int g);




                                                                                               242
Rekurzivnom se podjelom niza u dva podniza, A[d..s] i A[s+1,g], koji su otprilike podjednake
veličine (indeks s se odredi kao srednja vrijednost s = (d+g)/2), dolazi se do temeljnog slučaja
kada u svakom nizu ima samo jedan element. Taj jedno-elementni niz je već sortiran, pa se pri
"izvlačenju" iz rekurzije može vršiti spajanje sortiranih podnizova. Ovaj postupak je ilustriran
na slici 4.




                  Slika 17. 4. Prikaz sortiranja spajanjem sortiranih podnizova

Za implementaciju ovog algoritma bitno je uočiti sljedeće:

    •    Ulazni niz nije nužno "fizikalno" dijeliti na podnizove, jer se podnizovi ne preklapaju.
         Dovoljno je zapamtiti indekse ulaznog niza koji određuju neki podniz.
    •    Spajanje podnizova se uvijek provodi s elementima koji su u ulaznom nizu poredani
         jedan do drugog; prvi podniz je A[d..s], a drugi podniz je A[s+1..g]. U tu svrhu koristit
         će se funkcija:

        void merge(int *A, int d, int s, int g)
        /* Ulaz: dva sortirana niza A[d..s] i A[s+1..g]                    */
        /* Izlaz: sortirani niz A[d..g]                                    */


Očito je da se radi o algoritmu tipa "podijeli pa vladaj":
    1. Podijeli: podijeli niz A[d,g], na način da dva podniza A[d,s] i A[s+1,g] sadrže otprilike
        pojednak broj elemenata. To se postiže izborom: s=(d+g)/2.
    2. Vladaj: rekurzivno nastavi dijeliti oba podniza sve dok njihova veličina ne postane
        manja od 2 elementa (niz koji sadrži nula ili jedan element je sortirani niz).
    3. Spoji: Nakon toga, pri "izvlačenju" iz rekurzije, izvrši spajanje sortiranih nizova
        koristeći funkciju merge(A,d,s,g).

Implementacija ovog algoritma je jednostavna;

        void mergeSort(int *A, int d,            int g)
        {
           if (d < r ) {                         /*   temeljni slucaj - 1 element */
              int s = (d + g) / 2;               /*   s je indeks podjele niza     */
              mergeSort(A, d, s);                /*   rekurzivno podijeli A[d..s] */
              mergeSort(A, s+1, g);              /*   rekurzivno podijeli A[s+1..g]*/
              merge(A, d, s, g);                 /*   zatim spoji sortirane nizove */




                                                                                               243
}
      }

Još treba definirati funkciju merge(). Ona se može realizirati na način da se formiraju dva
pomoćna niza, donji[] i gornji[], u koje se kopira sortirane nizove A[d..s] i A[s+1..g].
Zatim se iz tih pomoćnih sortiranih nizova formira jedan sortirani niz u području ulaznog niza
A[d..g]. Postupak je ilustriran na slici 5.




                              Slika 17.5. Spajanje sortiranih nizova

Implementacija je sljedeća:

/* Spajanje podnizove A[d..s] i A[s+1..g] u sortirani niz                     A[d..g]. */

void merge(int *A, int d, int s, int g)
{
  int m = s - d + 1;                  /*                broj elemenata u A[d..s] */
  int n = g - s;                      /*                broj elemenata u A[s+1..g] */
  int i;                              /*                indeks u donji niz*/
  int j;                              /*                indeks u gornji niz*/
  int k;                                                /* indeks u orig. niz A */
  int *donji = malloc(sizeof(int) * m);                 /* niz A[d..s] */
  int *gornji = malloc(sizeof(int) * n);                /* niz A[s+1..g] */

  /* Kopiraj A[d..s] u donji[0..m-1] i A[s+1..g] u gornji[0..n-1]. */
  for (i = 0, k = d; i < m; i++, k++)
    donji[i] = A[k];
  for (j = 0, k = s+1; j < n; j++, k++)
    gornji[j] = A[k];

  /* Usporedbom donji[0..m-1] i gornji[0..n-1], */
  /* pomakni manji element na sljedeću poziciju u A[d..g]. */
  i = 0; j = 0; k = d;
  while(i < m && j < n; )
    if (donji[i] < gornji[j])
      A[k++] = donji[i++];
    else
      A[k++] = gornji[j++];

  /* Preostale elemente jednostavno kopiraj */
  /* Jedna od ove dvije petlje će imati nula iteracija! */
  while (i < m)
    A[k++] = donji[i++];
  while (j < n)
    A[k++] = gornji[j++];




                                                                                          244
/* Dealociraj memoriju koju zauzimaju donji i gornji. */
    free(donji);
    free(gornji);
}

Operacija kopiranja iz pomoćnih nizova u sortirani niz A[d..g] provodi se jednostavnom
usporedbom sadržaja donji[i] i gornji[j]. Kopira se manji element u A i inkrementira pozicija u
nizu. Na taj način, ova se operacija vrši u linearnom vremenu O(g-d+1).
Pomoćni nizovi su formirani alociranjem memorije, stoga se na kraju funkcije vrši oslobađanje
memorije. U realnoj se primjeni može koristiti brži postupak, bez alociranja memorije, na način
da se pomoćni nizovi deklariraju kao globalne varijable. Dimenzija ovih globalnih nizova mora
biti veća od polovine dimenzije niza koji se sortira. Na sličan način se može provesti i sortiranje
datoteka.
Složenost metode spajanja podnizova
Može se na jednostavan način pokazati da je vremenska složenost ovog algoritma jednaka O(n
log2 n), ukoliko se uzme da je veličina niza potencija broja 2, tj. da je n = 2m. Pošto se pri
svakom rekurzivnom pozivu niz dijeli na dva podniza, sve dok duljina podniza ne postane
jednaka 1, proizlazi da je broj razina podijele niza jednak log2 n. Na k-toj razini niz je podijeljen
na 2k podnizova duljine n/2k. To znači da spajanje sortiranih nizova na k-toj razini ima složenost
2kxO(n/2k)= O(n), a pošto ima log2 n razina, proizlazi da je ukupna složenost jednaka O(n log2
n). Do istog rezultata se dolazi i uz znatno rigorozniju analizu složenosti.
Može se pokazati da je ovo najbolji rezultat koji se može postići pri sortiranju nizova. Jedini
problem ovog algoritma je što zahtijeva povećanu prostornu složenost.

17.8.1 Quicksort
Quicksort je najčešće korištena metoda sortiranja. Njome se u prosječnom slučaju postiže
složenost O(n log2 n), a u najgorem slučaju O(n2). To je lošiji rezultat nego kod mergesort
metode, međutim prostorna složenost je manja nego kod mergesort metode, jer se sve operacije
vrše na ulaznom nizu.

Quicksort algoritam koristi metodu podijeli pa vladaj u sljedećem obliku:

    Algoritam: Sortiraj niz A[d..g]

    PODIJELI. Izaberi jedan element iz niza A[d..g] i zapamti njegovu vrijednost. Taj element
    se naziva pivot. Nakon toga podijeli A[d..g] u dva podniza A[d..p] i A[g+1..d] koji imaju
    slijedeća svojstva:
           Svaki element A[d..p] je manji ili jednak pivotu.
           Svaki element A[p+1..g] je veći ili jednak pivotu.
           Niti jedan podniz ne sadrži sve elemente (odnosno ne smije biti prazan).

    VLADAJ. Rekurzivno sortiraj oba podniza A[d..p] i A[p+1..g], i problem će biti riješen
    kada oba podniza budu imala manje od 2 elementa.

Uvjet da nijedan podniz ne bude prazan je potreban jer, kada to ne bi bilo ispunjeno, rekurzivni
problem bi bio isti kao originalni problem, pa bi nastala bekonačna rekurzija.

Podjela na podnizove, tako da budu ispunjeni postavljeni uvjeti, vrši se funkcijom

       int podijeli(int *A, int d, int g)

koja vraća indeks podjele na podnizove.




                                                                                                 245
Podjela se vrši pomoću dva indeksa (i, j) i pivota koji se proizvoljno odabire, prema pravilu:
    • Pomakni i od početka prema kraju niza, dok se ne nađe element A[i] koji je veći ili
        jednak pivotu,
    • Pomakni j od kraja prema početku niza, dok se ne nađe element A[j] koji je manji ili
        jednak pivotu,
    • Zatim zamijeni vrijednosti A[i] i A[j], kako bi svaki svaki element A[d..i] bio manji ili
        jednak pivotu, a svaki element A[j..g] veći ili jednak pivotu.
Ovaj se proces nastavlja dok se ne dobije da je i > j. Tada je podjela završena, a j označava
indeks koji vraća funkcija. Ovaj uvjet ujedno osigurava da nijedan podniz neće biti prazan.
Postupak je ilustriran na slici 17.6.




                                       Slika 17.6. Postupak podjele niza A[1..6]. Za pivot je
                                      odabran prvi element A[1] vrijednosti 6. Označeni su
                                       indeksi (i,j), a potamnjeni elementi pokazuju koje se
                                    elemente zamjenjuje. Linija podjela je prikazana u slučaju
                                              kada indeks i postane veći od indeksa j.


      /* Podijeli A[d..g] u podnizove A[d..p] i A[p+1..g],
       * gdje je d <= p < g, i
       *          A[d..p] <= A[p+1..g].
       * Početno se izabire A[d] kao pivot
       * Vraća indeks podjele.
       */

      int podijeli(int *A, int d, int g)
      {
        int i = d - 1;               /* index lijeve strane A[d..g] */
      int j = g + 1;              /* index desne strane A[d..g] */
        int pivot = A[d];            /* izbor pivot-a               */

         while (1) {
           /* Nadji sljedeći manji indeks j za koji je A[j] <= pivot. */
           while (A[--j] > pivot)
           {}

           /* Nadji sljedeći veći indeks i za koji je A[i] >= pivot. */
           while (A[++i] < pivot)
           {}

           /* Ako je i >= j, raspored je OK.              */




                                                                                           246
/* inače, zamijeni A[i] sa A[j] i nastavi. */

           if (i < j)
             swap(&A[i], &A[j]);
           else
             return j;
           }
      }


Pomoću ove funkcije se iskazuje kompletni quicksort algoritam:


      /* Sortiraj niz A[d..g] - quicksort algoritmom
       * 1.Podijeli A[d..g] u podnizove A[d..p] i A[p+1..g],
       *     d <= p < g,
       *     i svaki element A[d..p] je <= od elementa A[p+1..g].
       * 2.Zatim rekurzivno sortiraj A[d..p] i A[p+1..g].
       */

      void quicksort(int *A, int d, int g)
      {
         if (d < g) /* završava kada podniz ima manje od 2 elementa */
         {
            int p = podijeli(A, d, g); /* p je indeks podjele        */
            quicksort(A, d, p);        /* rekurzivno sortiraj A[d..p]*/
            quicksort(A, p+1, g);    /* rekurzivno sortiraj A[p+1..g]*/

          }
      }


Analiza složenosti quicksort algoritma
    Prvo treba uočiti da je vrijeme za podjelu podnizova od n elemenata, proporcionalno broju
elemenata cn (konstanta c > 0), tj. složenost je O(n). Analizirajmo zatim broj mogućih
rekurzivnih poziva.

Najbolji slučaj
U najboljem slučaju nizovi se dijele na dvije jednake polovine. Pošto je tada ukupan broj
razlaganja jednak log2 n dobije se da je složenost O(n)⋅O(log2 n) = O(n log2 n).

Najgori slučaj
U najgorem slučaju podjela na podnizove se vrši tako da je u jednom podnizu uvijek samo jedan
element. To znači da bi tada nastaje n rekurzivnih poziva, pa je ukupna složenost O(n) ⋅O(n)=
O(n2). Najgori slučaj nastupa kada je niz sortiran ili "skoro" sortiran.

Prosječni slučaj
Analiza prosječnog slučaja je dosta komplicirana. Ovdje se neće izvoditi. Važan je zaključak da
se u prosječnom slučaju dobija složenost koja je bliska najboljem slučaju O(n log2 n).

Postavlja se pitanje: Kako odabrati pivot element?
     U prethodnom algoritmu za pivota je odabran početni element A[d]. U praksi se pokazalo
da je znatno povoljnije izbor pivota vršiti po nekom slučajnom uzorku. Recimo, ako se slučajno




                                                                                           247
odabere indeks ix i element A[ix] kao pivot, tada se smanjuje mogućnost da se dobije
nepovoljna podjela kod skoro sortiranih nizova.
     Sljedećim postupkom se dobiva vjerojatnost dobre podjele 50%. Odaberu se tri indeksa po
slučajnom uzorku, a zatim se za pivot odabere jedan od ova tri elementa koji je najbliži njihovoj
srednjoj vrijednosti (medijan). Dublje opravdanje ove metode ovdje neće biti dano. Pokazalo se,
da uz primjenu takovog postupka, quicksort predstavlja najbrži poznati način sortiranja nizova,
pa je implementiran u standardnoj biblioteci C jezika.


17.9 Zaključak
Glavna korist od korištenja rekurzije kao programske tehnike je:

    •   rekurzivne funkcije su jasnije, jednostavnije, kraće, i lakše ih je razumjeti od
        odgovarajućih ne-rekurzivnih funkcija, jer najčešće neposredno odgovaraju apstraktnom
        algoritmu rješenja problema.

Glavni nedostatak korištenja rekurzije kao programske tehnike je:
   • izvršenje rekurzivnih funkcija troši više računarskih resursa od odgovarajućih ne-
        rekurzivnih funkcija.

U mnogo slučajeva rekurzivne se funkcije mogu transformirati u ne-rekurzivne funkcije. To se
obavlja jednostavno u slučaju korištenja “repne rekurzije”.

Analiza složenosti algoritama se uglavnom provodi analizom vremena izvršenja programa.
Složenost algoritma se iskazuje “veliki-O” notacijom.

U analizi složenosti algoritama često se koristi princip matematičke indukcije. On se postavlja
na sličan način kao i rekurzivna pravila: definira se temeljni slučaj i pravilo indukcije
(rekurzije).

Kao primjer analize algoritama, analizirano je nekoliko metoda za sortiranje nizova. Pokazano
je da se korištenjem metode “podijeli pa vladaj” dobivaju efikasni algoritmi (quicksort i
mergesort).




                                                                                             248
18 Samoreferentne strukture i liste


Naglasci:
   • samoreferentne strukture
   • jednostruko vezana lista
   • dvostruko vezana lista
   • operacije s listom
   • sortirane liste
   • implementacija ADT STACK pomoću jednostruko vezane liste
   • implementacija ADT QUEUE pomoću jednostruko vezane liste
   • implementacija ADT DEQUEUE pomoću dvostruko vezane liste


18.1 Samoreferentne strukture i lista
     Karakteristika je strukturnih tipova da članovi strukture mogu biti bilo kojeg prethodno
definiranog tipa. To znači da struktura ne može sadržavati "samu sebe", jer se ona tek definira.
Dozvoljeno je pak da struktura sadrži pokazivač na "sebe". Takve strukture se nazivaju
samoreferentne ili rekurzivne strukture.
     Razmotrimo strukturu node koja pored nekog elementa (elem) sadrži pokazivač (next)
koji se može inicirati da pokazuje na objekt istog strukturnog tipa.

      struct node
      {
         int elem;
         struct node * next;
      }


Primjerice, neka su definirama tri objekta (N1, N2, N3) i pokazivač List, tipa structnode:
struct node N1, N2, N3;

      struct node *List;

tada je moguće inicirati pokazivač List tako da pokazuje na bilo koji od ova tri objekta. Uzet
ćemo da pokazivač List pokazuje na objekt N1:

      List = &N1;

Pošto objekti N1, N2 i N3 sadrže član next, koji je pokazivač tipa struct node, može se
uspostaviti veza između objekata, tako da se pokazivaču next jednog objekta pridijeli adresa
drugog objekta. Primjerice, sljedećim naredbama se definira sadržaj i veza sa drugim objektom.

      N1.elem    =   1;
      N2.elem    =   2;
      N3.elem    =   3;
      N1.next    =   &N2;     /* prvi povezujemo s drugim */
      N2.next    =   &N3;     /* drugi povezujemo s trećim */
      N3.next    =   NULL;    /* treći element ostavimo nepovezanim */




                                                                                            249
To stanje se može ilustrirati sljedećom slikom:




                                     Slika 18.1 Vezana lista

Ovakova struktura se naziva vezana lista. Sastoji se od međusobno povezanih objekata koji se
nazivaju čvorovi (eng. node), jer ilustracija prikazuje graf koji se sastoji od čvorova i
usmjerenih veza. Prvi čvor vezane liste se obično naziva glava liste. Njegovu adresu se bilježi u
pokazivaču List. Posljednji čvor se naziva kraj liste. Pokazivač next tog čvora se uvijek
postavlja na vrijednost NULL, što služi kao oznaka kraja liste.

      Strukturalna definicija: Vezana lista skup elemenata, koji je realiziran na način
      da je svaki element dio čvora, koji sadrži i vezu sa sljedećim čvorom. Prazna lista
      ne sadrži ni jedan čvor.


     Do sada je pojam liste korišten za označavanje uređenog skupa elemenata A={a1,a2,...an}.
Uređenost skupa je određena redom elemenata liste. Pojam liste se sada koristi i za označavanje
strukture podataka koju se može definirati rekurzivno:


      Apstraktna definicija: Lista je linearna struktura podataka koja sadrži nula ili
      više elemenata, a ima dvije karakteristike:
             1. Lista može biti prazna
             2. Lista se sastoji od elementa liste iza kojeg slijedi lista elemenata


     Prema ovoj definiciji lista se može realizirati kao niz elemenata ili kao vezana lista. Listi,
koja se formira kao niz elemenata, jednostavno se pristupa elementima liste pomoću indeksa
niza. Znatno je kompliciraniji postupak ako treba umetnuti ili izbrisati element liste. Bit će
pokazano da se te operacije, pri korištenju vezane liste, izvode brzo i jednostavno. Još je jedna
bitna razlika između niza i vezane liste. Nizovi zauzimaju fiksnu veličinu memorije, a veličina
vezane liste (i zauzeće memorije), je određena brojem elemenata liste. Ako se umetne element,
vezana lista se smanjuje, a ako se izbriše element vezana lista se smanjuje.
     Zbog ovog svojstva vezana lista je dinamička struktura podataka, a zbog svojstva da služi
prikupljanju podataka često se kaže da predstavlja kolekciju elemenata.


18.2 Operacije s vezanom listom
     Cilj je pokazati kako se vrše operacije s vezanom listom kao dinamičkom strukturom
podataka. Najprije će biti pokazano kako se formira lista cijelih brojeva. Polazi se od definirane
strukture “node”, koja sadrži jedan element kolekcije i vezu kojom se pristupa elementima
kolekcije, u sljedećem obliku:




                                                                                               250
typedef int elemT;
      typedef struct node         Node;
      typedef Node *LIST;

      struct node
      {
           elemT elem;          /* element liste */
           Node *next;          /* pokazuje na slijedeći čvor */
      }

Tip Node označava čvor liste, a tip LIST označava pokazivač na čvor liste. Deklaracija

      LIST List = NULL;

inicijalizira varijablu List koja, prema apstraktnoj definiciji, predstavlja praznu listu, a
programski predstavlja pokazivač koji na čvor koji sadrži glavu liste. Zvat ćemo ga pokazivač
liste.
   Proces stvaranja vezane liste počinje stvaranjem čvora vezane liste. Formira se alociranjem
memorije za strukturu node.

List = malloc(sizeof (Node));

Nakon toga se upisuje vrijednost elementa liste, primjerice

List->elem = 5;

Upitnik označava da nije definiran sadržaj pokazivača List->next. Uobičajeno je da se on
postavi na vrijednost NULL, jer pokazuje na “ništa”. Time se ujedno označava kraj liste (po
analogiju sa stringovima, gdje znak '0' označava kraj stringa). To simbolički prikazujemo
slikom:

List->next = NULL;

Sada se u ovu kolekciju može dodati još jedan element, sljedećim postupkom. Prvo, se formira
čvor kojem pristupamo pomoću pokazivača q.

Node *q=malloc(sizeof(Node));
q->elem =7;



Zatim se ova dva čvora “povežu” sljedećim naredbama:



      q->next = List;




                                                                                          251
List = q;




      Novi element je postavljen na početak liste. Pokazivač q više nije potreban jer on sada ima
istu vrijednost kao pokazivač List (q se koristi samo kao pomoćni pokazivač za formiranje
vezane liste). Na ovaj se način može formirati lista s proizvoljnim brojem članova. Početak liste
(ili glava liste) je zabilježen u pokazivaču kojeg se tradicionalno naziva List. Kraj liste se je
obilježen s NULL vrijednošću “next” pokazivača krajnjeg čvora liste.
      U ovom primjeru lista je formirana tako da se novi čvor postavlja na glavu liste. Kasnije će
biti pokazano kako se novi čvor može dodati na kraj liste ili umetnuti između dva čvora liste.
      Sada će postupak formiranja i manipuliranja s listom biti opisan s nekoliko temeljnih
funkcija, koje se mogu lako prilagoditi različitim tipovima elementa liste. Imena funkcija koje
su neovisne o tipu elementa liste bit će zapisana malim slovima. Ostale funkcije, čija imena
sadrže velika slova, ovise o tipu elementa liste. Kasnije će biti pokazano kako se te funkcije
prilagođavaju tipu elementa liste.

18.2.1 Formiranje čvora liste
     U radu s listom, za formiranje novog čvora liste koristit će se funkcija newNode().Ona
prima argument tipa elemT, a vraća pokazivač na alocirani čvor ili NULL ako se ne može
izvršiti alociranje memorije.

      Node *newNode(elemT x)
      {
          Node *n = malloc(sizeof(Node));
          if(n != NULL) n->elem = x;
          return n;
      }

Također, potrebno je definirati funkciju freeNode(), kojom se oslobađa memorija koju
zauzima čvor.

      void freeNode(Node *p)
      {
         /* ako se element liste formira alociranjem memorije
          * tada ovdje treba dodati naredbu za dealociranje
          * elementa liste.
          */
          free(p); /* dealociraj čvor liste */
      }

Uočite da implementacija ove funkcije ovisi o definiciji elementa liste. Ako se element liste
formira alociranjem memorije, primjerice, ako je element liste dinamički string, tada u ovu
funkciju treba dodati naredbu za dealociranje elementa liste.

18.2.2 Dodavanje elementa na glavi liste
    Prethodno opisani postupak formiranja liste, na način da se novi čvor umeće na glavu liste,
implementiran je u funkciji koja se naziva add_front_node().

      void add_front_node(LIST *pList, Node *n)
      {
         if(n != NULL) /* izvrši samo ako je alociran element */
         {




                                                                                              252
n->next = *pList;
                *pList = n;
           }
       }

     Prvi argument funkcije je pokazivač na pokazivač liste. Ovakvi prijenos argumenta je
nužan jer se u funkciji mijenja vrijednost pokazivača liste. Drugi argument je pokazivač čvora
koji se unosi u listu.
Dodavanje elementa x u listu List sada se vrši jednostavnim pozivom funkcije:

               add_front_node(&List, newNode(x));

Složenost ove operacije je O(1).

18.2.3 Brisanje čvora na glavi liste
     Brisanje čvora glave liste je jednostavna operacija. Njome čvor, koji slijedi iza glave liste,
(List->next) postaje glava liste, a trenutni čvor glave liste (n) se dealocira iz memorije. To
ilustrira slika 18.2.



      Node *n = List;
      List = List->next;
      freeNode(n);



                                   Slika 18.2 Brisanje s glave liste

Postupak brisanja glave liste se formalizira funkcijom delete_front_node().

       void delete_front_node(LIST *pList)
       {
          Node *n = *pList;
          if(n != NULL) {
              *pList = *pList->next;
              freeNode(n);
          }
       }

Uočite da se i u ovoj funkciji mijenja vrijednost pokazivača glave liste, stoga se u funkciju
prenosi pokazivač na pokazivač liste. Složenost ove operacije je O(1).

18.2.4 Brisanje cijele liste
Brisanje cijele liste se vrši funkcijom delete_all_nodes(), na način da se brišu svi čvorovi
sprijeda.

       void delete_all_nodes( LIST *pList )
       {
             while (*pList != NULL)
                  delete_front_node(pList);
        }




                                                                                               253
18.2.5 Obilazak liste
   Ako je poznat pokazivač liste uvijek se može odrediti pokazivač na sljedeći element
pomoću “next” pokazivača.

       Node *ptr = List->next;

Dalje se sukcesivno može usmjeravati pokazivač ptr na sljedeći element liste naredbom

       ptr = ptr->next;

Na taj se način može pristupiti svim elementima liste. Taj postupak se zove obilazak liste (eng.
list traversing). Obilazak liste završava kada je ptr == NULL.

Primjer: Pretpostavimo da želimo ispisati sadržaj liste redoslijedom od glave prema kraju liste.
To možemo ostvariti sljedećim programom:

       Node *p = List;              /*           koristi pomoćni pokazivač p */
       while (p != NULL)            /*           ponavljaj do kraja liste    */
       {
           printf("%dn", p->elem); /*           ispiši sadržaj elementa               */
           p = p->next;             /*           i postavi pokazivač na                */
                                    /*           sljedeći element liste                */
       }

     U ovom primjeru na sve elemente liste je primijenjena ista funkcija. Postupak kojim se na
sve elemente liste djeluje nekom funkcijom može se poopćiti funkcijom list_for_each() u
sljedećem obliku:

       void list_for_each(LIST L, void (*pfun)(Node *))
       {
           while( L != NULL) {
               (*pfun)(L);
               L = L->next;
           }
       }

Prvi argument ove funkcije je lista, a drugi argument ove funkcije je pokazivač na funkciju,
koja se primijenjuje na sve elemente liste. To može biti bilo koja void funkcija kojoj je
argument tipa Node *. Definiramo li funkciju:

       void printNode(Node *n) { printf("%dn", n->elem);}

tada poziv funkcije:

       list_for each(L, printNode);

ispisuje sadržaj cijele liste
     Kada je potrebno izvršiti obilazak liste od kraja prema glavi liste, ne može se primijeniti
iterativni postupak. U tom slučaju              se      može koristiti rekurzivna funkcija
reverse_list_for_each().

       void reverse_list_for_each(LIST L, void (*pfun)(Node *))
       {
           if (L == NULL) return;
           reverse_list_for_each(L->next, pfun);
           (*pfun)(L);




                                                                                            254
}

Obilazak liste je nužan i kada treba odrediti posljednji čvor liste. To vrši funkcija
last_node().

      Node *last_node(LIST L)
      {/*vraća pokazivač na krajnji čvor liste*/
          if(L == NULL) return NULL;
          while ( L->next != NULL)
              L = L->next;
          return L;
      }


Broj elemenata koji sadrži lista daje funkcija num_list_elements().

      int num_list_elements(LIST L)
      {
          int num = 0;
          while ( L != NULL) {
              num++;
              L = L->next;
          }
          return num;        /* broj elemenata liste */
      }

18.2.6 Traženje elementa liste
    Često je razlog za obilazak liste traženje elementa liste. U slučaju kada je element liste
prostog skalarnog tipa može se koristiti funkciju find_list_element().

      /* funkcija: find_list_element
       * ispituje da li lista sadrži element x
       * Argumenti:
       *    x – element kojeg se traži
       *    List – pokazivač na glavu liste
       * Vraća:
       *    pokazivač na čvor koji sadrži x, ili NULL ako x nije u listi
       */

      Node *find_list_element(LIST L, elemT x)
      {
          while( L != NULL && p->elem != x )
              L = L->next;
          return L;
      }

Pretraživanje liste ima složenost O(n), gdje je n broj elemenata liste.

18.2.7 Dodavanje čvora na kraju liste
Dodavanje čvora na kraju liste vrši se na sljedeći način:
     Ako lista još nije formirana, tj. ako je List==NULL,
           koristi se postupak opisan u funkciji add_front_node().
     Ako je List != NULL, tada
         1. Obilaskom liste odredi se pokazivač krajnjeg čvora liste. Taj pokazivač,
             nazovimo ga p, ima karakteristiku da je p->next == NULL.




                                                                                          255
2. Zatim se p->next postavi na vrijednost pokazivača čvora kojeg se dodaje u
             listu.
          3. Pošto dodani čvor predstavlja novi kraj liste njega se zaključuje s NULL .

Ovaj postupak je realiziran funkcijom add_back_node();

      /* funkcija: add_back_node
       * ---------------------
       * Dodaje čvor na kraj liste
       * Argumenti:
       *   pList - pokazivač na pokazivač liste
       *   n – pokazivač na čvor koji se dodaje u listu
       */
      void add_back_node(LIST *pList, Node *n)
      {
         if(n == NULL)              /* Izvršava se samo ako je                             */
              return;               /* alociran čvor.                                      */
         if(*pList == NULL) {       /* Ako lista nije formirana                            */
             *pList = n;            /* iniciraj pokazivač                                  */
             n->next = NULL;
         }
         else {
            LIST p = *pList;
            while ( p->next != NULL) /* 1. odredi krajnji čvor                             */
                p = p -> next;
            p ->next = n;             /* 2. dodaj novi čvor                                */
            n->next = NULL;           /* 3. označi kraj liste                              */
         }
       }

Ovu funkciju se koristi po istom obrascu kao i funkciju add_front_node(), tj. novi element
(x) se dodaje naredbom:

      add_back_node(&List, newNode(x));

18.2.8 Umetanje i brisanje čvora unutar liste
Postupak umetanja ili brisanja čvora n ilustrira slika 18.3




                    Slika 18.3 Umetanje i brisanje čvora unutar vezane liste

Brisanje čvora koji slijedi iza čvora “p” ( na slici, to je čvor “n”) vrši se naredbama:




                                                                                                256
n = p->next;                 /* odredi sljedeći */
      p->next = n->next;
      freeNode(n);

Ako treba izbrisati čvor “n”, potrebno je odrediti čvor “p”, koji mu prethodi.

      p = /* odredi čvor koji prethodi čvoru n*/
      p->next = n->next;
      freeNode(n);

Umetanje čvora “n” iza čvora “p” se provodi naredbama:

      n->next = p->next;
      p->next = n;

Umetanje čvora “n” iza čvora “x” provodi se tako da se najprije odredi čvor “p” koji prethodi
čvoru “x”, a zatim se provodi prethodni postupak.
Operaciju kojom se određuje čvor koji prethodi čvoru “n” realizira se funkcijom
get_previous_node(), koja vraća pokazivač na prethodni čvor, ili NULL ako ne postoji
prethodni čvor.

      Node *get_previous_node(LIST List, Node *n )
      {
          Node *t, *pre;
          t = pre = List;            /* start od glave    */
          while( (t != n)            /* dok ne pronađeš n */
              && (t->next != NULL ))
          {
              pre = t;               /* pamti prethodni   */
              t = t->next ;
          }
          return (pre);              /* vrati prethodni   */
      }

Sada se postupak brisanja čvora može realizirati funkcijom delete_node(). Njome se iz liste
briše čvor “n”.

      void delete_node(LIST *pList, Node *n)
      {
          if(*pList == NULL || n == NULL)
              return;
          if(*pList == n) {          /* ako n pokazuje glavu */
             *pList = n->next;       /* odstrani glavu       */
          }
          else {
             Node *pre = get_previous_node(*pList, n );
             pre->next = n->next;
          }
          freeNode(n);               /* dealociraj čvor      */
      }

18.2.9 Brisanje čvora na kraju liste
Brisanje čvora na kraju liste je jednako komplicirana operacija kao i brisanje unutar liste, jer se i
u ovom slučaju mora odrediti čvor koji mu prethodi. To je realizirano funkcijom
delete_back_node().

      /* Funkcija: delete_back_node




                                                                                                 257
* odstranjuje čvor na kraju liste
       * Argumenti:
       *   pList - pokazivač na pokazivač liste.
       */

      void delete_back_node(LIST *pList)
      {
          Node *pre, back;                          /* pre – prethodni   */
          if (*pList == NULL)                       /* back – krajnji    */
              return;
          back = pre = *pList;                      /* start od glave    */
          while(back->next != NULL ) {              /* pronađi kraj liste*/
              pre = back;                           /* zapamti prethodni */
              back = back->next ;
          }

           if(back == *pList)              /*   ako je krajnji = glava   */
             *pList == NULL;               /*   napravi praznu listu     */
           else                            /*   inače                    */
             pre->next = NULL;             /*   prethodni postaje krajnji*/
           freeNode(back);                 /*   dealociraj čvor          */
      }


Primjer: U program testlist.c testiraju se prije opisane funkcije.


      void printNode(Node *n)
      { if(n) printf( "%d ", n->elem ); }

      void printList(LIST List)
      {
          if( L == NULL ) printf( "Lista je praznan" );
          else list_for_each(L, printNode);
          printf( "n" );
      }

      int main( )
      {
          LIST List;
          int i;

           /* obvezno iniciraj listu */
           List = NULL ;

           /* formiraj listu s 10 cijelih brojeva */
           for( i = 0; i < 10; i++ ) {
              add_front_node(&List, newNode(i));
               printList(List);
           }

           /* izbriši prednji i stražni element */
           delete_front_node(&List);
           delete_back_node(&List);
           printList(List);

           if(find_list_element(List, 5) != NULL)
              printf( "pronadjen element :%dn", 5 );




                                                                              258
if(find_list_element(List, 9) == NULL)
               printf( "Nije pronadjen element :%dn", 9 ) ;

            add_back_node(&List, newNode(9));
            printList(List);
            delete_all_nodes(&List) ;
            printList(List);
            return 0;
      }


Nakon izvršenja dobije se ispis:

      0
      1 0
      2 1 0
      3 2 1 0
      4 3 2 1 0
      5 4 3 2 1 0
      6 5 4 3 2 1 0
      7 6 5 4 3 2 1 0
      8 7 6 5 4 3 2 1 0
      9 8 7 6 5 4 3 2 1 0
      8 7 6 5 4 3 2 1
      pronadjen element :5
      Nije pronadjen element :9
      8 7 6 5 4 3 2 1 9
      Lista je prazna



18.3 Što može biti element liste
     Funkcije koje su u prethodnom poglavlju korištene za rad s vezanom listom mogu se, u
neizmijenjenom obliku, primijeniti jedino na listu cijelih brojeva. Sada će biti pokazano kako se
te funkcije mogu prilagoditi za rad s listama koje sadrže proizvoljan tip podataka.
     Uzmimo za primjer da u listi treba zapisati podatke o studentima: 1) njihovo ime i 2)
ocjenu. To se može realizirati na dva načina:
Prvi je način da se čvor liste formira tako da sadrži više različitih tipova podataka, primjerice:

      typedef struct snode StudentNode;
      typedef StudentNode *STUDENTLIST;

      typedef struct snode
      {
          StudentNode *next;
          char *ime;
          int ocjena;
      } StudentNode;

Drugi je način da se element liste definira strukturom koja ima više članova, primjerice:

      typedef struct student_info {
          char *ime;
          int ocjena;
      }Student;

      struct snode




                                                                                                 259
{
             StudentNode *next;           /* pokazuje na slijedeći čvor */
             Student elem;                /* element liste tipa Student */
      };

U oba slučaja lista se formira istim postupkom kao i lista stringova, jedino je potrebno
modificirati funkcije newNode(), freeNode(), Find() i Print(). Primjerice, funkcija
newNode() u ovom slučaju ima dva argumenta: ime i ocjenu, a realizira se na sljedeći način.

Prvi način                                    Drugi način

StudentNode                                   StudentNode
*newNode(char *ime, int ocjena)               *newNode(char *ime, int ocjena)
{                                             {
   StudentNode *novi =                           StudentNode *novi =
   malloc(sizeof(StudentNode));                  malloc(sizeof(StudentNode));
   if(novi != NULL) {                            if(novi != NULL) {
      novi->ime = strdup(ime);                      novi->elem.ime = strdup(ime);
      novi->ocjena = ocjena;                        novi->elem.ocjena = ocjena;
   }                                             }
   return novi;                                  return novi;
}                                             }


18.4 Lista sa sortiranim redoslijedom elemenata
     U dosadašnjim primjerima podaci su bili svrstani u listi redoslijedom kojim su i uneseni u
listu. Često je pak potrebno da podaci u listi budu u sortiranom redoslijedu. To se može postići
na dva načina. Prvi je način da se izvrši sortiranje liste, a drugi je način da se već pri samom
unošenju podataka ostvari sortirani redoslijed elemenata. S obzirom da je namjena liste da služi
kao kolekcija u koju se često unose i odstranjuju elementi, ovaj drugi pristup se češće koristi.
     Izrada liste s podacima o imenu i ocjeni studenta je tipičan primjer korištenja liste u kojem
je poželjno imati sortiran redoslijed elemenata. Sada će biti prikazana izvedba modula za
manipuliranje s listom studenata.
Prema uputama za formiranje modula, iznesenim u poglavlju 9, modul za listu studenata će se
formirati od sljedećih datoteka:
    1. datoteka specifikacije modula ("studentlist.h") sadrži deklaracije funkcija i struktura
        koje se koriste za rad s listom.
    2. datoteka implementacije ("studentlist.c") sadrži definicije varijabli i implementaciju
        potrebnih funkcija.
    3. datoteka primitivnih funkcija za manipuliranje listom ("listprim.c") koje su definirane u
        prethodnom poglavlju. Ova datoteka će se koristiti isključivo kao #include datoteka
        u datoteci "studentlist.c". Sve funkcije iz ove datoteke su označene prefiksom static,
        što znači da će biti vidljive samo u datoteci "studentlist.c".
    4. Testiranje modula se provodi programom studlist-test.c.

Datoteka specifikacije –"studentlist.h"

      #ifndef _STUDENTLIST_H_
      #define _STUDENTLIST_H_

      /*     sortirana lista za evidenciju studenata */

      typedef struct stnode StudentNode;
      typedef StudentNode *STUDENTLIST;




                                                                                              260
struct stnode
      {
          StudentNode *next;
          char *ime;
          int ocjena;
      };

      void StudentL_insertSorted (STUDENTLIST *pList,
                                  char *ime, int ocjena);
      /* U listu umeće podatke o studentu: ime i ocjenu
       * Ako ime već postoji, zamjenuje se vrijednost ocjene.
       * Lista je uvijek sortirana prema imenu studenta
       */

      StudentNode *StudentL_find(STUDENTLIST L, char *s);
      /* Traži čvor liste u kojem je ime jednako stringu s
       * Vraća pokazivač na čvor, ili NULL ako nije pronađen string
       */

      void StudentL_deleteNode(STUDENTLIST *pList, StudentNode *n);
      /* Odstranjuje čvor liste na kojeg pokazuje n */

      void StudentL_delete(STUDENTLIST *pList) ;
      /* Odstranjuje sve čvorove liste */

      int StudentL_size(STUDENTLIST List);
      /* Vraća broj elemanate liste */

      void StudentL_print(STUDENTLIST L );
      /* Ispisuje sadržaj liste */

      #endif

Sve deklaracije su napisane unutar makro naredbi

      #ifndef _STUDENTLIST_H_
      #define _STUDENTLIST_H_
        ........ deklaracije .....
      #endif

To osigurava da se u jednoj izvornoj datoteci samo jedan put može umetnuti h-datoteka.

Čvor liste je opisan strukturom StudentNode, a tip pokazivač na taj čvor je nazvan
STUDENTLIST. Imena svih funkcija počinju prefiksom StudentL_ , a ostatak imena je u
skladu s imenom ekvivalentnih primitivnih funkcija koje su definirane u prethodnom poglavlju.

Datoteka implementacije – "studentlist.c"

      #include <stdio.h>
      #include <stdlib.h>
      #include "studentlist.h"

      static StudentNode *newNode(char *ime, int ocjena)
      {
          StudentNode *n = malloc(sizeof(StudentNode));
          if(n != NULL)
          {
              n->ime=strdup(ime);




                                                                                         261
n->ocjena = ocjena;
      }
      return n;
}

static void freeNode(StudentNode *n)
{
    if(n) { free(n->ime); free(n); }
}

#define LIST STUDENTLIST
#define Node StudentNode

#include "listprim.c"

#undef LIST
#undef Node

int   StudentL_size(STUDENTLIST List)
{     return num_list_elements(List);
}

void StudentL_deleteNode(STUDENTLIST *pList, StudentNode *n)
{    delete_node(pList, n);
}

void StudentL_delete(STUDENTLIST *pList)
{    delete_all_nodes(pList);
}

void
StudentL_insertSorted (STUDENTLIST *pList,      char *ime, int ocjena)
{
    StudentNode *tmp = *pList;
    StudentNode *pre = NULL;
    StudentNode *n;
    int cmp;

      if (!*pList) { /* ako je lista prazna, formiraj prvi čvor */
             n= newNode (ime, ocjena);
         n->next=NULL;
         *pList = n;
         return;
      }

      /* nađi zadnji čvor (i njemu prethodni) u kojem je ime manje
         ili jednako zadanom imenu, ili kraj liste */

      cmp = strcmp(ime, tmp->ime);
      while ((tmp->next) && (cmp > 0)) {
          pre = tmp;
          tmp = tmp->next;
          cmp = strcmp(ime, tmp->ime);
      }
      /* ako ime već postoji, zamijeni ocjenu    */
      if(cmp == 0) {
             tmp->ocjena = ocjena;
             return;
      }




                                                                   262
/*inače dodaj novi čvor */
             n= newNode (ime, ocjena);
             n->next=NULL;

             /* ako je dosegnut kraj liste, dodaj na kraj liste */
             if ((tmp->next == NULL) && (cmp > 0)) {
                 tmp->next = n;
               return;
             }
             /*ili umetni iza prethodnog*/
             if (pre != NULL) {
                 pre->next = n;
                 n->next = tmp;
             }
             /*ili stavi na početak liste ako je prethodni == NULL*/
             else {
                 n->next = *pList;
                 *pList = n;
             }
      }

      StudentNode *StudentL_find(STUDENTLIST L, char *ime)
      {
          while( L != NULL && strcmp(L->ime, ime) )
                L = L->next;
          return L;
      }

      void PrintStInfo(StudentNode *n )
      {     printf( "Ime:%s, ocjena:%d n", n->ime, n->ocjena );
      }

      void StudentL_print(STUDENTLIST L )
      {
          if( L == NULL ) printf( "Lista je praznan" );
          else             list_for_each(L, PrintStInfo);
          printf( "n" );
      }

Izvedba svih funkcija je izvršena po poznatom obrascu, jedino je potrebno objasniti algoritam
za sortirano umetanje čvorova liste. On glasi:

          Algoritam: Sortirano umetanje čvora liste (ime,ocjena)
          Ako je lista prazna,
             Formiraj čvor s zadanim imenom i ocjenom
             i stavi ga na početak liste
          inače
            Nađi zadnji čvor (i njemu prethodni) u kojem je ime leksikografski manje
             ili jednako zadanom imenu, ili kraj liste

            Ako čvor s zadanim imenom već postoji,
                Zamijeni ocjenu
            inače
                Formiraj čvor s zadanim imenom i ocjenom.
                Ako je dosegnut kraj liste
                   Dodaj na kraj liste




                                                                                         263
inače
                 Umetni iza prethodnog
                  ili stavi na početak liste ako je prethodni == NULL

Testiranje modula se provodi programom studlist_test.c.

      /* Datoteka: studlist_test.c */
      #include <stdio.h>
      #include <stdlib.h>
      #include "studentlist.h"

      int main( )
      {
          STUDENTLIST List, n;
          int i;

            List = NULL ;

            StudentL_insertSorted            (&List,   "Bovan   Marko", 5);
            StudentL_insertSorted            (&List,   "Radic   Jure", 2);
            StudentL_insertSorted            (&List,   "Marin   Ivo", 3);
            StudentL_insertSorted            (&List,   "Bovan   Marko", 2);

            printf("Lista ima %d elemenatan", StudentL_size(List));
            StudentL_print(List );

            n = StudentL_find(List, "Bovan Marko");
            if(n != NULL)
               StudentL_deleteNode(&List, n);

            StudentL_print(List);
            StudentL_delete(&List) ;

            return 0;
      }

Program se kompilira komandom:

      c:> cl studlist_test.c            studentlist.c

Nakon izvršenja, dobije se sljedeći ispis:

      Lista ima     3 elemenata
      Ime:Bovan     Marko, ocjena:2
      Ime:Marin     Ivo, ocjena:3
      Ime:Radic     Jure, ocjena:2

      Ime:Marin Ivo, ocjena:3
      Ime:Radic Jure, ocjena:2


Zadatak: Objasnite zašto u prethodnom ispisu lista početno ima tri elementa, iako je u
programu četiri puta primijenjena funkcija StudentL_insertSorted().

Zadatak: Napišite program u kojem korisnik unosi rezultate ispita (ime i ocjenu studenta) u
sortiranu listu. Unos završava kada se otkuca “prazno ime” ili ocjena 0. Nakon toga treba




                                                                                       264
rezultate ispita upisati u tekstualnu datoteku, na način da se u jednom retku ispiše redni broj,
ime i ocjena studenta.


18.5 Implementacija ADT STACK pomoću linearne liste
     U poglavlju 16 izvršena je implementacija ADT STACK pomoću niza elemenata. Stog se
može efikasno implementirati i pomoću linearne liste. Uzmemo li da je specifikacija ADT
STACK ista kao i specifikacija ADT za implementaciju pomoću niza u datoteci "stack.h", koja je
opisana u poglavlju 16, implementacija se može napraviti na način kako je opisano u datoteci
"stack-list.c".
     Sada STACK predstavlja pokazivač na strukturu stack, koja ima samo jedan element,
pokazivač na čvor linearne liste. Taj pokazivač je nazvan top, i on pokazuje na glavu liste.
Operacija Push() je implementirana kao umetanje elementa na glavu liste, a pop() kao
brisanje elementa s glave liste.

      /* Datoteka: stack-list.c */
      /* Implementacija ADT STACK pomoću vezane liste */
      #include <stdlib.h>
      #include <assert.h>
      #include "stack.h"

      /* typedef int stackElemT;                   definiran u stack.h*/
      /* typedef struct stack *STACK;              definiran u stack.h*/

      struct node {
           stackElemT elem;
           struct node *next;
      };

      struct stack {
             struct node *top;
      };

      static void stack_error(char *s)
      {
             printf("nGreska: %sn", s);
             exit(1);
      }

      STACK stack_new(void)
      {/*alocira ADT STACK*/
          STACK S = malloc(sizeof(struct stack));
          if(S != NULL) S->top = NULL;
          return S;
      }

      void stack_free(STACK S)
      {/*dealocira ADT STACK*/
          struct node *n;
          assert(S != NULL);
          if( !stack_empty(S))
             for (n = S->top; n != NULL; n = n->next)
                free(n);
      }

      int stack_empty(STACK S)




                                                                                            265
{/* vraća != 0 ako je stog prazan*/
         assert(S != NULL);
         return ( S->top==NULL);
      }

      unsigned stack_count(STACK S)
      {/*vraća broj elemenata na stogu*/
         unsigned num = 0;
         struct node *n;
         assert(S != NULL);
         if( !stack_empty(S))
            for (n = S->top; n != NULL;                n = n->next)
               num++;
         return num;
      }

      stackElemT stack_top(STACK S)
      {/*vraća vrijednost elementa na vrhu stoga*/
         assert(S != NULL);
         return S->top->elem;
      }

      stackElemT stack_pop(STACK S)
      {/* Skida čvor s vrha stoga */
         stackElemT el;
         struct node *n;
         assert(S != NULL);
         if (stack_empty(S)) stack_error("Stog je prazan");
         n = S->top;
         el = n->elem;
         S->top = n->next;
         free(n);
         return el;
      }

      void stack_push(STACK S, stackElemT el)
      {/* Ubacuje čvor na vrh stoga*/
         struct node *n;
         assert(S != NULL);
         n = malloc(sizeof(struct node));
         if (n != NULL) {
            n->elem = el;
            n->next = S->top;
            S->top = n;
         }
         else
            printf(" Nema dovoljno memorije!n");
      }

Testiranje ove implementacije se provodi s programom stack-test.c, koji je opisan u
poglavlju 16. Jedina razlika je da se u u tom programu umjesto uključenja datoteke "stack-arr.c"
uključi datoteka "stack-list.c".

Zadatak: Napišite ADT imena STRSTACK za stog na kojeg će se stavljati stringovi. Vodite
računa da treba alocirati i dealocirati memoriju koju zauzima string. Napišite program za
testiranje ADT STRSTACK.




                                                                                            266
18.6 Implementacija ADT QUEUE pomoću vezane liste
    Red se može jednostavno implementirati i pomoću vezane liste. Za implementacija reda
pomoću vezane liste koristit će se prethodna specifikaciju ADT-a QUEUE (danu u datoteci
"queue.h"). U ovoj implementaciji QUEUE predstavlja pokazivač na strukturu queue, koja
ima dva člana: front i back. To su pokazivači na prednji i stražnji element liste. Operacijom
put() stavlja se element na kraj liste, a operacijom get() skida se element s glave liste.




               Slika 18.4 Operacije s listom koju se koristi kao red za čekanje


      /* Datoteka: queue-list.c */

      /* Queue    realiziran pomoću vezane liste*/
      #include    <stdio.h>
      #include    <stdlib.h>
      #include    <assert.h>
      #include    "queue.h"

      /* typedef int queueElemT; */
      /* typedef struct queue *QUEUE; */

      struct node {
           queueElemT elem;
           struct node *next;
      };

      struct queue {
          struct node * front;
          struct node * back;
      };

      QUEUE queue_new(void)
      { /*alocira ADT QUEUE*/
          QUEUE Q = malloc(sizeof(struct queue));
          if(Q != NULL){ Q->front = Q->back = NULL; }
          return Q;
      }

      void queue_free(QUEUE Q)
      {/*dealocira ADT QUEUE*/
          struct node *n;
          assert(Q != NULL);
          while ((n = Q->front) != NULL ) {
               Q->front = n ->next;
               free(n);
          }




                                                                                         267
free(Q);
}

int queue_empty(QUEUE Q)
{/* vraća != 0 ako je red prazan*/
    assert(Q != NULL);
    return Q->front == NULL;
}

int queue_full(QUEUE Q)
{ /* vraća != 0 ako je red popunjen*/
  /* uvijek možemo dodati element u listu */
     return 0;
}

void queue_put(QUEUE Q, queueElemT el)
{/* stavlja element el u red Q*/
    struct node * n;
    assert(Q != NULL);
    n = malloc(sizeof(struct node));
    if (n != NULL) {
       n->elem = el;
       n->next = NULL;
       if (queue_empty(Q)) Q->front = n;
       else                Q->back->next = n;
       Q->back = n;
    }
}

queueElemT queue_get(QUEUE Q)
{/* vraća element iz reda Q*/
    queueElemT el;
    struct node * n;
    assert(Q != NULL);
    n = Q->front;
    el = n->elem;
    Q->front = n->next;
    if (Q->front == NULL)    Q->back = NULL;
    free(n);
    return el;
}

unsigned queue_count(QUEUE Q)
{/*vraća broj elemenata reda*/
    unsigned num = 0;
    struct node *n;
    assert(Q != NULL);
    if( !queue_empty(Q))
       for (n = Q->front; n != NULL;   n = n->next)
          num++;
    return num;
}

void queue_print(QUEUE Q)
{
    int i;
    struct node *n;
    assert(Q != NULL);
    n = Q->front;




                                                      268
if( Q->front == NULL )
               printf( "Red je prazann" );
           else do {
               printf( "%d, ", n->elem );
               n = n->next;
           } while( n != NULL );
           printf( "n" );
      }

Testiranje ove implementacije se provodi s programom queue-test.c, opisanim u pogavlju
16, tako da se u njemu umjeto uključenja datoteke "queue-arr.c" uključi datoteka "queue-
list.c".


18.7 Dvostruko vezana lista
     Ukoliko se čvor liste formira tako da sadrži dva pokazivača, next - koji pokazuje na
sljedeći čvor i prev - koji pokazuje na prethodni čvor, dobije se dvostruko vezana lista.
Realizira se pomoću sljedeće strukture podataka:

      typedef int elemT;
      typedef struct node Node;
      typedef Node *DLIST;

      struct node
      {
           elemT elem;        /* element liste */
           Node *next;          /* pokazuje na sljedeći čvor */
           Node *prev;          /* pokazuje na prethodni čvor */
      }




                             Slika 18.5 Dvostruko vezana lista



Veze među čvorovima ilustrira slika 18.5. Karakteristike prikazane liste su:
   1. Pokazivač next krajnjeg elementa i pokazivač prev glave liste su jednaki NULL.
   2. Ovakvu listu se može iterativno obilaziti u oba smjera, od glave prema kraju liste
      (korištenjem pokazivača next) i od kraja prema početku liste (korištenjem pokazivača
      prev).
   3. Umetanje unutar liste i brisanje iz liste se provodi jednostavnije i brže nego kod
      jednostruko vezane liste, jer je u svakom čvoru poznat pokazivač na prethodni čvor.
   4. U odnosu na jednostruko vezanu listu, dvostruko vezana lista zauzima više memorije
      (zbog pokazivača prev) .




                                                                                            269
5. Ako se vrši umetanje i brisanje čvorova samo na početku liste, tada je bolje rješenje
       koristiti jednostruko vezanu listu.

Kroz dva primjera bit će pokazano kako se implementira dvostruko vezana lista.

Primjer:   Prikana      je    implementacija   funkcija   za    umetanje   čvora   na    glavi   liste
(dlist_add_front_node)     i      odstranjivanje     čvora       na      glavi     liste
(dlist_delete_front_node). Uočite da je potrebno izvršiti nešto više operacija nego kod
jednostruko vezane liste.

      void dlist_add_front_node(DLIST *pList, Node *n)
      {
          if(n != NULL) /* izvrši samo ako je alociran čvor */
          {
              n->next = *pList;
              n->prev = NULL;
              if(*pList != NULL)
                 (*pList)->prev = n;
              *pList = n;
          }
      }

      void dlist_delete_front_node(DLIST *pList)
      {
          Node *tmp = *pList;
          if(*pList != NULL) {
              *pList = (*pList)->next;
              if(*pList != NULL)
                 (*pList)->prev = NULL;
              freeNode(tmp);
          }
      }


Primjer:    Prikazana        je   implementacija   funkcija    za   brisanje   čvora    unutar   liste
(dlist_delete_node). Uočite da se ova operacija izvršava efikasnije nego kod primjene
jednostruko vezane liste.

      void dlist_delete_node(DLIST *pList, Node *n)
      {
          if(*pList == NULL || n == NULL)
              return;
          if(*pList == n) {          /* ako n pokazuje glavu */
             *pList = n->next;       /* odstrani glavu */
             *plist->prev = NULL;
          }
          else {
             n->prev->next = n->next;
             if(n->next != NULL)
                n->next->prev = n->prev;
          }
          freeNode(n);    /* dealociraj čvor */
      }


Zadatak: Napišite funkciju za brisanje i umetanje čvora na kraju liste. Za testiranje
implementacije koristite isti program kao kod jednostruko vezane liste.




                                                                                                 270
Napomena: Funkcije za formiranje čvora, brisanje liste i traženje elementa liste su iste kao kod
jednostruko vezane liste.


18.8 Generički dvostrani red - ADT DEQUEUE
Dvostruka se lista može iskoristiti za efikasnu implementaciju dvostranog reda (eng. double
ended queue) u obliku ADT DEQUEUE.

Temeljne operacije s dvostranim redom - DEQUEUE su:

       front()        - vraća element na glavi liste
       push_front(el) - stavlja element el na glavu liste
       pop_front()    - skida element s glave liste
       back()         - vraća element s kraja liste
       push_back(el) - stavlja element el na kraj liste
       pop_back()     - skida element s kraja liste
       find(el)       - vraća pokazivač na traženi element ili null ako element nije u listi
       delete(el)     - briše element el ako postoji u listi
       for_each(fun) - primjenjuje funkciju fun na sve elemente liste
       size()         - vraća broj elemenata u listi
       empty() - vraća nenultu vrijednost ako je red prazan, inače vraća 0

Cilj je izvršiti generičku implementaciju ADT DQUEUE, tako da se on može primijeniti na bilo
koji tip elemenata reda. Zbog toga će se u implementaciji ADT-a koristiti sljedeće strukture
podataka:

      typedef struct dnode DNode;

      struct dnode {
        void   *elem;
        DNode *prev;
        DNode *next;
      };

      typedef struct dequeue Dequeue;
      typedef struct dequeue *DEQUEUE;

      typedef int   (*CompareFuncT)(void *, void *);
      typedef void *(*CopyFuncT)(void *);
      typedef void (*FreeFuncT)(void *);


      struct dequeue {
        DNode     *front;                   /*pokazivač prednjeg čvora liste    */
        DNode     *back;                    /*pokazivač stražnjeg čvora liste   */
        int        size;                    /*broj elemenata u listi            */
        CompareFuncT CompareElem;           /*funkcija za usporedbu elemenata   */
        CopyFuncT    CopyElem;              /*funkcija za kopiranje i alociranje*/
        FreeFuncT    FreeElem;              /*funkcija za dealociranje elementa */
      };

     Čvor liste je opisan strukturom DNode. Sadrži dva pokazivača koji pokazuju na prethodni i
sljedeći čvor liste te pokazivač na element liste koji je deklariran kao void*. Tom pokazivaču
se može pridijeliti adresa bilo koje strukture podataka, ali void pokazivači ne sadrže




                                                                                            271
informaciju o operacijama koje se mogu provoditi s podacima na koje oni pokazuju. Zbog toga
će biti potrebno da korisnik definira tri pokazivača na funkcije;
    o    CompareElem - pokazivač funkcije za usporedbu dva elementa (poput strcpy),
    o    CopyElem - pokazivač funkcije za alociranje i kopiranje elementa (poput strdup) i
    o    FreeElem - pokazivač funkcije za dealociranje memorije koju element liste zauzima
         (popot free).
     Ovi se pokazivači bilježei u strukturi Dequeue, koja je temelj za definiranje ADT
DEQUEUE. Tip ovih funkcija je definiran s tri typedef definicije. U structuri Dequeue
bilježe se i pokazivači na prednji (front) i stražnji element liste (back) te broj elemenata u listi
(size).       Sadržaje ove strukture se određuje pri inicijalizaciji ADT-a funkcijom
dequeue_new(). Ta i ostale funkcije opisane su u datoteci "dequeue.h".


Datoteka specifikacije - "dequeue.h":

        /* Datoteka: dequeue.h
         * Specifikacija ADT DEQUEUE
         */

        #ifndef _DEQUEUE_H_
        #define _DEQUEUE_H_

        typedef struct dequeue *DEQUEUE;

        typedef int   (*CompareFuncT)(void *, void *);
        typedef void *(*CopyFuncT)(void *);
        typedef void (*FreeFuncT)(void *);

        DEQUEUE dequeue_new(CompareFuncT Compare,
                            CopyFuncT Copy, FreeFuncT Free);
        /* Konstruktor ADT DEQUEUE
         * Argumenti:
         *   Free - pokazivač funkcije za dealociranje elementa liste
         *   Copy - pokazivač funkcije za alociranje elementa liste
         *   Compare - pokazivač funkcije za usporedbu elemenata liste
         *             vraća 0 ako su elementi jednaki,
         *                  <0 ako je prvi element manji od drugoga
         *                  >0 ako je prvi element veći od drugoga
         * Ako su svi pokazivači funkcija jednaki nuli,
         * tada se podrazumijeva se da je element liste cijeli broj.
         * Primjer korištenja:
         * Za rad s listom stringova konstruktor je
         *      DEQUEUE d = dequeue_new(strcmp,strdup, free);
         * Za rad s listom cijelih brojeva konstruktor je
         *      DEQUEUE d = dequeue_new(0,0,0);
         * i tada se void pokazivači tretiraju kao cijeli brojevi
         */

        void dequeue_free(DEQUEUE d);
        /* Destruktor DEQUEUE d */

        int dequeue_size(DEQUEUE d);
        /* Vraća veličunu DEQUEUE d*/

        int dequeue_empty(DEQUEUE d);
        /* Vraća 1 ako je DEQUEUE d prazan, inače vraća 0*/




                                                                                                272
void *dequeue_front(DEQUEUE d);
     /* Vraća pokazivač elementa na glavi DEQUEUE d
      * ili cijeli broj ako lista sadrži cijele brojeve
      */
     void dequeue_push_front(DEQUEUE d, void *el);
     /* Stavlja element el na glavu DEQUEUE d*/

     void dequeue_pop_front(DEQUEUE d);
     /* Skida element s glave DEQUEUE d*/

     void *dequeue_back(DEQUEUE d);
     /* Vraća pokazivač elementa s kraja DEQUEUE d
      * ili cijeli broj ako lista sadrži cijele brojeve
      */
     void dequeue_push_back(DEQUEUE d, void *el);
     /* Stavlja element el na kraj DEQUEUE d*/

     void dequeue_pop_back(DEQUEUE d);
     /* Skida element s kraja DEQUEUE d*/

     void *dequeue_find(DEQUEUE d, void *el);
     /* Vraća pokazivač elementa liste koji je jednak elementu el */

     int dequeue_delete(DEQUEUE d, void *el);
     /* Briše element el. Ako postoji vraća 1, inače vraća 0 */

     void dequeue_for_each(DEQUEUE d, void (*func)(void *));
     /* Primjenjuje funkciju func na sve elemente DEQUEUE d*/

     #endif

Implementacija ADT DEQUEUE je opisana u datoteci "dequeue.c" .

     /* Datoteka: dequeue.c
      * Implementacija ADT DEQUEUE
      */
     #include <stdlib.h>
     #include <string.h>
     #include <assert.h>
     #include "dequeue.h"

     typedef struct dnode DNode;

     struct dnode {
        void   *elem;
        DNode *prev;
        DNode *next;
     };

     typedef struct dequeue Dequeue;

     struct dequeue {
         DNode     *front;
         DNode     *back;
         int        size;
         CompareFuncT CompareElem;
         CopyFuncT    CopyElem;
         FreeFuncT    FreeElem;




                                                                       273
};

static int CompareInternal(void *a, void *b)
{
    if((int)a > (int)b) return 1;
    else if((int)a < (int)b) return -1;
    else return 0;
}

DEQUEUE dequeue_new( CompareFuncT Compare, CopyFuncT Copy,
FreeFuncT Free)
{
    DEQUEUE d=(DEQUEUE) malloc(sizeof(Dequeue));
    if(d ) {
        d->front = d->back = NULL;
        d->size = 0;
        d->CopyElem = Copy;
        d->FreeElem = Free;
        if(Compare == NULL)
            d->CompareElem = CompareInternal;
        else
            d->CompareElem = Compare;
    }
    return d;
}

/* dvije pomoćne funkcije - make_node i free_node*/

static DNode *make_node(DEQUEUE d, void *el)
{/* stvara čvor koristeći CopyElem funkciju */
    DNode *n=(DNode *) malloc(sizeof(DNode));
    if(n) {
        n->prev = n->next = NULL;
        if(d->CopyElem != NULL)
            n->elem = d->CopyElem(el);
        else
            n->elem = el;
    }
    return n;
}

static void free_node(DEQUEUE d, DNode *n)
{/* dealocira čvor koristeći FreeElem funkciju */
    if(n) {
        if(d->FreeElem != NULL)
            d->FreeElem(n->elem);
        free(n);
    }
}

void dequeue_free(DEQUEUE d)
{
    DNode *tmp, *node;
    assert(d);
    node = d->front;
    while (node)   {
        tmp = node;
        node = node->next;
        free_node(d, tmp);




                                                             274
}
    free(d);
}

int dequeue_size(DEQUEUE d)
{
    assert(d);
    return d->size;
}

int dequeue_empty(DEQUEUE d)
{
    assert(d);
    return d->size==0;
}

void *dequeue_front(DEQUEUE d)
{
    assert(d);
    if(d->front) return d->front->elem;
    else return NULL;
}

void dequeue_push_front(DEQUEUE d, void *elem)
{
    DNode *new_node;
    assert(d);
    new_node = make_node(d, elem);
    if(new_node == NULL) return;
    if (d->front) {
        new_node->next = d->front;
        d->front->prev = new_node;
        d->front = new_node;
    }
    else {
        d->front = d->back = new_node;
    }
    d->size ++;
}

void dequeue_pop_front(DEQUEUE d)
{
    DNode *old;
    assert(d);
    if (d->front) {
        old = d->front;
        d->front = d->front->next;
        if(d->front == NULL)
            d->back = NULL;
        else
            d->front->prev = NULL;
        free_node(d, old);
        d->size --;
    }
}

void* dequeue_back(DEQUEUE d)
{
    assert(d);




                                                 275
if(d->back) return d->back->elem;
    else return NULL;
}

void dequeue_push_back(DEQUEUE d, void * elem)
{
    DNode *new_node;
    DNode *last;
    assert(d);
    new_node = make_node(d, elem);
    if(new_node == NULL) return;
    if (d->back) {
        last = d->back;
        last->next = new_node;
        new_node->prev = last;
        d->back = new_node;
    }
    else {
        d->front = d->back = new_node;
    }
    d->size++;
}

void dequeue_pop_back(DEQUEUE d)
{
    DNode *old;
    assert(d);
    if (d->back) {
        old = d->back;
        d->back = d->back->prev;
        if(d->back == NULL)
            d->front = NULL;
        else
            d->back->next = NULL;
        free_node(d, old);
        d->size--;
    }
}

int dequeue_delete(DEQUEUE d, void * elem)
{
    DNode *tmp;
    assert(d);
    tmp = d->front;
    while (tmp) {
        /*prvo pronađi element*/
        if ((*d->CompareElem)(tmp->elem, elem) != 0)
            tmp = tmp->next;
        /*element u čvoru tmp*/
        else if(tmp == d->front) {
            dequeue_pop_front(d); break;
        }
        else if(tmp == d->back){
            dequeue_pop_back(d); break;
        }
        else {
            if (tmp->prev) tmp->prev->next = tmp->next;
            if (tmp->next) tmp->next->prev = tmp->prev;
            free_node(d, tmp);




                                                          276
d->size--;
                 break;
             }
         }
         return tmp != NULL;
     }

     void *dequeue_find(DEQUEUE d, void * elem)
     {
         DNode* node;
         assert(d);
         node = d->front;
         while (node){
             if((*d->CompareElem)(node->elem, elem) == 0)
                 return node->elem;
             node = node->next;
         }
         return NULL;
     }


     void dequeue_for_each(DEQUEUE d, void (*func)(void *))
     {
         DNode* node;
         assert(d);
         node = d->front;
         while (node)
         {
             DNode *next = node->next;
             (*func) (node->elem);
             node = next;
         }
     }

Testiranje DEQUEUE se provedi programom dequeue-teststr.c. U njemu korisnik
proizvoljno umeće ili briše stringove u dvostranom redu.

     Datoteka: dequeue-teststr.c
     #include <stdio.h>
     #include <stdlib.h>
     #include <string.h>
     #include <assert.h>
     #include "dequeue.h"

     void upute(void)
     {
         printf ("Izbornik:n"
             "   1 Umetni ime sprijeda n"
             "   2 Umetni ime straga n"
             "   3 Odstrani sprijedan"
             "   4 Odstrani stragan"
             "   0 Krajn");
     }

     void print_elem(void *el)
     {printf("%s ", (char*)el);}

     void dequeue_print(DEQUEUE D)
     {




                                                                        277
if(!dequeue_empty(D))
        dequeue_for_each(D, print_elem);
}

int main()
{
    int izbor;
    char elem[255];

    DEQUEUE D = dequeue_new(strcmp, strdup, free);

    upute();
    printf("? ");
    scanf("%d", &izbor);

    while (izbor != 0) {

       switch(izbor) {

        case 1:
            printf("Otkucaj ime: ");
            scanf("n%s", elem);
            dequeue_push_front(D, elem);
            printf("%s ubacen u red.n", elem);
            dequeue_print(D);
            break;
        case 2:
            printf("Otkucaj ime: ");
            scanf("n%s", elem);
            dequeue_push_back(D, elem);
            printf("%s ubacen u red.n", elem);
            dequeue_print(D);
            break;
        case 3:
            if (!dequeue_empty(D)) {
                printf("Bit ce %s odstranjen sprijeda.n",
                       (char *) dequeue_front(D));
                dequeue_pop_front(D);
            }
            dequeue_print(D);
            break;
        case 4:
            if (!dequeue_empty(D)) {
                printf("Bit ce %s odstranjen straga.n",
                        (char *)dequeue_back(D));
                dequeue_pop_back(D);
            }
            dequeue_print(D);
            break;
        default:
            printf("Pogresan Izbor.nn");
            upute();
            break;
       }

       printf("n? ");
       scanf("%d", &izbor);
    }
    if(dequeue_find(D, "ivo")) {




                                                             278
dequeue_delete(D, "ivo");
                   dequeue_print(D);
             }
             dequeue_free(D);
             return 0;
      }


Uočite da operacije traženja imaju složenost O(n), a sve ostale operacije imaju složenost O(1).

Zadatak: Prethodni primjer testiranja dvostranog reda, koji sadrži stringove, promijenite tako
da se njime testira dvostrani red koji sadrži cijele brojeve. Koristite konstruktor oblika:

      DEQUEUE d = dequeue_new(0,0,0);

Zadatak: Prethodni primjer testiranja dvostranog reda, koji sadrži stringove, promijenite tako
da se njime testira dvostrani red koji sadrži realne brojeve tipa double. U tom slučaju morate
definirati funkcije

      void FreeFunc(double* pd);
      /*dealocira memoriju koju zauzima broj tipa double*/

      double *CopyFunc(double* pd);
      /*vraća pokazivač na kopiju alociranog broja *pd                     */

      int CompareDouble(double* pd1, double* pd1);
      /* Vraća 0 ako je *pd1 == *pd2
       * Vraća 1 ako je *pd1 > *pd2
       * Vraća -1 ako je *pd1 < *pd2
       */

i koristiti konstruktor oblika;

      DEQUEUE d = dequeue_new(FreeFunc, Copy Func, CompareFunc);

Zadatak: Generički pristup koji je korišten za definiranje ADT DEQUEUE može se iskoristiti
za definiranje ADT koji služi za rad sa skupovima. Definirajte ADT SET kojim se, kao i u
matemetici, može raditi sa skupovima pomoću sljedećih operacija:

          ADT SET
          Empty(S)                - vraća nenultu vrijednost ako S prazan skup
          size(S)                 - vraća broj elemenata u skupu S, tj. vraća |S|
          insert(S, x)            - stavlja element x u skup S, ako x ∉ S. Nakon toga je x ∈ S
          member(S, x)            - vraća true ako je x ∈ S
          delete(S, x)            - briše element x ako postoji u skupu S
          for_each(S, fun)        - primjenjuje funkciju fun na sve elemente skupa S
          intersection(S1, S2)    - vraća skup S koji je jednak presjeku skupova S1 ∩ S2
          union(S1, S2)           - vraća skup S koji je jednak uniji skupova S1 ∪ S2
          difference(S1, S2)      - vraća skup S koji je jednak razlici skupova S1  S2

Za implementaciju ovih operacija dovoljno je koristiti jednostruko vezanu listu.

Zadatak: Često se kolekcija elemenata, koja, za razliku od skupa, može sadržavati više
istovrsnih elemenata, naziva vreća (eng. BAG). Realizirajte ADT BAG pomoću vezane liste.




                                                                                             279
Svakom elementu liste pridodajte atribut kojeg se obično naziva referentni izbroj (eng. referent
count). Primjerice, možete za čvor liste koristiti sljedeću strukturu:

               struct bag_node {
                    int ref_count;
                    void *element;
                    struct bag_node *next;
               }

Referentni izbroj pokazuje koliko je istih elemenata u vreći.

Realizirajte generički ADT BAG koji podržava sljedeće operacije:
        ADT BAG
        empty(B)                 - vraća nenultu vrijednost ako je vreća prazna
        size(B)                  - vraća broj elemenata u vreći
        insert(B, x)             - stavlja element x u vreću B po sljedećem algoritmu:
                                       ako u vreći već postoji element vrijednoti x, tada
                                           uvećavaj x.ref_count
                                       inače,
                                          kopiraj i alociraj element x u vreću,
                                          postavi x.ref_count na vrijednost 1.
        find(B, x)               - vraća vrijednost x.ref_count koji znači koliko ima istovrsnih
                                   elemenata vrijednosti x, ili 0 ako element x nije u vreći
        delete(B, x)             - briše element x ako postoji u vreći, po sljedećem algoritmu:
                                        smanji x.ref_count za jedan.
                                        ako je x.ref_count jednak nuli, tada
                                             dealociraj memoriju koju zauzima x
                                        vrati x.ref_count



18.9 Zaključak
     Vezana lista je dinamička strukutura. Povećava se i smanjuje prema zahtjevima korisnika.
     Liste su kolekcije sa “sekvencijalnim pristupom”, za razliku od nizova koje se koristi kao
kolekcije sa “slučajnim pristupom”.
     Apstraktno, liste predstavljaju kolekciju elemenata. Operacije umetanja i brisanja
elemenata kolekcije provode se jednostavnije s vezanom listom, nego je to slučaj s nizovima.
Umetanje i brisanje elementa unutar jednostruko vezane liste je relativno spora operacija jer se
u njoj mora odrediti i položaj elementa koji prethodi mjestu umetanja. Znatno je brže umetanje
elemenata na glavi liste.
     Vezane liste se mogu slikovito usporediti s vlakom kojem su vagoni povezani jedan za
drugim. Iz jednog vagona se može prijeći samo u susjedni vagon. Vagoni se mogu dodavati
tako da se spoje na postojeću kompoziciju ili da se umetnu unutar kompozicije vlaka.
Dodavanje vagona je jednostavna operacija, a umetanje vagona unutar kompozicije je složena
operacija.
     Kada je nužno vršiti često umetanje i brisanje čvorova unutar liste, tada je povoljno koristiti
dvostruko vezanu listu.
     Korištenjem tehnike definiranja apstraktnih tipova pokazano je da se pomoću liste mogu
efikasno realizirati apstraktni tipovi STACK, QUEUE i DEQUEUE. U implementaciji ADT
DEQUEUE pokazano je kako se mogu stvarati generičke kolekcije podataka.




                                                                                                280
19 Razgranate strukture - stabla


Naglasci:
   • binarno stablo
   • stablo s aritmetičkim izrazima
   • leksički i sintaktički analizator aritmetičkih izraza
   • višesmjerna stabla
   • hrpa i prioritetni redovi


19.1 Definicija stabla
Stablo (eng. tree) je apstraktna struktura podataka koja se četo koristi za predstavljanje
hijerarhijskih odnosa, primjerice, slika 19.1 prikazuje hijerarhiju porodičnog stabla. Struktura
stabla je potpuno određena čvorovima stabla i vezama između čvorova. Čvorovi stabla sadrže
neki podatak, a veze određuju hijerarhijske odnose, prema sljedećem pravilu:
    1. svaki čvor (osim jednog) ima samo jednog prethodnika,
    2. svaki čvor ima određen broj slijednika.




                                          Slika 19.1 Porodično stablo

Ako nije ograničen broj slijednika, stablo se naziva općenito stablo (eng. general tree).
Prikazano je na slici 19.2. Ako čvorovi stabla imaju maksimalno k slijednika, stablo se naziva k-
stupanjsko stablo. U specijalnom slučaju, kada elementi stabla imaju maksimalno 2 slijednika,
stablo se naziva binarno stablo.

                 dubina                       korijen
                 0                              a


                 1                 b                        c
                                                                        x   unutarnji čvor

                                                                            vanjski čvor
                 2             d          e         f       g       h   y
                                                                            (list)

                 3        visina stabla                 i       j


                          Slika 19.2 Strukturalni prikaz općenitog stabla




                                                                                             281
Jedinstveni čvor koji nema prethodnika naziva se korijen (eng. root) stabla.

Čvorovi koji nemaju slijednika nazivaju se listovi stabla (eng. leaf) ili vanjski čvorovi, svi ostali
čvorovi su unutarnji čvorovi, a predstavljaju podstabla (eng. subtree).

Slijednik čvora se naziva dijete (eng. child), a prethodnik čvora se naziva roditelj (eng. parent).
Ako više čvorova ima istog roditelja oni su jedan drugome braća (ili sestre) (eng. siblings). U
binarnom stablu čvor može imati dvoje djece koja se nazivaju lijevo dijete (eng. left child) i
desno dijete (eng. right child).

Kada je važan redoslijed djece, stablo se naziva uređeno stablo (eng ordered tree).

Uzmemo li niz čvorova n1, n2, . . . , nk na način da je ni roditelj od ni+1 for 1 ≤ i < k, tada ovaj
niz tvori stazu od čvora n1 do čvora nk. Duljina staze je jednaka broju čvorova u stazi umanjeno
za jedan (od čvora do njega samog vodi staza nulte duljine).

Visina čvora u stablu jednaka je duljini najdulje staze od čvora do lista (visina stabla je jednaka
visini korijena stabla).

Dubina čvora je jednaka je duljini staze od korijena stabla do tog čvora. Ona određuje razinu
čvora.

Puno ili popunjeno k-stupanjsko stablo je stablo u kojem svi listovi imaju istu dubinu, a svaki
čvor ima svih k grana. U takvom stablu, na dubini 0 postoji 1 (k0 ) čvor, na dubini 1 postoji k1
čvorova, na dubini 2 postoji k2 čvorova..., pa ukupan broj unutarnjih čvorova Nu u stablu visine
h iznosi:
                                           h
                                                      k h −1
        N u = 1 + k 1 + k 2 + .. + k h −1 = ∑ k i =                                    (1)
                                          i =0         k −1

i maksimalni broj vanjskih čvorova (listova) iznosi kh.


19.2 Binarno stablo
Najčešće korišteni oblik stabla je binarno stablo. Definira se rekurzivno:

Definicija: Binarno stablo je:
    1. prazno stablo, ili
    2. sastoji se od čvora koji se naziva korijen i dva čvora koji se nazivaju lijevo i desno
         dijete, a oni su također binarna stabla (podstabla).




                         Slika 19.3 (a) Puno i (b) potpuno binarno stablo




                                                                                                 282
Ukupan broj unutarnjih čvorova punog binarnog stabla dobije se iz izraza (1). Tada je k=2 i
Nu=2h-1. Broj vanjskih čvorova (listova) jednak je Nv = 2h. Makimalni broj čvorova u punom
stablu, visine h, jednak je N = Nu+ Nu = 2h+1-1. Duljina staze od korijena do čvora jednaka je
visini stabla. Kod punog stabla visina iznosi h = log2(N+1)-1.

Kaže se da je binarno stablo potpuno 1 (eng. complete binary tree) ako je to puno stablo ili ako
od tog stabla može nastati puno stablo dodavanjem čvorova na na donjoj razini s desna. Ova
stabla su ilustrirana na slici 19.3. Kasnije ćemo vidjeti da se potpuna stabla mogu efikasno
implementirati pomoću nizova.
19.2.1 Programska implementacija binarnog stabla
    Čvorovi binarnog stabla se najčešće opisuju samoreferentnim dinamičkim strukturama,
primjerice u C jeziku čvor binarnog stabla se može definirati strukturom tnode:

         typedef char elemT;

         typedef struct tnode {
             elemT   elem;
             struct tnode * left;
             struct tnode * right;
          } Tnode, *TREE;

Ova struktura sadrži neki element i dva pokazivača: left pokazuje na lijevo dijete, a right
pokazuje na desno dijete. Ponekad se ovoj strukturi dodaje i pokazivač na roditelja.
Pokazivači služe uspostavljanju veza među čvorovima. Slika 19.4 prikazuje veze među
čvorovima jednog stabla.




      Slika 19.4 Dva načina prikaza binarnog stabla: a) strukturalni prikaz i b) prikaz pomoću
      pokazivača na strukturu tnode. U strukturalnom prikazu krugovi označavaju unutarnje
                         čvorove, a pravokutnici označavaju listove stabla

Binarno stablo se može prikazati i tekstualnim zapisom, prema algoritmu prefiksne notacije
stabla, na sljedeći način:

Algoritam: Prefiksna notacija stabla
   1. Ako čvor predstavlja prazno stablo (NULL pokazivač), tada
           zapiši prazne zagrade ():
   2. Ako čvor predstavlja list stabla, tada
           zapiši element lista.
   3. Ako čvor predstavlja unutarnji čvor, tada
           unutar zagrada zapiši element čvora te lijevi i desni čvor.

1
    Neki autori puno stablo nazivaju potpuno stablo, a potpuno stablo nazivaju "skoro potpuno stablo".




                                                                                                         283
Za stablo sa slike 19.4 vrijedi zapis:

      (A
           (B () D)                  ili           (A (B () D) (C E F) ).
           (C E F)
      )

     Ovaj oblik notacije se naziva prefiksna notacija jer se apstraktno može smatrati da element
unutarnjeg čvora stabla predstavlja prefiksni operator koji djeluje na svoju djecu.
     Prefiksna notacija se često koristi za zapis aritmetičkih izraza. Ona se vrši na način da se u
zagradama najprije zapiše operator, a zatim dva operanda. Operand može biti broj ili izraz. Ako
je operand izraz, ponovo se primjenjuje isto pravilo. Primjerice, izraz infiksne notacije 8*(7+3)
ima prefiksnu notaciju ( * 8 (+ 3 7) ). Općenito, svaki se aritmetički izraz može napisati u
prefiksnoj notaciji. Zbog ovog svojstva aritmetički izrazi se mogu pohraniti u binarnom stablu.
Primjer je prikazan na slici 19.5.




                                           infiksna notacija = (7 – 3) * (4 + 5)
                                           prefiksna notacija = ( * (- 7 3) (+ 4 5))


                           Slika 19.5 Binarno stablo aritmetičkog izraza

Programski se stablo se može formirati pomoću funkcije make_tnode().

      TREE make_tnode (elemT elem, TREE left, TREE right)
      {
          TREE t = malloc(sizeof(Tnode));
          if(t) { t->left = left; t->right = right; t->elem = elem;}
          return t;
      }

Argumenti funkcije su vrijednost elementa čvora (elem) i pokazivači na lijevo i desno dijete
(left, right). Funkcija vraća pokazivač na formirani čvor, ili NULL ako se ne može izvršiti
alokacija memorije. Kada se formira vanjski čvor stabla (list), tada se vrijednost argumenata
left i right postavlja na vrijednost NULL. U svim ostalim čvorovima bar jedan od ova dva
pokazivača mora biti različit od NULL.

Primjer: Stablo sa slike 19.4 može se formirati sljedećim naredbama:

      /* 1. formiraj listove */
      TREE l1 = make_tnode('7',            NULL,NULL);
      TREE l2 = make_tnode('3',            NULL,NULL);
      TREE l3 = make_tnode('4',            NULL,NULL);
      TREE l4 = make_tnode('5',            NULL,NULL);

      /* 2.formiraj podstabla */
      TREE t1 = make_tnode('-', l1, l2);
      TREE t2 = make_tnode('+', l3, l4);

      /* 3. formiraj korijena stabla */




                                                                                               284
TREE t = make_tnode('*', t1, t2);

Varijabla t je pokazivač na korijen stabla. Ostale varijable su pomoćne pokazivačke varijable
za formiranje listova i podstabala. Isto se može ostvariti jednom naredbom:

      TREE t = make_tnode('*',
            make_tnode('-',
                  make_tnode('7',            NULL,NULL) ),
                  make_tnode('3',            NULL,NULL) )),
            make_tnode('+',
                  make_tnode('4',            NULL,NULL),
                  make_tnode('5',            NULL,NULL))
            );

Uočite da se u ovom slučaju poziv funkcije make_tnode() obavlja prema prije opisanoj
prefiksnoj notaciji stabla.
    U prethodnom primjeru korisnik obavlja sve operacije formiranja stabla. Kasnije će biti
pokazano kako se može automatizirati postupak formiranja stabla.
Prazno stablo se formira naredbom:

      TREE     t = NULL;

pa funkcija

      int tree_is_empty(TREE t) {return t == NULL;}

vraća nulu ako je stablo prazno, ili nenultu vrijednost ako je stablo nije prazno.
     Korisna je i funkcija tree_is_leaf() kojom se određuje da li čvor predstavlja list
stabla.

      int tree_is_leaf(TREE t) {return !(t->left || t->right);}

tree_is_leaf()vraća nulu ako čvor t nije list stabla, ili nenultu vrijednost ako je čvor t list
stabla.
    Rekurzivna definicija stabala čini da je većinu operacija nad stablom najlakše definirati
rekurzivno.
Primjer: Funkcija tree_size(t) vraća broj čvorova binarnog stabla.

      int tree_size(TREE t)
      {
          if(tree_is_empty(t))   return 0 ;
          else return 1 + tree_size(t->left) + tree_size(t->right);
      }

Primjer: Funkcija print_prefiks() ispisuje prefiksnu notaciju stabla koje sadrži
aritmetički izraz. Realizirana je prema rekurzivnom algoritmu prefiksne notacije stabla.

      void print_prefiks (TREE t)
      {
          if(t == NULL)       {printf(" () "); return;}
          if(tree_is_leaf(t)) {printf("%c ",t->elem); return;}
          printf("( %c ",t->elem);
          print_prefiks (t->left);
          print_prefiks (t->right);
          printf(" )");




                                                                                            285
}

Zadatak: Testirajte primjenu funkcije print_prefilks() u programu:


      /* Datoteka: print_nodes.c
      int main()
      {
          TREE t = make_tnode('*',
              make_tnode('-',
                make_tnode('7', NULL,NULL),
                make_tnode('3', NULL,NULL)),
              make_tnode('+',
                make_tnode('4', NULL,NULL),
                make_tnode('5', NULL,NULL))
           );

            print_prefiks (t);

            return 0;
      }



19.2.2 Obilazak binarnog stabla
    Obilazak stabla je postupak kojim se na sve čvorove stabla primjenjuje neka operacija.
Kroz čvor se smije "proći" više puta, ali se operacija nad čvorom izvršava samo jedan put.
    Općeniti obrazac za rekurzivni obilazak binarnog stabla, počevši od čvora N (koji nije
prazan), temelji se na tri operacije:
    (L) rekurzivno obiđi lijevo stablo. Nakon ove operacije ponovo si u čvoru N.
    (D) rekurzivno obiđi desno stablo. Nakon ove operacije ponovo si u čvoru N.
    (P) procesiraj čvor N.
Ove tri operacije se mogu izvršiti bilo kojim redoslijedom. Ako se vrši (L) prije (D) tada je
obilazak s lijeva na desno (eng. left-right traversal), u suprotnom je obilazak s desna na lijevo
(right-left traversal). Prema redoslijedu operacija (L) (D) (P), razlikuju se tri načina obilaska
stabla:
                          Obilazak stabla         Redoslijed operacija
                          Pre-order ili prefiks       (P) (L) (D)
                          Post-order ili postfiks     (L) (D) (P)
                          In-order ili infiks         (L) (P) (D)

Slika 19.6 prikazuje stazu obilaska stabla za sva tri načina.




                                                                                             286
Slika 19.6 Tri načina obilaska stabla (za obilazak s lijeva na desno). Usmjerene crtkane linije
                             pokazuju redoslijed obilaska čvorova

Obilazak stabla se programski može realizirati funkcijom tree_traverse():

      enum torder {PREORDER,INORDER,POSTORDER};

      void tree_traverse(TREE t, int order, void (*visit)(TREE))
      {
          if (t != NULL){
              if (order == PREORDER) visit(t);
              tree_traverse(t->left, order, visit);
              if (order == INORDER) visit (t);
              tree_traverse(t->right, order, visit);
              if (order == POSTORDER) visit(t);
          }
      }

Funkciji tree_traverse() prvi je argument pokazivač čvora stabla. Drugi argument je
vrijednost iz skupa {PREORDER, INORDER, POSTORDER}, kojom se određuje način obilaska
stabla. Treći argument je pokazivač na tzv. visit funkciju kojoj je argument pokazivač čvora
na koji ta funkcija djeluje.
Primjerice, "visit" funkcija print_char() ispisuje vrijednost elementa čvora.

      void print_char(TREE t)
      {
         printf("%c ",t->elem);
      }

Zadatak: Funkcije print_char() i tree_traverse() uvrstite u prethodni program
"print_nodes.c", a u funkciji main(), umjesto naredbe print_prefiks(), napišite naredbe:

      printf("nPREORDER: "); tree_traverse(t, PREORDER, print_el);
      printf("nINORDER: ");   tree_traverse(t, INORDER,   print_el);
      printf("nPOSTORDER: "); tree_traverse(t, POSTORDER, print_el);

Nakon izvršenja programa dobije se ispis:

      PREORDER: * - 7 3 + 4 5
      INORDER: 7 - 3 * 4 + 5
      POSTORDER: 7 3 - 4 5 + *

Ovaj primjer pokazuje da se pomoću binarnog stabla aritmetički izrazi lako pretvaraju u
prefiksni, infiksni i postfiksni oblik.

19.2.3 Brisanje stabla
    Brisanje stabla je operacija kojom se dealociraju svi čvorovi stabla, koje time postaje
prazno stablo. Ovu operaciju treba izvesti tako da se najprije dealociraju listovi stabla, a tek
onda unutarnji čvorovi. Slika 19.4 pokazuje da se to može izvesti postorder obilaskom stabla.
Operacija brisanja stabla je implementirana u funkciji tree_delete().

      TREE tree_delete( TREE t )
      {
          if( t != NULL )    {
              tree_delete( t->left );
              tree_delete( t->right );




                                                                                             287
free( t );
            }
            return NULL;
      }

Kod primjene ove funkcije treba uvijek voditi računa da se ona koristi u obliku

      t = tree_delete(t);

jer na taj način t postaje jednako NULL, što označava prazno stablo.

19.2.4 Vrednovanje aritmetičkog izraza
     Gotovo svi moderni kompilatori i interpreteri, u prvoj fazi kompiliranja, pretvaraju izvorni
kôd u oblik koji je podesan za optimiranje kôda i generiranje strojnog kôda ili interpretiranje
kôda. Praksa je pokazala da je najbolji način da se izvorni kod najprije pretvori u jedan oblik
"sintaktičkog" stabla. Dio tog sintaktičkog stabla, kojim se bilježi aritmetičke izraze, sličan je
ovdje opisanom binarnom stablu.
     U prethodnim primjerima, zbog jednostavnosti, element čvora je bio tipa char. Ako se pak
želi formirati stablo koje će sadržavati realne brojeve, tada treba izmijeniti definiciju tipa
elementa čvora. Kod aritmetičkih izraza potrebno je da čvor sadrži ili znak operatora ili realni
broj. To se može ostvariti tipom koji je unija elemenata tipa char (ili int) i tipa double.
Koristit će se definicija:

      typedef union elemT {
           int op;
           double num;
      } elemT;

      typedef struct tnode {
          elemT   elem;
          struct tnode * left;
          struct tnode * right;
      } Tnode, *TREE;


Za formiranje čvora koristit će se funkcije make_opnode() i make_numnode().

      TREE make_opnode (int op, TREE left, TREE right)
      {   /* formira unutarnji čvor koji sadrži operator op*/
          /* vraća pokazivač na taj čvor, ili NULL ako je greška*/
          TREE t = malloc(sizeof(Tnode));
          if(t) { t->left = left; t->right = right; t->elem.op = op;}
          return t;
      }

      TREE make_numnode (double num)
      {   /* formira vanjski čvor koji sadrži realni broj - num*/
          /* vraća pokazivač na taj čvor, ili NULL ako je greška */
          TREE t = malloc(sizeof(Tnode));
          if(t) { t->left = NULL; t->right = NULL; t->elem.num = num;}
          return t;
      }

    Programski jezici Lisp i Sheme koriste prefiksnu notaciju za zapis svih svojih konstrukcija,
a pohrana i izračunavanje vrijednosti izraza (tzv. evaluacija) vrši se obilaskom binarnog stabla.
Koristi se sljedeći algoritam:




                                                                                              288
Algoritam: Vrednovanje aritmetičkog izraza koji je zapisan u binarnom stablu
     Temeljni slučaj: Ako čvor sadrži operand, vrati vrijednost operanda
     Pravilo rekurzije: Ako čvor sadrži operator, rekurzivno dobavi lijevi i desni operand,
     izvrši operaciju koju određuje operator i vrati rezultat

Ovaj algoritam je implementiran u funkciji evaluate().

      double evaluate(TREE t)
      { /* vraća vrijednost izraza koji je u stablu t*/
          double x,y;
          int op;

           if(tree_is_leaf(t))              /* ako je t list stabla                */
               return t->elem.num;          /* vrati vrijednost operanda           */
                                            /* inače,                              */

           op = t->elem.op;          /* dobavi operator            */
           x = evaluate(t->left);    /* dobavi lijevi operand      */
           y = evaluate(t->right);   /* dobavi desni operand       */
           switch (op) {             /* izračunaj vrijednost izraza*/
               case '+': return x+y;
               case '-': return x-y;
               case '*': return x*y;
               case '/': return (y == 0)? 0 : x/y;
               default: printf("nGreska"); return 0;
           }
      }

Primjer: U programu evaluate.c, pokazana je primjena funkcije evaluate() za proračun
vrijednosti prefiksnog izraza (*(- 7.5 3)(+ 4 5.1)). Program koristi funkcije i definicije
koje su definirane u datotekama "prefix_tree.h" i "prefix_tree.c".

      /***************************************************************/
      /* Datoteka: prefix_tree.h                                     */
      /***************************************************************/
      #ifndef _PREFIX_TREE_H_
      #define _PREFIX_TREE_H_

      typedef union elementip
      {    int op;
           double num;
      }elemT;

      typedef struct tnode {
          elemT   elem;
          struct tnode * left;
          struct tnode * right;
      } Tnode, *TREE;

      int tree_is_leaf(TREE t);
      void print_prefiks (TREE t);
      TREE make_opnode (int op, TREE left, TREE right);
      TREE make_numnode (double num);
      double evaluate(TREE t);

      #endif

      /***************************************************************/




                                                                                        289
/* Datoteka: prefix_tree.c                                     */
/***************************************************************/
#include <stdio.h>
#include <stdlib.h>

#include "prefix_tree.h"

int tree_is_leaf(TREE t) {return !(t->left || t->right);}

void print_prefiks (TREE t)
{
    if(t == NULL)
        {printf(" () "); return;}
    if(tree_is_leaf(t))
        {printf("%f ",t->elem.num);   return;}
    printf("( %c ",t->elem.op);
    print_prefiks (t->left);
    print_prefiks (t->right);
    printf(" )");
}

TREE make_opnode (int op, TREE left, TREE right)
{
    TREE t = malloc(sizeof(Tnode));
    if(t) { t->left = left; t->right = right; t->elem.op = op;}
    return t;
}

TREE make_numnode (double num)
{
    TREE t = malloc(sizeof(Tnode));
    if(t) { t->left = NULL; t->right = NULL; t->elem.num = num;}
    return t;
}

TREE tree_delete( TREE t )
{
    if( t != NULL )    {
        tree_delete( t->left );
        tree_delete( t->right );
        free( t );
    }
    return NULL;
}

double evaluate(TREE t)
{
    double x,y;
    int op;

   if(tree_is_leaf(t))      /* operand*/
        return t->elem.num;
   /*else analyse operator*/
   op = t->elem.op;
   x = evaluate(t->left);
   y = evaluate(t->right);
   switch (op) {
       case '+': return x+y;
       case '-': return x-y;




                                                               290
case '*': return x*y;
                 case '/': return (y==0)? 0 : x/y;
                 default: printf("nGreska"); return 0;
            }
      }

      /************************************************************/
      /* Datoteka: evaluate.c                                      */
      /************************************************************/
      #include <stdio.h>
      #include <stdlib.h>

      #include "prefix_tree.c"

      int main()
      {
          TREE t = make_opnode('*',
              make_opnode('-', make_numnode(7.5), make_numnode(3)),
              make_opnode('+', make_numnode(4), make_numnode(5.1))
           );

            printf("nVrijednost prefiksnog izraza:n");
            print_prefiks(t);
            printf("njednaka je: %f", evaluate(t));

            return 0;
      }

Program se kompilira komandom:

      c:> cl evaluate.c prefix_tree.c

Nakon izvršenja programa "evaluate.exe" dobije se ispis:

      Vrijednost prefiksnog izraza:
      ( * ( - 7.500000 3.000000 )( + 4.000000 5.100000                       ) )
      jednaka je: 40.950000



19.3 Interpreter prefiksnih izraza
Sada će biti pokazano kako se realizira interpreter prefiksnih izraza koje unosi korisnik pomoću
tipkovnice ili iz datoteke.

19.3.1 Dobavi-vrednuj-ispiši
Rad interpretera se obično opisuje tzv. dobavi-vrednuj-ispiši petljom (eng. read-eval-print loop),
koja je definirana sljedećim algoritmom:

Algoritam : Interpreter s dobavi-vrednuj-ispiši petljom
   Ponavljaj:
   1. Dobavi naredbu – dobavi izvorni kod naredbe i interno ga spremi u prikladnu
       podatkovnu strukturu. Ako je primljen zahtjev za kraj rada, završi petlju.
   2. Vrednuj naredbu – izvrši operacije na internoj podatkovnoj strukturi, koje rezultiraju
       nekom vrijednošću. Primjerice, obilaskom binarnog stabla izračunaj vrijednost
       aritmetičkog izraza.
   3. Ispiši rezultat.




                                                                                              291
U prethodnoj sekciji je pokazano kako se vrednuje i ispisuje rezultat aritmetičkog izraza koji je
zapisan u binarnom stablu. Sada će biti pokazano kako se realizira "dobavi" operacija, tj. kako
se od korisnika dobavlja naredba te kako se vrši leksička i sintaktička analiza izvornog kôda
naredbe, koja rezultira binarnom stablom aritmetičkog izraza.

U leksičkoj i sintaktičkoj analizi koristit će se specifikacije u BNF notaciji, koja je opisana u
poglavlju 6. U BNF notaciji gramatička se pravila zapisuju u obliku produkcije:
       A:α
gdje je A neterminalni simbol, a α predstavlja niz terminalnih ili neterminalnih simbola X1
X2...Xn .
Ako je simbol Xi neterminalni simbol, zapisuje se kurzivom. Na taj se način razlikuje od
terminalnog simbola. Ako je simbol Xi opcioni, što znači da ne mora biti u produkciji, zapisuje
sa sufiksom opt, tj. Xopt.
Ako je neterminalni simbol A definiran s više alternativnih produkcija A : α1 , A : α2 ... A : αn.,
koristi se notacija:

       A : α1 | α2 | ...| αn

U gramatičkim analizama opcioni simbol se tretira kao neterminalni simbol koji je definiran
produkcijom:

       Xopt : X | ε

gdje ε označava "praznu" produkciju. Kaže se da je Xopt definiran ili kao X ili kao prazna
produkcija (ε-produkcija).

19.3.1 Leksička analiza prefiksnih aritmetičkih izraza
Razmotrimo jedan prefiksni izraz, s realnim brojevima, koji je napisan u više linija teksta:

       (* (- 3.7 35)                               # komentar
          (/ 8 (+ 9.1 -5) ) )                      # .......
       $                                           # kraj unosa

Ovaj izraz se može pročitati na sljedeći način: "pomnoži razliku 3.7 i 35 i kvocijent od 8 i
zbroja 9.1 i -5". Nakon ovog izraza slijedi znak $. On označava kraj unosa.

U prikazanom prefiksnom aritmetičkom izrazu su zastupljene sljedeće leksičke i gramatičke
kategorije:

 leksem                   značaj                        token - gramatički   vrijednost tokena
 (iz ulaznog toka)                                      terminalni simbol    ( ili atribut tokena)
 (, ),                    zagrade za grupiranje         '( ' ')'             '(' ')'
                          izraza
 3.7     35     -5        realni broj                   NUM                  numerička     vrijednost
                                                                             realnog broja
 +,     -,                aritmetički operatori         OPERATOR             '+', '-',
 *,     /,                                                                   '*', '/'
 $,     EOF               oznak kraja unosa             QUIT                 '$' ili EOF
                          (EOF kod datoteka)
 razmak, tab,             separatori (bijeli
 nova linija              znakovi: 'n', 't', 'n' )
 # .......                komentar (počinje




                                                                                                     292
znakom # )

Leksička analiza je proces kojim se sekvencijalno u nizu ulaznih znakova prepoznaje neki
leksem i gramatički simbol koji on predstavlja, tj. token. Primjerice, leksem "37.5" predstavlja
token NUM. Ako je potrebno, uz token se bilježi i njegova vrijednost, koja predstavlja atribut
tokena.
Leksički analizator se može jednostavno implementirati pomoću funkcije getToken() i
sljedećih deklaracija:

      /**************************************************************/
      /* datoteka: prefix_lex.h                                     */
      /**************************************************************/
      #ifndef _PREFIX_LEX_H_
      #define _PREFIX_LEX_H_

      /* tokeni (terminalni simboli) */
      #define NUM      255
      #define OPERATOR 256
      #define QUIT     257

           /* globalne varijable*/
      extern FILE *input = stdin;    /* ulazni tok        */
      extern elemT tokenval;       /* vrijednost tokena */
      extern char lexeme[];           /* leksem tokena     */

      int getToken();
      /* Pri svakom pozivu funkcija vraća sljedeći token iz ulaznog
      toka,
         ili 0, ako je registriran nepredviđeni znak.
         Vrijednost tokena i leksem tokena se zapisuju u globalne
      varijable
         tokenval i lexeme.
      */
      #endif


Implementacija funkcije getToken() je u datoteci "prefix_lex.c".

      /**************************************************************/
      /* datoteka prefix_lex.c                                      */
      /**************************************************************/
      #include <stdio.h>
      #include <stdlib.h>
      #include <ctype.h>


      #include "lexan.h"

      FILE *input = stdin;            /* ulazni tok        */
      elemT tokenval;               /* vrijednost tokena */
      char lexeme[64];                /* leksem tokena     */

      int getToken()
      {
          int ch;
          while(1) {
              ch = getc(input);




                                                                                            293
/* preskoči bijele znakove */
                     if(ch == ' ' || ch =='t' || ch =='n' || ch =='r')
                         continue;

                     /* zapamti prvi znak */
                     lexeme[0]=(char)ch; lexeme[1]='0';

                     if (isdigit(ch)) { /* dobavi broj */
                         int i = 1;     /* prva znamenka je u lexeme[0]*/
                         ch = getc(input);
                         while(isdigit(ch) && i<62) {
                             lexeme[i++]=(char)ch; ch = getc(input);
                         }
                         if(ch == '.') {
                            lexeme[i++]=(char)ch; ch = getc(input);
                            while(isdigit(ch) && i<62){
                                lexeme[i++]=(char)ch; ch = getc(input);
                            }
                         }
                         lexeme[i] = '0';
                         tokenval.num = atof(lexeme);
                         if (ch != EOF) ungetc(ch,input);
                         return NUM;
                     } else if(ch == '+' || ch == '-'
                            || ch == '*' || ch == '/' ) {
                         tokenval.op = ch;
                         return OPERATOR;
                     }else if(ch == '(' || ch == ')') {
                         return ch;
                     }else if(ch == '$' || ch == EOF) {
                         return QUIT;
                     }else if(ch == '#'){ /*komentar do kraja linije */
                         while (ch != 'n') ch = getc(input);
                     }else
                        return 0; /* nedozvoljen znak */
               }
        }

     U funkciji getToken() se unutar petlje dobavlja znak iz toka naredbom getc(input).
Petlja se ponavlja ako su na ulazu "bijeli znakovi" ili ako je dobavljen znak '#', koji označava
početak komentara. U svim ostalim slučajevima petlja se prekida nakon analize dobavljenog
znaka, a analiza se vrši na sljedeći način:
    •       Ako je dobavljeni znak numerička znamenka, dobavlja se realni broj oblika:

                realni_broj :      niz_znamenki decimalni_dioopt
                decimalni_dio :    . niz_znamenki opt

            Bilježenje broja se vrši prema prethodnom pravilu: najprije se bilježi niz znamenki, a
            zatim, ako iza njih slijedi točka, bilježi se decimalni dio. Leksem broja se bilježi u
            stringu lexeme, njegova numerička vrijednost u tokenval.num, a funkcija
            getToken() vraća token NUM.
    •       Ako je dobavljeni znak operator +, -, * ili / , getToken() vraća token OPERATOR,
            a vrijednost tokena bilježi u tokenval.op.
    •       Ako je dobavljeni znak lijeva ili desna zagrada, getToken() vraća ASCII vrijednost
            znaka, što služi kao oznaka tokena.
    •       Ako je dobavljeni znak &, ili ako je kraj datoteke, getToken() vraća token QUIT.




                                                                                              294
•    U svim ostalim slučajevima getToken() vraća vrijednost nula, što označava
         nepoznati token.

    Funkcija getToken() ima jednostavnu strukturu. Lako se može proširiti da obuhvati širi
skup tokena. Zbog toga, ona može poslužiti kao obrazac prema kojem se može se napisati
kvalitetni leksički analizator za znatno složeniji programski jezik.

19.3.3 Sintaktička analiza prefiksnih izraza
U skladu s BNF notacijom gramatika naredbe za proračun prefiksnih aritmetičkih izraza glasi:

        Naredba :   ( Izraz )
                    | QUIT
        Izraz :     OPERATOR Operand Operand
        Operand :   NUM
                    | - NUM
                    | ( Izraz )

gdje su Naredba, Izraz i Operand neterminalni simboli, a zagrade ( ), OPERATOR, NUM i
QUIT su terminalni simboli (tokeni), koji postoje u ulaznom toku.

Naredba je startni simbol gramatike. Desna strana njegove produkcije određuje temeljni oblik
rečenice koja se može ostvariti ovom gramatikom. Moguća su dva oblika rečenice: "QUIT" –
što predstavlja naredbu za kraj rada interpretera i "( Izraz )" – što predstavlja naredbu da se
izračuna vrijednost Izraza. Ovaj drugi oblik sadrži neterminalni simbol Izraz. Zbog toga, to nije
stvarna rečenica jezika, već se naziva rečenična forma (eng. sentential form).

Izraz označava pravilo po kojem u ulaznom toku, na mjestu gdje se očekuje prefiksni izraz,
simboli moraju biti poredani tako na najprije bude simbol OPERATOR, a iza njega moraju biti
dva operanda. Ako u rečeničnoj formi (Izraz) zamijenimo neterminalni simbol Izraz s desnom
stranom produkcije, tada rečenična forma ima oblik (OPERATOR Operand, Operand).

Operand je neterminalni simbol koji je definiran pravilom po kojem u ulaznom toku, na mjestu
gdje se očekuje operand, može biti ili broj (NUM) , ili negativni broj (-NUM) ili zagradama
omeđeni Izraz.

Rečenice, koje može generirati gramatika, su sve rečenice koje se mogu dobiti iz rečenične
forme, na način da se u rečeničnoj formi neterminalni simboli zamijene s desnom stranom
njihove produkcije, i to sve dok rečenična forma ne sadrži isključivo terminalne simbole.
Opisani proces se naziva derivacija, a označava se simbolom ⇒. Evo primjera dvije moguće
derivacije:

Naredba                                                  Naredba
⇒ ( Izraz )                                              ⇒ ( Izraz )
⇒ ( OPERATOR    Operand Operand )                        ⇒ ( OPERATOR Operand Operand )
⇒ ( OPERATOR    NUM     Operand )                        ⇒ ( OPERATOR NUM Operand )
⇒ ( OPERATOR    NUM     ( Izraz ) )                      ⇒ ( OPERATOR NUM      -NUM )
⇒ ( OPERATOR    NUM ( OPERATOR Operand Operand ) )
⇒ ( OPERATOR    NUM ( OPERATOR NUM Operand ) )
⇒ ( OPERATOR    NUM ( OPERATOR NUM NUM ) )
⇒ ( OPERATOR    NUM ( OPERATOR NUM NUM ) )




                                                                                             295
U svakoj rečeničnj formi podcrtan je simbol koji se zamjenjuje s desnom stranom njegove
produkcije.
      Različitost mogućih derivacija je posljedica alternativnih definicija neterminalnog simbola
Operand. Primijetite da je moguć beskonačan broj različitih derivacija jer je Izraz definiran
rekurzivno (Izraz je definiran pomoću Operanda, a Operand pomoću Izraza).
      Prvi zadatak sintaktičke analize je otkriti da li postoji derivacija koja odgovara ulaznom
nizu tokena. Ona se može vršiti na više načina. Ovdje će biti korištena tzv. "top-down" analiza,
u kojoj se ulazni niz promatra s lijeva na desno, jedan po jedan token, i istovremeno vrši
derivacija rečenične forme koja slijedi iz položaja tokena u ulaznom nizu.
      Pogledajmo najprije kako čovjek može izvršiti sintaktičku analizu. U "top-down" analizi
polazi se od startnog simbola gramatike i analizira desna strana njegove produkcija, koja
predstavlja temeljnu rečeničnu formu. Simboli iz rečenične forme se analiziraju jedan po jedan,
s lijeva na desno, i uspoređuju s ulaznim tokenom. Na temelju usporedbe rečeničnog i ulaznog
simbola, vrše se dvije radnje:
Prihvati - Ako u rečeničnoj formi slijedi token, on mora biti jednak ulaznom tokenu. Ako je to
           zadovoljeno, dobavlja se sljedeći ulazni token, inače ulazni niz nije u skladu s
           gramatikom.

Proširi - Ako u rečeničnoj formi slijedi neterminalni simbol, umjesto njega se u rečeničnu
          formu umeće desna strana produkcije. Međutim, ako je neterminalni simbol
          definiran s više alternativnih produkcija, tada se umeće ona alternativna produkcija
          koja generira rečenice koje mogu počinjati s ulaznim tokenon. Ako neterminalni
          simbol predstavlja opcioni simbol, odnosno simbol koji može generirati praznu
          rečenicu, tada se umjesto njega umeće desna strana produkcija samo u slučaju ako
          taj simbol generira rečenice koje mogu počinjati s ulaznim tokenom, inače se taj
          simbol odstranjuje i analizira se sljedeći simbol.
    Analiza se nastavlja sve dok sljedeći ulazni token ne postane posljednji token ulaznog niza.
Analiza je uspješna ako rečenična forma isključivo sadrži tokene.
    Primjer ovakve analize prikazuje slika 19.7. Prvi stupac sadrži rečeničnu formu, koja
nastaje tijekom derivacije. Drugi stupac opisuje operaciju kojem se prihvaća ili proširuje
rečenični oblik. Treći stupac prikazuje sljedeći ulazni token. U svakoj rečeničnoj formi
podebljan je token kojeg se prihvaća i podcrtan je neterminalni simbol kojeg se zamjenjuje
desnom stranom produkcije.


Derivacija rečenične forme za ulazni niz ( * 9 (+ 8 3) )    Operacija                       Ulaz

Naredba                                                     proširi Naredba                  (
⇒ ( Izraz )                                                 prihvati: (, proširi Izraz       *
⇒ ( OPERATOR Operand Operand )                              prihvati OP, proširi Operand     9
⇒ ( OPERATOR NUM    Operand )                               prihvati NUM, proširi Operand    (
⇒ ( OPERATOR NUM    ( Izraz ) )                             prihvati (, proširi Izraz        +
⇒ ( OPERATOR NUM ( OPERATOR Operand Operand ) )             prihvati OP, proširi Operand     8
⇒ ( OPERATOR NUM ( OPERATOR NUM Operand ) )                 prihvati NUM, proširi Operand    3
⇒ ( OPERATOR NUM ( OPERATOR NUM NUM ) )                     prihvati NUM, prihvati )         )
⇒ ( OPERATOR NUM ( OPERATOR NUM NUM ) )                     prihvati ) .                     )


Slika 19.7 Top-down sintaktička analiza – gramatička derivacija za ulazni niz (*9(+8 3)).


    Prethodno izvršena analiza naziva se LL(1) analiza. Ovaj naziv slijedi iz činjenice da je
ulazni niz analiziran s lijeva (L) i da je u rečeničnoj formi vršena supstitucija najljevijeg




                                                                                             296
neterminalnog simbola (L) na temelju poznavanja samo jednog sljedećeg tokena (1). Gramatika,
koja se može analizirati na ovaj način, naziva se LL(1) gramatika.
Tri su temeljna zahtjeva da bi gramatika bila LL(1):
    1. Neterminalni simbol ne smije biti definiran s više alternativnih produkcija koje počinju
         s istim simbolom, jer se tada ne može znati koju produkciju primijeniti na temelju
         poznavanja samo jednog sljedećeg ulaznog tokena. Primjerice, u produkciji
                A:tB|tC
         obje alternative počinju s istim tokenom t. Ova restrikcija se može lako otkloniti ako se
         produkcija transformira oblik
               A : t A1
               A1 : B | C

    2. Neterminalni simbol ne smije biti definiran produkcijom koja počinje s tim istim
       simbolom. Takve produkcije se nazivaju lijevo rekurzivne produkcije. Primjerice, u
       produkciji
            A: x | A+x
       neterminal A može biti definiran s x, zbog toga alternativa, A + x, također može početi s
       x. Prema tome, ne može se odrediti koju produkciju primijeniti. To vrijedi bez obzira
       koliko ulaznih tokena poznajemo, jer se A može proširiti s A+x proizvoljan broj puta.

    3. Ako u nizu simbola postoji opcioni simbol, odnosno simbol koja generira praznu
       rečenicu, tada se mora razlikovati skup tokena s kojim mogu počinjati rečenice koje
       generira taj simbol i skup tokena s kojim mogu počinjati rečenice koje generira simbol
       koji slijedi iza njega. Ako to nije ispunjeno ne može se odrediti da li analizirati opcioni
       simbol ili simbol koji slijedi iza njega.

Bez obzira na ove gramatičke restrikcije, LL(1) gramatika je korištena u izradi mnogih
interpretera i kompilatora, jer se za ovaj tip gramatike može jednostavno napisati program koji
vrši sintaktičku analizu.

19.3.4 Rekurzivno silazni parser
Program koji obavlja sintaktičku analizu, za bilo koji ulazni niz tokena, naziva se sintaktički
analizator ili parser. Parser za gramatiku tipa LL(1) može se realizirati u obliku koji se naziva
rekurzivno silazni parser (eng, recursive descent parser). Program parsera se sastoji od
potprograma koji imaju imena neterminalnih simbola gramatike. Zadatak je svakog
potprograma, počevši od startnog simbola gramatike, da provjeri može li se "proširiti" rečenična
forma s desnom stranom produkcije, a na temelju poznavanja sljedećeg ulaznog tokena.
Formalno se taj postupak može opisati na sljedećim algoritmom:

Algoritam: Definiranje rekurzivno silaznog parsera za LL(1) gramatiku

1. Ako je neterminalni simbol definiran produkcijom oblika
       A:α
gdje je α općenito predstavlja niz od jedan ili više tokena ili neterminalnih simbola, potprogram
A() ima oblik:

      A() {   Analiziraj α ; }

Analiziraj α; simbolički označava naredbu koja ima oblik:

      Ako je α token t, tada




                                                                                               297
Analiziraj t; ≡  Ako je ulazni token jednak t, dobavi sljedeći ulazni token
                              inače dojavi grešku
      Ako je α neterminalni simbol N, tada
            Analiziraj N; ≡ N(); /*poziv potprograma za neterminalni simbol N*/

      Ako je α = X1 X2...Xn , gdje Xi može biti token ili neterminalni simbol, tada

             Analiziraj X1 X2...Xn ; ≡ Analiziraj X1 ; Analiziraj X2 ; ... Analiziraj Xn ;

       Ako je α opcioni simbol Xopt , tada

             Analiziraj Xopt ; ≡
             Ako X generira rečenice koje mogu počinjati s ulaznim tokenom, tada
                   Analiziraj X;

2. Ako je neterminalni simbol definiran s alternativnim produkcijama, potprogram prvo treba
odrediti koja alternativna produkcija odgovara položaju sljedećeg ulaznog tokena, a zatim se
provjerava odabrana produkcija. Formalno, ako produkcija ima oblik

      A: α1 | α2 | .. αn

gdje su α1, α2.. αn desne strane alternativnih produkcija, potprogram A() ima oblik:

      A()
      {
             Ako α1 generira rečenice koje mogu počinjati s ulaznim tokenom
                    Analiziraj α1;
             inače, ako α2 generira rečenice koje mogu počinjati s ulaznim tokenom
                    Analiziraj α2;
               ....
             inače, ako αn generira rečenice koje mogu počinjati s ulaznim tokenom
                    Analiziraj αn;
             inače, dojavi grešku
      }

3. Rekurzivni silazni parser se realizira pozivom potprograma koji ima ime startnog simbola
gramatike. Taj potprogram se definira prema prethodna dva pravilu, uz dopunu da prva naredba
mora biti dobava sljedećeg ulaznog tokena, koji se tretira kao globalna varijabla.

4. Dojava greške, u najprostijem obliku, je operacija kojom se ispisuje poruka o nastaloj greški i
prekida izvršenje programa. Postoje naprednije metode obrade greške, u kojima se ne prekida
izvršenje programa, međutim one neće biti opisane u ovoj knjizi.

Primjer: Parser prefiksnih aritmetičkih izraza.
Parser koristi funkciju getToken() za dobavu sljedećeg tokena, koji se bilježi u globalnoj
varijabli next_token. Globalna varijabla tokenval sadrži vrijednost tokena. Funkcija za
dojavu sintaktičke greške i prekid programa naziva se syn_error().


      void syn_error(char *poruka) /* Dojava sintaktičke greške */
      {                               /* u obliku "Greska: poruka" */
        printf("nGreska: %s",poruka);




                                                                                              298
exit(1);                              /* i prekid programa             */
      }

      int next_token;        /* sljedeći ulazni token */

    Za rad parsera definirane su tri funkcije: Naredba(), Izraz() i Operand(), čija
imena su neterminalni simboli gramatike, a prema algoritmu za rekurzivno silazni parser.

      void Izraz();
      void Operand();

      void Naredba()      /* Naredba : ( Izraz ) | QUIT*/
      {
          next_token = getToken();
              if(next_token == '(') {
              next_token = getToken();
              Izraz();
              if(next_token != ')') syn_error(" nedostaje ')' !");
          }
          else if(next_token == QUIT)
              return;
          else
              syn_error(" nedozvoljen simbol!");
      }

      void Izraz()   /* Izraz : OPERATOR Operand Operand */
      {
           if(next_token == OPERATOR) next_token = getToken();
           else else syn_error(" nedozvoljen operator!");
           Operand();
           Operand();
       }

      void Operand()   /* Operand : NUM |-NUM | ( Izraz ) */
      {
          if( next_token == NUM) {
              next_token = getToken();
          }
          else if( next_token == OPERATOR && tokenval.op == '-') {
              next_token = getToken();
              if( next_token == NUM) next_token = getToken();
              else syn_error("pogresno unesen broj!");
          }
          else if( next_token == '(') {
              next_token = getToken();
              Izraz();
              if(next_token == ')') next_token = getToken();
              else syn_error(" nedostaje ')' !");
          }
          else
              syn_error("pogresno unesen broj!");
      }

Postupak parsiranja se može provjeriti sljedećim programom:

      int main()
      {
         printf("Otkucaj prefiksni izraz, primjerice (*5(- 8 3.1))n");
         Naredba();




                                                                                      299
printf("Parsiranje uspjesno!");
            return 0;
      }

Nakon izvršenja ovog programa korisnik će vidjeti samo dva moguća ispisa:

      Ako je izraz unesen u skladu s gramatikom bit će ispisano:

             Parsiranje uspjesno!

      inače, bit će dojavljena greška, primjerice u obliku:

             Greska: nedostaje ) !

     Ovaj parser jedino zna prepoznti da li je ulazni niz u skladu s zadanom gramatikom.
Međutim, mi želimo da on obavi još jednu radnju - da generira binarno stablo koje sadrži
aritmetički izraz. Kako se to može obaviti, bit će pokazano u sljedećem odjeljku.

19.3.5 Semantičke akcije
     Radnje, koje slijede iz značenja gramatički ispravnog zapisa, nazivaju se semantičke akcije.
U programskim jezicima često je dovoljno sagledati samo dijelove rečenice da bi se izvršile
semantičke akcije. Primjerice, kod analize aritmetičkih izraza, koje želimo prikazati pomoću
binarnog stabla, znamo da kada bude prepoznat token NUM tada treba formirati čvor koji je list
binarnog stabla. Taj čvor sadrži atribut tokena NUM (tj. njegovu numeričku vrijednost).
Postavlja se pitanje: kome pridjeliti pokazivač na taj čvor? Možemo rezonirati na sljedeći način:
ako je prepoznat NUM, to ujedno znači da je prepoznata produkcija Operand : NUM, pa
apstraktno možemo uzeti da Operand ima semantički atribut (ili vrijednost) koja je jednaka
pokazivaču na čvor stabla.
     Slično rezoniramo i ako je prepoznat operator i dva operanda. Tada treba formirati
unutarnji čvor binarnog stabla. Pošto to ujedno znači da je prepoznata produkcija za Izraz,
možemo uzeti da Izraz ima semantički atribut koji je, također, pokazivač na čvor stabla.
     Konačno, možemo uzeti da simbol Naredba ima semantički atribut tipa pokazivača na čvor
stabla, i to na korijen stabla, jer analizom produkcije Naredba : (Izraz) | QUIT, se dobije
pokazivač na čvor stabla kojim počinje Izraz. Ako je pak prepoznata naredba QUIT, tada se ne
formira stablo, pa u tom slučaju semantički atribut simbola Naredba mora biti NULL
pokazivač.
Uvedimo sada sljedeće oznake semantičkih atributa:
   • NUM:num je vrijednost tokena NUM               (dobije se iz globalne varijable
      tokenval.num)
   • OPERATOR:op je vrijednost tokena OPERATOR (dobije se iz globalne varijable
      tokenval.op)
   • Naredba:t je pokazivač korijena binarnog stabla.
   • Izraz:t je pokazivač unutarnjeg čvora binarnog stabla, koji sadrži operator i
      pokazivače na operande.
   • Operand:t je pokazivač na čvor binarnog stabla, koji je ili list stabla ili pokazivač na
      čvor koji sadrži izraz.

Sada se semantičke akcije mogu specificirati zajedno sa specifikacijom gramatike, kao što je
prikazana na slici 19.8.

Gramatika                                 Pseudo kôd za semantičke akcije
Naredba :




                                                                                             300
( Izraz )                          Naredba:t = Izraz:t;
          | QUIT                             Naredba:t = NULL;
Izraz :
          OPERATOR Operand Operand
                                             Izraz:t = tree_opnode(OPERATOR:op,
Operand :                                           Operand:t1, Operand:t2);
      NUM
      | - NUM                                Operand:t = tree_numnode(              NUM:num);
      | ( Izraz )                            Operand:t = tree_numnode(              NUM:num);
                                             Operand:t = Izraz:t;


              Slika 19. 8 Semantičke akcije za generiranje stabla s aritmetičkim izrazima.

Prikazane semantičke akcije se mogu uvrstiti u izvorni kôd parsera na sljedeći način:

1. Za sve neterminalne simbole, kojima je atribut stablo izraza, deklariraju se parsne funkcije
koje vraćaju pokazivač stabla. Deklaracija triju funkcija parsera glasi:

                 TREE Naredba();
                 TREE Izraz();
                 TREE Operand();

Vrijednost koju ove funkcije vraćaju je vrijednost semantičkog atributa odgovarajućih
terminalnih simbola, primjerice, vrijednost koju vraća funkcija Izraz() u pseudo kôdu
semantičkih akcija je označena s Izraz:t.

2. Unutar definicije funkcija parsera na prikladan način se umeću semantičke akcije koje su
definirane na slici 19.5. Primjerice, funkcija Izraz() ima oblik:

                 TREE Izraz()
                 {
                     TREE t1,t2, t = NULL;

                      if(next_token == OPERATOR)
                      {
                          int op = tokenval.op;
                          next_token = getToken();
                           t1 = Operand();
                           t2 = Operand();
                           t = make_opnode(op, t1, t2);
                      }
                      else
                          syn_error(" nedozvoljen operator!");
                      return t;
                 }

     Podebljano je prikazan kôd semantičkih akcija. Unutar tijela funkcije definirane su tri
varijable tipa TREE. Varijabla t je semantički atribut simbola Izraz, i njenu vrijednost vraća
ova funkcija. Varijable t1 i t2 su semantički atributi simbola Operand, a dobiju se pozivom
funkcije Operand(). Formiranje čvora stabla se vrši tek kada su "prepoznati" operator i oba
operanda. Vrijednost semantičkog atributa tokena OPERATOR se bilježi u varijabli op.
     Sličan je postupak definiranja funkcija Naredba() i Operand(). Kompletni izvorni kôd
je u programu prefix_int.c.




                                                                                                301
/* Datoteka: prefix_int.c
#include <stdio.h>
#include <stdlib.h>

#include "prefix_lex.h"
#include "prefix_tree.h"

void syn_error(char *str)   /* dojava greške */
{
  printf("nGreska: %s",str);
  exit(1);
}

int next_token;

TREE Izraz();
TREE Operand();

TREE Naredba ()         /* Naredba : ( Izraz ) | QUIT */
{
    TREE t;
    next_token = getToken();

    if(next_token == '(') {
        next_token = getToken();
        t = Izraz();
        if(next_token != ')') syn_error(" nedostaje ')' !");
    }
    else if(next_token == QUIT)
        return NULL;
    else
        syn_error(" nedozvoljen simbol !");

    return t;
}

TREE Izraz()           /* Izraz : OPERATOR Operand Operand    */
{
   TREE t1,t2, t = NULL;

    if(next_token == OPERATOR)
    {
     int op = tokenval.op;
        next_token = getToken();
        t1 = Operand();
        t2 = Operand();
        t = make_opnode(op, t1, t2);
    }
    else
        syn_error(" nedozvoljen operator!");
    return t;
}

TREE Operand ()      /* Operand :   NUM | - NUM | (Izraz)*/
{
    TREE t = NULL;

    if( next_token == '(') {
        next_token = getToken();




                                                                   302
t = Izraz();
               if(next_token != ')') syn_error(" nedostaje ')' !");
               else next_token = getToken();
          }
          else if( next_token == NUM) {
              t = make_numnode(tokenval.num);
              next_token = getToken();
          }
          else if( next_token == OPERATOR && tokenval.op == '-') {
              next_token = getToken();
              if( next_token == NUM) {
                  t = make_numnode(-tokenval.num);
                  next_token = getToken();
              }
              else
                  syn_error("krivo unesen broj !");
          }
          else
              syn_error("krivo unesen broj !");
          return t;
     }


     int main(int argc, char **argv)
     {
         double rez;
         TREE t;

          if(argc == 2) { /* unos iz datoteke */
              input = fopen(argv[1],"r");
              if (input == NULL) {
                 printf("Ne postoji datoteka imena: %s", argv[1]);
                     exit(1);
              }
          }
          else /* unos tipkovnicom - input == stdin*/
             printf(
             "Unesite prefiksni aritmeticki izraz ili $ za krajn>");

          while (1)
          {
              t = Naredba();
              if( t == NULL) break;
              rez = evaluate(t);
              if(input == stdin)             /* unos tipkovnicom */
                 printf("nRezultat: %fn>", evaluate(t));
              else {                         /* unos iz datoteke */
                  print_prefiks (t);
                  printf("n=%fn", evaluate(t));
              }
              t = tree_delete(t);
          }
          if(input != stdin)
              fclose(input);
          return 0;
     }


Program se kompilira komandom:




                                                                        303
c:> cl prefix_int.c prefix_tree.c prefix_lex.c

Program "prefix_int.exe" se može koristiti na dva načina. Prvi je način da se program pozove
bez argumenata komandne linije. Tada program čeka da korisnik unese aritmetički izraz ili znak
za kraj unosa ($) s tipkovnice. Drugi je način da se program pozove s argumentom koji je ime
datoteke u kojoj je zapisano više prefiksnih izraza, primjerice, komandom

      c:> prefix_int pr.txt

analizira se tekstualna datoteka "pr.txt". Ako ona ima sljedeći sadržaj:

      (* 8 9)
      (* 8 (+ 8 9))
      (+ 8 (+ 8 9))

program ispisuje rezultat u obliku:

      ( * 8.000000 9.000000 )
      =72.000000
      ( * 8.000000 ( + 8.000000 9.000000                   ) )
      =136.000000
      ( + 8.000000 ( + 8.000000 9.000000                   ) )
      =25.000000

    Primijetite da u datoteci "pr.txt" nije korišten znak $, koji pri unosu s tipkovnice predstavlja
naredbu za kraj unosa, jer program prihvaća i znak za kraj datoteke (EOF) kao naredbu za kraja
unosa.

Zadatak: Napišite interpreter infiksnih izraza, za kojeg vrijedi sljedeća gramatika:

       Naredba : Izraz ENTER ;
       Izraz : Clan '+' Izraz | Clan '-' Izraz | Clan
       Clan : Faktor '*' Clan | Faktor '/' Clan | Faktor
       Faktor : NUM | - NUM | ( Izraz )

Tokeni su: ENTER - znak nove linije
      NUM - realni broj
      +,-,*,/ - operatori

Interpreter očekuje da korisnik otkuca aritmetički izraz u infiksnom obliku, primjerice
67.8*(7-9) i da pritisne <enter>, tj znak nove linije. Nakon toga interpreter ispisuje rezultat.

    U analizi ovog zadatka prvo treba primijetiti da prethodna gramatika nije LL(1) jer u
drugom i trećem pravilu postoje zajednički lijevi prefiksi. Zbog toga, treba koristiti
modificiranu gramatiku:

      Naredba : Izraz ENTER
      Izraz : Clan IzrazOptopt
      IzrazOpt : '+' Izraz
               | '-' Izraz
      Clan : Faktor ClanOptopt
      ClanOpt: '*' Clan




                                                                                                304
| '/' Clan
       Faktor : NUM | - NUM | ( Izraz )

Ovo je LL(1) gramatika i za nju se može napisati rekurzivni silazni parser. Primjerice, funkcija
parsera za pravilo Clan ima oblik:

      TREE Clan ()
      {
          TREE t2, t1;
          t1 = Factor();
          if(next_token == '*' || next_token = '/')                    {
              int tok = next_token;
              t2 = ClanOpt();
              return make_tnode(tok, t1, t2);
          }
          else return t1;
      }

      TREE ClanOpt()
      {
          if(is_next_token('*')) {
              match('*');
              return Clan ();
           }
          else if(is_next_token('/')) {
              match('/');
              return Clan ();
          }
          else syn_error("greška")
      }

Napišite ostale funkcije parsera i izvršite potrebne izmjene u leksičkom analizatoru (funkcija
getToken() treba vraćati poseban token za sve operatore i za znak nove linije).


19.4 Stabla s proizvoljnim brojem grana
Ako čvorovi stabla imaju više djece, kao u M-stupanjskom stablu tada se oni mogu opisati
samoreferentnom strukturom Node koja sadrži niz od M pokazivača na djecu čvora.

      #define NUM_CHILD M
      typedef int elemT:

      typedef struct _node {
            void * data;
                struct _node * child[NUM_CHILD];
      } Node;


Čvor se formira pomoću funkcije makeNode():

      Node * makeNode( elemT elem, Node *N0, Node *N1,..., Node *Nm)
      {
         Node *n=(Node *) malloc(sizeof(Node));
         if(n) {
                n->elem = elem;
                n->child[0] = N0;




                                                                                            305
n->child[1] = N1;
               .....
                  n->child[m] = Nm;
          }
          return n;
      }

Ako podstabla imaju različite stupnjeva, zovemo ih višesmjerna stabla (eng. multiway tree). U
tom slučaju može se koristiti prethodna struktura tako da se svi child[i] pokazivači postave
jednakim nuli za i-smjerni čvor. Primjerice, dvosmjerni čvor bi formirali naredbom:

      makeNode( elem, N0, N1, NULL,..,NULL);

Loša strana ovakvog pristupa je da se troši memorija za pokazivače nepostojećih čvorova. Taj
se problem može riješiti na dva načina. Prvi je da se i niz child alocira dinamički te da se u
strukturi Node bilježi broj alociranih čorova. Tada struktura Node može biti ovakva:

      typedef struct _node {
             elemT elem;
         int num_child;                       /*broj alociranih čvorova*/
             struct _node **child;            /*pokazivač na niz pokazivača*/
      } Node;

    Drugi način za stvaranje čvorova višesmjernog stabla je da se koriste dva pokazivača. Prvi,
nazvat ćemo ga first_children, pokazuje prvo dijete čvora, a drugi, nazvat ćemo ga
next_sibling pokazuje na listu braće prvog djeteta.

      typedef struct _node {
          elemT elem;
          struct _node *first_child;
          struct _node *next_sibling;
      } Node;

Dakle, next_sibling je pokazivač glave jednostruko vezane liste, koja sadrži djecu čvora
osim prvog djeteta. Kraj liste se uvijek označava NULL pokazivačem. Za stvaranje čvorova i
uspostavljanje veza među njima mogu poslužiti sljedeće funkcije:

      Node *makeLeaf(elemT elem)
      { /*stvaranje lista*/
         Node *n= (Node*)malloc(sizeof(Node));
         if(n) {
             n->first_child = n->next_sibling = 0;
             n->elem = elem;
         }
         return n;
      }

      Node * make1ChildNode(elemT elem, Node *N1)
      { /*čvor s jednim djetetom*/
          Node *n=makeLeaf(elem);
          if(n) n->first_child = N1;
          return n;
      }

      Node * make2ChildNode(elemT elem, Node *N1, Node *N2)
      { /*čvor s dva djeteta*/
          Node *n=makeLeaf(elem);
          if(n) {




                                                                                           306
n->first_child = N1;
                   N1->next_sibling = N2;
                   N2->next_sibling = 0;
           }
           return n;
      }

      Node * make3ChildNode(elemT elem, Node *N1, Node *N2, Node *N3)
      { /*čvor s tri djeteta*/
          Node *n=makeLeaf(elem);
          if(n) {
               n->first_child = N1;
               N1->next_sibling = N2;
               N2->next_sibling = N3;
               N3->next_sibling = 0;
          }
          return n;
      }
      itd.



Veze čvora zapisane u          Veze čvora zapisane u           Ekvivalentno binarno
Node *child[3];                Node *first-child               stablo
                               Node *next-sibling

         1                             1                                      1
        /|                           /                                     /
       / |                          /                                    2
      / |                          /                                    / 
     2   3   4                     2---3---4                            5    3
    /       |                    /       /                                   
   5   6     7                   5---6   7                                6     4
            /                          /                                      /
           8   9                       8---9                                 7
                                                                            /
                                                                           8
                                                                            
          a)                            b)                        c)         9

Slika 19.9 Tri načina zapisa stabla kojem čvorovi imaju promjenjljivi broj djece. a)
pokazivači čvorova su u nizu child[], b) pokazivači čvorova su first_child i elementi liste
next_sibling, c) ekvivalentno binarno stablo koje se dobije iz drugog oblika rotiranjem
liste čvorova braće za 45o u smjeru okretanja kazaljke sata.

    Važno je primijetiti da zapis stabla pomoću first_child i next_sibling čvorova ima
ekvivalentan zapis pomoću binarnog stabla. To je pokazano na slici 19.9. Stablo je oformljeno
naredbama:

      Node   *N1   =   make2ChildNode   (7,   makeLeaf(8), makeLeaf(9));
      Node   *N2   =   make1ChildNode   (4,   N1);
      Node   *N3   =   make2ChildNode   (2,   makeLeaf(5), makeLeaf(6));
      Node   *N0   =   make3ChildNode   (1,   N3, makeLeaf(3), N2);

Ekvivalentno binarno stablo se dobije iz višesmjernog stabla (first_child-next_sibling tipa)
"rotiranjem" veza next_sibling liste za 45o u smjeru okretanja kazaljke sata.




                                                                                         307
Definicija: Višesmjerno stablo T se može predstaviti odgovarajućim binarnim stablom
B. Ako su {n1,..., nk} čvorovi T, a {n'1,..., n'k} čvorovi B, tada čvor nk odgovara čvoru
n'k. Oni imaju isti sadržaj. Ako je nk korijen od T, tada je n'k korijen od B. Veze među
čvorovima su:
     • Ako je nl prvo dijete od nk, tada je n'l lijevo dijete od n'k. (Ako nk nema djece,
        tada n'k nema lijevo dijete.)
     • Ako je ns sljedeći neposredno desni brat od nk, tada je n's desno dijete od n'k.
Bez obzira na ovu ekvivalentnost, sva tri opisana oblika implementacije višesmjernog stabla
imaju različiti apstraktni značaj pa se i koriste u različitim primjenama. Zapis s nizom
pokazivača djece često se koristi kod izrade sintaktičkih stabala, a zapis s first_child i
next_sibling pokazivačima je uobičajen kod interpretetera jezika LISP i SCHEME.

Algoritmi za obilazak višesmjernog stabla slični su algoritmima binarnog stabla, i lako se
definiraju rekurzivno:

Algoritam: visina stabla kojem je korijen n
tree_height(n) =         Ako je n != 0 tada vrati 0, inače
                         vrati 1 + MAX(tree_height (child1), … tree_height (childn) )

Algoritam: veličina stabla kojem je korijen n
tree_size(n) =           Ako je čvor n != 0 tada vrati 0, inače
                         vrati 1 + tree_size(child1) + … + tree_size(childn)

Algoritam: obilazak stabla - postorder
postorder (n) =         Ako je čvor n != 0 tada za svako dijete od n pozovi postorder(dijete).
                        Posjeti čvor.

Algoritam: obilazak stabla - preorder
preorder (n) =          Ako je čvor n != 0 tada
                        Posjeti čvor n, zatim za svako dijete od n pozovi postorder(dijete).

Primjer: funkcija postorder() vrši postorder obilazak stabla i na čvorove primjenjuje
funkciju (*visit)().

      void postorder(Node * n, void (*visit)(Node *))
      {
         Node * c;
         if (n == NULL) return;
         c = n->first_child;
         while (c != NULL) {
            postorder(c, visit);
             c = c->next_sibling;
         }
         visit(n);
      }

Stablo, kojem je pokazivač korijena t, možemo izbrisati naredbom postorder(t, free);.

Primjer: funkcija tree_size() vrši postorder obilazak stabla i vraća broj čvorova stabla.

      int tree_size(Node * n)
      { /* postorder obilazak */




                                                                                            308
Node * c; int m;
          if (n == NULL) return 0;
          m = 1;
          c = n->first_child;
          while (c != NULL) {
             m += tree_size(c);
             c = c->next_sibling;
          }
          return m;
      }

Primjer: funkcija print_prefiks()vrši preorder obilazak stabla i ispisuje prefiksnu notaciju
stabla.

      void print_prefiks (Node * n)
      {   Node *x;
          if(n == NULL) { printf(" () "); return;}
          if(isLeaf(n)) { printf("%d ",n->data); return; }
          printf("( %d ",n->data);
              x = n->first_child;
          while(x != 0) {
             print_prefiks (x);
                 x = x->next_sibling;
              }
          printf(" )");
      }

Ako se ova funkcija primijeni na              stablo   koje   je   prikazano     na   slici   19.9
(print_prefiks(N0);), dobije se ispis:

      ( 1 ( 2 5 6       ) 3 ( 4 ( 7 8 9        ) ) )

Pitanje: Zašto kod višesmjernih stabala nema smisla "inorder" obilazak stabla?

Obilazak stabla može biti i proizvoljan. Tada obično korisnik ispituje sadržaj čvora i na temelju
toga određuje u kojem smjeru će dalje obilaziti stablo.


19.5 Prioritetni redovi i hrpe
    U prethodnim odjeljcima stabla su programski implementirana pomoću pokazivača i
samoreferentnih struktura. Sada će biti pokazan primjer u kojem se stabla mogu efikasno
implementirati pomoću nizova. Analizirat će se posebni tipovi stabala, odnosno nizova, koji se
nazivaju hrpa i pomoću kojih će biti realizirani tzv. prioritetni redovi (eng. priority queue).
     Najjednostavnije kazano, prioritetni red je skup podataka u kojem se svakom elementu
skupa pridružuje oznaka prioriteta . Prioritetni redovi se često koriste. Evo dva primjera:
      Primjer 1: Pacijenti ulaze u čekaonicu, te iz nje odlaze liječniku na pregled. Prvi na redu
      za liječnika nije onaj koji je prvi ušao, već onaj čije je stanje najteže.
      Primjer 2: U računalu više procesa čeka u redu za izvršenje. Redoslijed kojim se
      izvršavaju procesi određen je prioritetom procesa. Procesor uzima iz reda program s
      najvećim prioritetom, te ga izvodi.
    U odnosu na obične redove, koji sadrže neki element x, prioritetni redovi sadrže elemente
koji imaju dva atributa: ključ(x) i vrijednost(x). Ključ simbola određuje njegov prioritet.
Najčešće se uzima da najveći prioritet ima simbol s najmanjim ključem, ali ponekad se uzima i
obrnuto, da najveći prioritet ima simbol s najvećim ključem.




                                                                                              309
Kod prioritetnog reda važne su samo dvije operacije : ubaci element i izbaci element s
najmanjim ključom (ili alternativno, s najvećim ključem). Ovdje ćemo pokazati izvedbu
apstraktnog dinamičkog tipa PQUEUE koji je definiran na sljedeći način:

      ADT PQUEQUE
      insert(Q, x)                         - ubacuje element x u red Q.

      delete_min(Q)                        - vraća najmanji element reda i izbacuje ga iz reda Q.
                                           Operacija nije definirana ako Q ne sadrži ni jedan element, tj
                                           ako je size(Q) = 0.

      size(Q)                              - vraća broj elemenata u prioritetnom redu Q.



    Ovaj ADT se može jednostavno realizirati pomoću linearne sortirane liste. U tom slučaju
operacija delete_min(), u kojoj se pronalazi i izbacuje prvi element u listi, ima vremensku
složenost O(1). Operacija insert() mora ubaciti novi element na “sortirano mjesto”. To znači da
u prosjeku treba obići bar pola sortirane liste, pa je vremenska složenost operacije insert() O(n).
    Efikasnija implementacija se može napraviti pomoću hrpe (eng. heap). Hrpa je naziv za
potpuno binarno stablo koje je zapisano pomoću niza elemenata, prema sljedećim pravilima:
1. Pravilo rasporeda: Potpuno stablo s N čvorova, koji sadrže elemente tipa <ključ,vrijednost>,
može se bilježiti u nizu elemenata, na indeksima i = 1,2,..N-1, prema sljdedećem pravilu:
    Ako element indeksa i predstavlja čvor stabla, tada
            - element indeksa 2*i+1 predstavlja lijevo dijete čvora,
            - element indeksa 2*i+2 predstavlja desno dijete čvora,
            - element indeksa (i-1)/2 predstavlja roditelja čvora,

2. Pravilo hrpe: Ključ roditalja nekog čvora uvijek je manji od ključa tog čvora, pa je najmanji
ključ u korijenu stabla.

                                      13



                       21                       16



             24                  31        19        69



        65        26        32                                13   21   16   24   31   19   69   65   26   32




                             Slika 19.10 Potpuno binarno stablo i ekvivalentni niz hrpe.

Na slici 19.10 prikazano je potpuno binarno stablo i ekvivalentni niz hrpe. Uočimo:
    - indeks namanjeg elementa (koji je u korijenu stabla) jednak je i = 0.
    - indeks kranjeg elementa je N-1. To je krajnji desni list stabla.

Kapacitet niza treba biti veći od broja elemenata niza. Zbog toga je povoljno da se niz realizira
kao dinamički niz koji se može povećavati (realocirati) ako, pri operaciji umetanja, broj
elemenata niza dosegne kapacitet niza.




                                                                                                                310
Postupno će biti opisana implementacija ADT PQUEUE na temelju specifikacije koja je
opisana u datoteci "pq.h".

     /* Datoteka: pqueue.h */
     /* Specifikacija ADT PQUEUE */

     typedef struct pq_elem {         /* element prioritetnog reda                 */
         int key;                     /* ključ za postavljanje prioriteta          */
         void *val;                   /* vrijednost elementa (bilo što)            */
     } elemT;

     PQUEUE pqueue_new(int capacity)
     /* Konstruktor ADT-a.
      * Argument: capacity - određuje početni kapacitet hrepe
      * Vraća: pokazivač ADT-a
      */

     void pqueue_free(PQUEUE pQ)
      /*Destruktor objekta pQ*/

     unsigned pqueue_size(PQUEUE pQ);
     /*Vraća broj elemenata u redu pQ*/

     void pqueue_insert(PQUEUE pQ, elemT x);
     /* Umeće element na kojeg pokazuje px u red
      * Argumenti: pQ - red
      *            x - element kojeg se umeće
      */

     elemT pqueue_delete_min(PQUEUE pQ);
     /* Briše najmanji element iz reda PQ
      * Vraća: element koji je izbrisan
      */

Implementacija ADT PQUEUE je opisana u datoteci "pq.c".

     /* Datoteka: pq.c */
     #include <stdlib.h>
     #include <stdio.h>
     #include <memory.h>
     #include <assert.h>
     #include "pq.h"

     typedef struct pq {
         int capacity;               /* kapacitet PQUEUE           */
         int N;                      /* broj umetnutih elemenata   */
         elemT *elements;         /* niz elemenata reda         */
     } PQueue, *PQUEUE;

     PQUEUE pqueue_new(int capacity)
     {
         PQUEUE pq = (PQUEUE)malloc(sizeof(PQueue));
         if(pq) {
             pq->N=0;
             pq->capacity=capacity;
             pq->elements = malloc(sizeof(elemT)*capacity);
             if(!pq->elements) exit(1);
         }
         return pq;




                                                                                   311
}

      void pqueue_free(PQUEUE pQ)
      { /*Destruktor ADT-a*/
          assert(pQ);
          free(pQ->elements);
          free(pQ);
      }

19.5.1 Umetanje elementa u prioritetni red
Ako želimo dodati element u prioritetni red možemo ga dodati na kraj niza. To je ekvivalentno
dodavanju krajnjeg desnog lista u stablu (prikazan crtkano na slici 19. 10). Ta operacija ima za
posljedicu da se N uveća za jedan, ali i da možda stablo ne zadovoljava pravilo hrpe. Da bi se
zadovoljilo pravilo hrpe koristi se postupak, ilustriran na slici 19.11, opisan u sljedećem
algoritmu:

Algoritam: Umetanje elementa x u hrpu
   1. Element iza krajnjeg elementa niza se tretira kao prazan (to je sljedeći krajnji desni list
       stabla). Podrazumijeva se da je kapacitet niza veći od broja elemenata u nizu.
   2. Ako je ključ roditelja praznog čvora manji od ključa elementa x, tada se element x
       upisuje u prazni čvor. Time postupak završen.
   3. Ako je ključ roditelja praznog čvora veći od ključa elementa x, tada se element roditelja
       kopira u prazni čvor, a čvor u kojem je bio roditelj se uzima kao prazni čvor. Postupak
       se ponavlja korakom 2.

                                        13
                17



                         21                       16
                                                                 17

               24                  31        19        69



          65        26        32                            13    21   16   24   31   19   69   65   26   32




    Slika 19.11 Stablo sa slike 19.10 prilikom dodavanja ključa 17. Strelice pokazuju koje
              elemente treba pomaknuti da bi se otvorilo mjesto za novi element.

Prethodni algoritam je primijenjen u funkciji pqueue_insert(). U toj funkciji se najprije
provjerava da li je popunjen kapacitet reda. Ako je popunjen, dvostruko se povećava kapacitet
reda. Nakon toga se vrši umetanje elementa prema prethodnome algoritmu.

      void pqueue_insert(PQUEUE pQ, elemT x)
      {
          int i;
          assert(pQ);
          /* provjeri kapacitet reda i povećaj ga ako je N>=kapacitet*/
          if ( pQ->N >= pQ->capacity-1 ) {
              pQ->capacity *= 2; /*udvostruèi kapacitet hrpe*/
              pQ->elements = (elemT *) realloc(pQ->elements,
                              sizeof(elemT)*pQ->capacity);
              if(!pQ->elements) exit(1);
          }




                                                                                                               312
/* umetni element x i povećaj N */
           i = pQ->N++;
           while (i) {
               int parent = (i-1)/2;
               if (x.key > pQ->elements[parent].key) break;
                   pQ->elements[i].key = pQ->elements[parent].key;
                  i = parent;
           }
               pQ->elements[i] = x;
      }

19.5.2 Brisanje elementa iz prioritetnog reda
Brisanje elementa iz prioritetnog reda se vrši samo na jedan način: briše se element s
minimalnim ključem pomoću funkcije delete_min(). Minimalni element se nalazi u korijenu
stabla, odnosno u hrpi na indeksu 0. Ako ga izbrišemo tada je korijen prazan i u njega treba
zapisati neki drugi element. Najzgodnije je u njega zapisati krajnji element niza i smanjiti
veličinu hrpe. Problem je što tada možda nije zadovoljeno pravilo hrpe. Dovođenje niza u
stanje koje zadovoljava pravilo hrpe vrši se funkcijom heapify(), prema sljedećem algoritmu:

Algoritam: uređenje hrpe kada vrh hrpe nije u skladu s pravilom hrpe
   1. Započni s indeksom koji predstavlja vrh hrpe. Spremi taj element u varijablu x. Nadalje
       se uzima da je vrh hrpe "prazan".
   2. Analiziraj djecu praznog čvora i odredi koje dijete je manje.
   3. Ako je ključ od x manji od ključa manjeg djeteta spremi x u prazan čvor i završi,
       inače upiši element manjeg djeteta u prazan čvor i postavi da čvor manjeg djeteta bude
       prazan čvor. Ponovi korak 2.

      void heapify(elemT *elements,int vrh, int N)
      {
          int min_child, i = vrh;
          /*zapamti element kojem tražimo mjesto u hrpi */
          elemT x = elements[i];
          /* (i) označava "prazni" element*/
          while (i < N/2) {
             int left = min_child = 2*i+1;
               int right = left + 1;
              /* Prvo odredi indeks manjeg djeteta - min_child */
              if ( (left < N-1)
                   && (elements[left].key > elements[right].key) )
                  min_child = right;
              /* Ako je min_child manji od x, upiši ga na prazno
                 mjesto (i), inače break */
             if ( x.key < elements[min_child].key) break;
             elements[i] = elements[min_child];
               i = min_child;
          }
          /* konačno stavi x na prazno mjesto*/
          elements[i] = x;
      }

     Sada je implementacija funkcije pqueue_delete_min() jednostavna. u njoj je najprije
zapamćen minimalni element, koji je na indeksu 0, u varijabli minimum. Zatim je krajnji
element upisan na početak niza, a veličina hrpe je smanjena. Slijedi poziv funkcije heapify(),
kojom se uređuje hrpa, ako je poremećeno pravilo hrpe. Postupak brisanja elementa je ilustriran
na slici 19.12.




                                                                                           313
elemT pqueue_delete_min(PQUEUE pQ)
            {
                elemT minimum;
                assert(pQ);
                assert( pQ->N > 0 );
                /* minimum je u korijenu (indeks 0) */
                minimum = pQ->elements[0];
                /* zadnji element prebaci u korijen i smanji veličinu hrpe*/
                pQ->elements[0] = pQ->elements[--pQ->N];
                /* preuredi hrpu od vrha prema dnu (vrh je na indeksu 0) */
                heapify(pQ->elements, 0, pQ->N);
                return minimum;
            }


               13
           minumum                                                                        16                                               16


                                    X > 16
                17                                16                       17                  X > 19                       17              x        19



      24                  21                 19        69        24                  21          19     69        24                  21        31         69



 65        26        32        31     X = 31                65        26        32                           65        26        32




Slika 19.12 Primjer kako se izvršava operacija delete_min(). Iz reda se odstranjuje ključ 13 koji
je na vrhu hrpe. Strelice pokazuju koje elemente treba pomaknuti da bi se otvorilo mjesto za
element s kraja hrpe ( x=31).

     Testiranje se provodi programom "pq-test.c". U programu se najprije 10 slučajno
generiranih brojeva upisuje u prioritetni red pQ, a zatim se briše element po element i ispisuje
vrijednost ključa.

            /* Datoteka: pq-test.c*/
            /* 1. Generira se 10 slučajnih brojeva i sprema u pqueue
             * 2. Briše iz pqueue i ispisuje element po element
             */
            #include <stdlib.h>
            #include <stdio.h>
            #include <memory.h>
            #include <assert.h>
            #include "pq.h"

            int main()
            {
                int i;
                elemT elem;

                          PQUEUE pQ =pqueue_new(20);
                          printf("Slucajno generirani brojevi:n");
                          for(i=0; i<10; i++) {
                              elem.key = rand() % 100 +1;
                              pqueue_insert(pQ, elem);
                              printf("%d ", elem.key);
                          }




                                                                                                                                                          314
printf("nIzlaz iz prioritetnog reda:n");
                while(pqueue_size(pQ)) {
                elem = pqueue_delete_min(pQ);
                printf("%d ", elem.key);
            }
            pqueue_free(pQ);
            printf("n");
            return 0;
      }

Kada se program izvrši dobije se ispis:

      Slucajno generirani brojevi:
      42 68 35 1 70 25 79 59 63 65

      Izlaz iz prioritetnog reda:
      1 25 35 42 59 63 65 68 70 79

Vidimo da je izlaz sortiran, jer smo iz reda brisali jedan po jedan minimalni element.

Vremenska složenost operacija insert() i delete_min() je malena jer se obje operacije provode
samo po jednoj stazi od vrha do dna hrpe ili obrnuto. Kod potpunog stabla duljina staze iznosi
log2(n+1)-1. Pošto nas zanima asimptotska vrijednost (za veliki n), vremenska složenost
iznosi O(log2n).

19.5.3 Sortiranje pomoću prioritetnog reda i heapsort metoda
Prethodni prinjer testiranja ADT PQUEUE pokazuje kako se može izvršiti sortiranje pomoću
prioritetnog reda

Algoritam: sortiranje niza A pomoću prioritetnog reda
    1. formiraj objekt tipa PQUEUE pQ.
    2. za sve elemente niza A[i], i =0,1,..N-1
            pqueue_insert(pQ, A[i])
    3. za sve i =0,1,..N-1
            A[i] = pqueue_deletemin(pQ);
    4. dealociraj objekt pQ

Ovaj algoritam ima vremensku složenost O(n logn), jer se u njemu n puta ponavljaju operacije
umetanja i brisanja, a one imaju složenost O(logn). Loša strana algoritma je da zahtijeva
dodatnu memoriju veličine originalnog niza.
     Funkcija heapify() se može efikasno upotrijebiti za sortiranje nizova bez upotrebe
dodatne memorije. Algoritam je jednostavan i primijenjen je u funkciji heapsort(). Sastoji se
od dva koraka. U prvom koraku se od proizvoljnog niza stvara hrpa. Funkcija heapify() se
primjenjuje n/2 puta, na svim unutarnjim čvorovima, najprije na najmanjem podstablu, koji je
trivijalno potpuno stablo, a zatim iterativno na svim većim podstablima do samog korijena. U
drugom koraku se element nultog indeksa (koji je u ovom slučaju minimalan) iterativno
zamjenjuje s krajnjim elementom, a zatim se funkcija heapify() primjenjuje na niz s
umanjenim brojem elemenata.

      void heapsort(elemT *A, int N )
      {
         /* sortira niz A duljine N -> A[i] > A[i+1]*/
             int i;
           /*1. stvari hrpu od postojeceg niza */




                                                                                          315
for(i = N/2-1; i >= 0; i--)
                  heapify( A, i, N );

            /*2. zamijeni minimum s posljednjim elementom i uredi hrpu */
              for(i = N-1; i>0; i-- ) {
                 elemT tmp = A[i]; A[i]=A[0]; A[0]= tmp;
                 heapify( A, 0, i);
              }
      }

     Ova verzija heapsort() funkcije sortira nizove od veće prema manjoj vrijednosti. Ako bi
trebali suprotan raspored elemenata tada treba izmijeniti funkciju heapify(), tako da ona
stvara hrpu s najvećim elementom na vrhu hrpe. Izmjena je jednostavna - treba u naredbama
usporedbe zamijeniti operatore < i >.
     Vremenska složenost heapsort metode je O(nlog2n) i to u najgorem slučaju, jer se u njoj
funkcija heapify() poziva maksimalno 3n/2 puta. Brzina izvršenja u prosječnom slučaju je
nešto sporija nego kod primjene funkcije quicksort(), ali postoje slučajevi kada
heapsort() daje najveću brzinu izvršenja.

Zadatak: Napišite verziju ADT-a za prioritetni red PQ kojem je na vrhu hrpe element s
maksimalnim ključem.
     ADT PQ
     insert(Q, x)           - ubacuje element x u red Q.

     delete_max(Q,)         - vraća najveći element reda i izbacuje ga iz reda Q
                            Operacija nije definirana ako Q ne sadrži ni jedan element, tj
                            ako je size(Q) = 0.

     size(Q)                - vraća broj elemenata u prioritetnom redu Q.


19.6 Zaključak
     Stabla omogućuju jednostavnu apstrakciju različitih podatkovnih struktura, od slike
porodičnog stabla do jezičkih sintaktičkih i semantičkih veza. U sljedećem poglavlju bit će
pokazano da stabla omogućuju stvaranje općih podatkovnih struktura tipa tablice, rječnika i
skupova.
     Stabla se najčešće koriste u obliku binarnog stabla. Pokazano je da se višesmjerna stabla
mogu prikazati ekvivalentnim binarnim stablima.
     Pokazana su dva načina programske implementacije stabla. U prvoj se stablo realizira
pomoću samoreferentnih struktura koje sadrže pokazivače, a u drugoj se stablo realizira pomoću
prostih nizova.
     Korištenje stabala je uvijek povezano uz neku primjenu za koju se definira potreban skup
operacija. Pokazana je potpuna realizacija interpretera prefiksnih aritmetičkih izraza, a
pokazano je i kako se može napraviti interpreter prefiksnih izraza. Izneseni su temelji
sintaktičke analize jednostavnih jezičkih konstrukcija koje zadovoljavaju LL(1) tip gramatike.
Pokazano je kako se realizira jednostavni leksički analizator i kako se iz BNF notacije
gramatike realizira rekurzivno silazni parser.
     Opisana je izvedba apstraktnog tipa podataka PQUEUE kojom se stvaraju prioritetni
redovi. Korištena je struktura tipa hrpe i pomoću nje je realizirana metoda sortiranja koja se
naziva heapsort.




                                                                                             316
20 Strukture za brzo traženje podataka


Naglasci:
   • Tablice simbola i rječnici
   • Hash tablica
   • Otvorena hash tablica
   • Zatvorena hash tablica
   • BST - binarno stablo traženja
   • RBT - crveno-crno stablo


20.1 Tablice simbola i rječnici
Tablice simbola i rječnici imaju sličnu karakteristiku, a to je da svaki simbol ima neku ključnu
oznaku (ključ) i vrijednost (ili definiciju) koja opisuje značaj simbola. Može se i reći da je
tablica simbola skup parova <ključ, vrijednost>. Ključ je najčešće tipa string, ali može biti i
numerička vrijednost.

Tri su temeljne operacije koje definiraju ADT tablice (ili rječnika):

   ADT TABLE
   insert (T, k, v)    - umeće simbol, ključa k i vrijednosti v u tablicu T, ako već nije u T.
   find (T, k)         - vraća vrijednost simbola ako u T postoji ključ k,
   delete(T, k)        - iz tablice T briše simbol kojem je ključ k, ako postoji .

Brzina ovih operacija ovisna je o tome koja struktura podataka je upotrijebljena za tablicu
simbola. Za implementaciju tablice moguće je koristiti nizove i liste. To ćemo pokazati
primjerom.

Primjer: Implementacija tablice pomoću niza.

Simbol opisuje struktura:

      typedef struct symbol {
         char *key;           /* ključ simbola */
         void *val;           /* pokazivač vrijednost simbola                         */
      }Symbol;

Tablica se sastoji od niza simbola :

      typedef struct _table {
         Symbol *array;      /* alocirani niz simbola     */
         int N;              /* broj simbola u nizu       */
         int M;              /* kapacitet alociranog niza */
      } *TABLE

Primjer funkcije za traženje:




                                                                                                 317
Symbol *find(TABLE T, char *key)
         { /* vraća pokazivač simbola ili NULL */
                int i;
            for (i = 0; i < T->N; i++)
               if(!strcmp(T->array[i].key, key);
                       return &T->array[i];
             return NULL;
         }

         /*sami napišite funkcije insert() i delete()*/

Zbog jednostavnosti uzeto je da je ključ tipa string i da je vrijednost simbola označana
pokazivačem tipa void * (dakle, pokazivačem na bilo koji tip podatka).

Primjer: Implementacija tablice pomoću vezane liste se može temeljiti na sljedećim
strukturama:

         typedef struct symbol {
               struct symbol *next                  /* veza u listi */
               char * key;                          /* ključ simbola */
               void * val;                          /* pokazivač vrijednost simbola     */
         }Symbol;

         typedef struct _table {
            Symbol *list;       /* tablica je lista*/
         } *TABLE

         Symbol *find(TABLE T, char * key)
         {
                Symbol *L = T->list;
            while( L != NULL) {
                if (!strcmp(L->key, key))
                    return L;
                L = L->next;
            }
            return NULL;
         }

U oba slučaja kompleksnost algoritma traženja je O(n) pa ovakova rješenja mogu zadovoljiti
samo za implementaciju manjih tablica. Kada je potrebno imati veće tablice cilj je smanjiti
kompleksnost na O(log n) ili čak na O(1). Ta poboljšanja se mogu ostvariti pomoću tzv. "hash"
tablica i specijano izvedenih stabala za traženje (BST, AVL, RED-BLACK, B-tree). Neke od
ovih struktura upoznat ćemo u ovom poglavlju.


20.2 Hash tablica
     Ideju hash2 tablice se može opisati na slijedeći način. Kada u telefonskom imeniku tražimo
neko ime, najprije moramo pronaći stranicu na kojoj je zapisano to ime, a zatim pregledom po
stranici dolazimo do traženog imena. Iskustvo pokazuje da više vremenu treba za traženje
stranice, nego za traženje imena na stranici. Zbog toga se kod telefonskih imenika grupe
stranica označavaju slovima abecede, kako bi se brže pronašla željena stranica. Na sličan način
podaci u hash tablici se grupiraju u više skupova (ili buketa). U kojem skupu se nalaze podaci,
određuje se iz ključa simbola pomoću posebne funkcije koja se naziva hash-funkcija.

2
    eng. hash – znači podjelu na manje dijelove, koji su otprilike jednake veličine




                                                                                             318
Definicija: Funkcija hash(k,m) raspodjeljuje argument k u jedan od m skupova, sa statistički
    uniformnom razdiobom. Funkcija vraća vrijednost iz intervala i ∈ [0..m-1]. Ta vrijednost
    označava da string pripada i-tom skupu.

     Postoji više izvedbi hash-funkcije koje zadovoljavaju navedenu definiciju. Za rad sa
stringovima ovdje će biti korišten sljedeći oblik hash-funkcije:

      unsigned hash(char *s, unsigned m)
      {
      /* Argument: s – string
       *           m - maksimalni broj grupa
       * Vraća: hash vrijednost iz intervala [0..m-1]
       */
          unsigned x=0;
          while(*s != '0') {
              s = *s + 31 * x;
              k++;
          }
          return x % m;
      }

    Može se primijetiti sličnost ove funkcije s funkcijom rand(), koja je opisana u poglavlju
11. Tada je slučajni broj xi , iz intervala [0..n-1], bio generiran iz prethodne vrijednost xi-1,
prema izrazu:
      xi = (a * xi-1 + b) % m

gdje su a i b konstante, koje su određene uvjetom da se broj generira sa statistički uniformnom
razdiobom. U slučaju prikazane hash-funkcije ova se zakonitost primjenjuje kumulativno za sve
elemente stringa (a=31, b=*s).

Kada je ključ cjelobrojnog tipa tada se može koristiti funkcija

      unsigned hash(int k, unsigned M) {
           return k > 0? k % M : - k % M;
      }

20.2.1 Otvorena hash tablica
     Sada će biti opisana izvedba tablice simbola, koja se naziva otvorena hash-tablica (eng.
Open Hashing ili Separate Chaining). Ilustrirana je na slici 20.1. Otvorena hash tablica se
realizira kao niz koji sadrži pokazivače na liste simbola koje imaju istu hash vrijednost. Te liste
se nazivaju buketi (eng. bucket).




                                                                                               319
Slika 20.1 Hash tablica s vezanom listom


    Operacije se provode prema sljedećem pravilu:
    insert(k,v)           Umetni simbol<k, v> na glavu liste bucket[hash(k, m)].
    find (k)              Traži simbol ključa k u listi bucket[hash(k, m)]. Vraća
simbol ili NULL
    delete(k)       Odstrani simbol<k, v> iz liste bucket[hash(k, m)].

Element niza buckets[i] je pokazivač i-te liste. Početno su svi pokazivači jednaki NULL.
Kada u tablici treba tražiti ili unijeti simbol kojem je ključno ime key, to se vrši u listi kojoj je
pokazivač jednak buckets[hash(key,m)].

Slijedi opis implementacije tablice, prema specifikaciji ADT TABLE iz datoteke "table.h".

Specifikacija za generičku hash tablicu
/*Datoteka: table.h */
/*Apstraktni tip TABLE */

typedef struct _table *TABLE;
typedef struct _symbol *SYMBOL;
                                                                     /* Primjer za string
*/
typedef    int   (*CompareFuncT)(void *, void *);                    /*     strcmp()     */
typedef    void *(*CopyFuncT)(void *);                               /*     strdup()     */
typedef    void (*FreeFuncT)(void *);                                /*     free()       */
typedef    unsigned (*HashFuncT)(void *, unsigned);                  /*     hash()       */

TABLE table_new(unsigned m, CompareFuncT f, HashFuncT h);
/* Konstruktor tablice
 * Argument: m - veličina tablice
 *            f - pokazivač funkcije za poredbu ključa (kao strcmp)
 *            h - pokazivač hash funkcije (kao hash_str)
 *                Ako se umjeto pokazivača f i h upiše 0
 *                podrazumjeva se rad s ključem tipa int
 * Vraća: pokazivač tablice ili NULL ako se ne može oformiti tablica
 */

void table_set_alloc(TABLE T, CopyFuncT copykey, FreeFuncT freekey,
                         CopyFuncT copyval, FreeFuncT freeval);
/* Postavlja pokazivače funkcija za alociranje ključa i vrijednosti
 * Argumenti: copykey - pokazivač funkcije za alociranje ključa
 *             freekey - pokazivač funkcije za dealociranje ključa
 *             copyval - pokazivač funkcije za alocira vrijednosti




                                                                                                 320
*                  freeval - pokazivač funkcije za dealocirane vrijednosti
 */

void table_free(TABLE T);
/* Briše tablicu - oslobađa memoriju*/

int table_insert(TABLE T, void *k, void *v);
/* Umeće simbol <k,v>, u tablicu simbola, ako već nije u tablici.
 * Argumenti: T - tablica simbola (rječnik)
 *            k - ključ simbola
 *            v - pokazivač vrijednosti simbola
 * Vraća: 1 ako je umetanje uspješno, inaće vraća 0
 */

SYMBOL table_find(TABLE T, void *k);
/* Traži simbol poznatog ključa k u tablici simbola T
 * Argumenti: T – tablica simbola (rječnik)
 *             k – ključ simbola
 * Vraća: pokazivač simbola,
 *         ili NULL ako simbol nije pronađen
 */

void *table_symbol_value(SYMBOL S);
/* Vraća pokazivač vrijednosti simbola S
 * ili cijeli broj, ako je vrijednost cijeli broj
 */

void *table_symbol_key(SYMBOL s);
/* Vraća pokazivač ključa simbola S
 * ili cijeli broj, ako je ključ cijeli broj
 */

int table_delete(TABLE T, void *k);
/* Traži simbol poznatog ključa k i briše ga iz tablice
 * Argumenti: T – tablica simbola (rjecnik)
 *             k – ključ simbola
 * Vraća: 1 ako je simbol izbrisan ili 0 ako simbol nije u tablici
 */

unsigned table_size(TABLE T);
/* Vraća broj elemenata u tablici */

Pokazivači ključa i vrijednosti simbola su tipa void *. Na taj način specifikacija se može
koristiti za implementaciju generičkih tablica, za bilo koji tip ključa i vrijednosti simbola. Za
potpunu generičku primjenu ADT-a, nakon inicijalizacije treba registrirati funkcije koje se
koriste za dinamičko alociranje i dealociranje memorije. To se vrši funkcijom
table_set_aloc(). Njeni argumenti su pokazivači na funkcije za alociranje i dealociranje
ključa i vrijednosti simbola. Ovo pokazivači na funkcije se koriste i definiraju na isti način koji
je opisan kod ADT DEQUEUE u poglavlju 18.

Zbog zahtjeva za generičkim operacijama potrebno je prilagoditi i deklaracije hash funkcija. U
datoteci "hash.c" definirane su hash funkcije za rad sa stringovima (hash_str) i cijelim
brojevima (hash_int):

      /* Datoteka: hash.c */

      unsigned hash_str(void            *key, unsigned M)




                                                                                               321
{/* Vraća hash vrijednost iz intervala [0..M-1] za string key */
          unsigned hashval=0;
          char *s = key;
          while(*s != '0') {
                  hashval = *s + 31 * hashval;
              s++;
              }
              return hashval % M;
      }

      unsigned hash_int(void *p, unsigned M)
      {/* Vraća hash vrijednost iz intervala [0..M-1] za cjeli broj p */

       /* Prvi argument će biti int, iako je deklariran void *p */
              int k = (int)p;
          return k > 0? k % M : - k % M;
      }

     Prema specifikacji ADT TABLE, tablica T, čiji simboli imaju ključ i vrijednost tipa string,
inicijalizira se naredbama:

      TABLE T = new_table(1023, strcmp, hash_str);
      table_set_alloc(T, strdup, free, strdup, free);

Veličina tablice se odabire proizvoljno. Poželjno je da m bude prosti broj.

     Ako je ključ cjelobrojnog tipa inicijalizacija se vrši samo jednom naredbom. Primjerice,
tablicu veličine m=1023, kojoj su ključ i vrijednost tipa int inicijalizira se naredbom:


      TABLE T = new_table(1023, 0, 0);

To znači da u implementaciji ADT-a treba predvidjeti automatsko postavljanje funkcije
usporedbe ključeva i hash funkcije za rad s cijelim brojevima, kada su drugi i treći argument
jednaki nuli.
     Pored apstraktnog objekta TABLE, specificiran je apstraktni objekt SYMBOL. Ideja je
sljedeća: želimo da su simboli tablice dostupni korisniku. Simboli imaju dva atributa: ključ i
vrijednost. Ako želimo pronaći vrijednost (ili ključ) simbola iz tablice, koristimo najprije
funkciju table_find(), koja vraća tip SYMBOL, a zatim                     pomoću funkcije
table_symbol_value() ili table_symbol_key(), iz poznatog simbola, dobijemo
pokazivač vrijednosti ili ključa simbola. Primjerice, nakon naredbe

      char *v      = table_symbol_value(table_find(T, "xxx"));

varijabla v pokazuje string koji je vrijednost simbola koji ima ključ "xxx". Ako ključ "xxx" nije
u tablici varijabla v sadrži NULL pokazivač. Ako simbol sadrži ključ i vrijednost cjelobrojnog
tipa, tada se umjesto pokazivača radi s cijelim brojevima,primjerice

      int v     = (int) table_symbol_value(table_find(T, 123));

    Implementacije ovih funkcija i objekata tipa TABLE i SIMBOL mogu biti različite. Sada
će biti pokazana implementacije otvorene hash tablice, a zatim će biti pokazan drugi tip
implementacije koji se naziva zatvorena hash tablica.
Implementacija otvorene hash tablice




                                                                                             322
/* Datoteka htable1.c    */
/* Otvorena hash tablica */
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include "table.h"

typedef struct _symbol {       /*   čvor liste simbola              */
    struct _symbol *next;      /*   pokazivač vezane liste          */
    void *key;                 /*   pokazivač ključa simbola        */
    void *val;                 /*   pokazivač vrijednosti simbola   */
}Symbol;

typedef struct _table {
   unsigned M;                /*   veličina tablice                 */
   unsigned N;                /*   broj simbola u tablici           */
   Symbol **bucket;           /*   niz pokazivača na listu simbola */
   CompareFuncT compare;      /*   funkcije usporedbe kljuca        */
   HashFuncT hash;            /*   hash funkcija                    */
   CopyFuncT copy_key;        /*   funkcija alociranja ključa       */
   CopyFuncT copy_val;        /*   funkcija alociranja vrijednosti */
   FreeFuncT free_key;        /*   funkcija dealociranja ključa     */
   FreeFuncT free_val;        /*   funkcija dealociranja vrijednosti*/
} Table, *TABLE;


static int compareInternal(void *a, void *b)
{   /* usporedba dva integera a i b*/
    if( (int)a > (int)b ) return 1;
    else if( (int)a < (int)b ) return -1;
    else return 0;
}

static unsigned hash_int(void *p, unsigned M)
{/* Vraća hash vrijednost iz intervala [0..M-1] za cjeli broj p */
 /* prvi argument će biti int iako je deklariran void *p */
    int k = (int)p;
    return k > 0? k % M : - k % M;
}

TABLE table_new(unsigned M, CompareFuncT compare, HashFuncT hash)
{
    TABLE T = (TABLE) malloc(sizeof(Table));
    if (T == NULL) return NULL;
    T->M = M;
    T->N = 0;
    T->bucket = (Symbol **) calloc(M, sizeof(Symbol *) );
    T->hash = hash ? hash : hashInternal;
    T->compare = compare? compare : compareInternal;
    T->free_key = T->free_val = NULL;
    T->copy_key = T->copy_val = NULL;
    return T;
}

void table_set_alloc(TABLE T, CopyFuncT copykey, FreeFuncT freekey,
                         CopyFuncT copyval, FreeFuncT freeval)
{
     T->copy_key = copykey;
     T->copy_val = copyval;




                                                                         323
T->free_key = freekey;
    T->free_val = freeval;
}

static void free_node(TABLE T, Symbol *n)
{
     assert(T);
     if(n == NULL) return;
     /* oslobodi sadržaj čvora: key, val i sam čvor*/
     if(T->free_key) T->free_key(n->key);
     if(T->free_val) T->free_val(n->val);
     free(n);
}

static Symbol *new_node(TABLE T, void *key, void *val)
{
     Symbol *np;
     assert(T);
     np = (Symbol *)malloc(sizeof(Symbol));
     if (np == NULL ) return NULL;
     np->key =(T->copy_key)? T->copy_key(key) : key;
     np->val =(T->copy_val)? T->copy_val(val) : val;
     return np;
}

void table_free(TABLE T)
{
     unsigned i;
     assert(T);
     for(i=0; i<T->M; i++) {
         Symbol *p, *t;
         p = T->bucket[i];    /* glava liste */
         while (p != NULL) { /* briše listu iz memorije */
            t = p;
            p = p->next;
            free_node(T, t);
         }
     }
     free(T->bucket); free(T);
}

unsigned table_size(TABLE T)
{    assert(T); return T->N;
}

SYMBOL table_find(TABLE T, void *key)
{
     Symbol *p;
     assert(T);
     p = T->bucket[T->hash(key, T->M)];
     while( p != NULL)   {
         if (T->compare(key, p->key) == 0)
              return (void *) p;     /* pronađen simbol */
          p = p->next;
    }
    return NULL;                    /* nije pronađen */
}

void *table_symbol_value(SYMBOL S)




                                                             324
{     if(S) return S->val; else return NULL;
}

void *table_symbol_key(SYMBOL S)
{    if(S) return S->key; else return NULL;
}

int   table_insert(TABLE T, void *key, void *val)
{
      SYMBOL s;
      unsigned h;
      assert(T);
      s = table_find(T, key);
      if (s == NULL) { /* ako ne postoji ključ                */
         Symbol *np = new_node(T, key, val);
         if (np == NULL) return 0;
         h = T->hash(key, T->M);
         np->next = T->bucket[h];
         T->bucket[h] = np;
         T->N++;
         return 1:
      }
      return 0;
}

int table_delete(TABLE T, void *key)
{
       Symbol *p , *prev;
       unsigned h = T->hash(key, T->M);
       assert(T);
       prev = 0;
       p = T->bucket[h];
       while (p && T->compare(p->key, key)) {
         prev = p; p = p->next;
       }
       if (!p) return 0;
       /* p - čvor koji brišemo, prev - prethodni čvor*/
       if (prev) prev->next = p->next;
       else         T->bucket[h] = p->next; /* glava buketa */
       --T->N;
       free_node (T, p);
       return 1;
}
Testiranje se provodi programom hash_test.c. U njemu se formira tablica simbola, i u nju se
unosi nekoliko simbola. Zatim se provjerava da li se simbol "while" nalaze u tablici. Zatim se
taj simbol briše i ponovo provjerava da li je u tablici.

       /* Datoteka: hash_test.c*/
       #include <stdio.h>
       #include <stdlib.h>
       #include <string.h>
       #include "table.h"
       #include "hash.c"

       int main( )
       {
           char str[20];
           SYMBOL s;




                                                                                          325
TABLE T = table_new(127, strcmp, hash_str);
             table_set_alloc(T, strdup, free, strdup, free);

             table_insert(T,       "if", "naredba selekcije");
             table_insert(T,       "while", "petlja");
             table_insert(T,       "for", "petlja");
             table_insert(T,       "printf", "C-funkcija");

             strcpy(str, "while");
             s=table_find(T, str);
             if(s) printf("n%s -> %s", str, table_symbol_value(s));
             else printf("n%s -> nije u tablici", str);

             /*izbriši simbol i provjeri da li je u tablici*/
             table_delete(T, str);
             s = table_find(T, str);
             if(s)     printf("n%s -> %s", str, table_symbol_value(s));
             else printf("n%s -> nije u tablicin", str);
             table_free(T);
             return 0;
        }


Nakon izvršenja programa dobije se ispis:

      while -> petlja
      while -> nije u tablici

     Hash-tablica s vezanom listom omogućuje realiziranje tablice simbola u kojoj se vrlo brzo
izvršavaju operacije traženja i umetanja u tablicu. Kada se radi s vrlo velikim skupom podataka,
može se uzeti da u prosječnom slučaju obje operacije imaju složenost O(1). Samo u najgorem
slučaju, kada svi ključevi imaju ustu vrijednost, složenost iznosi O(n). Do tog zaključka se
dolazi sljedećom analizom složenosti.
     Uzmimo da je u tablici s m buketa pohranjeno n simbola. Definirajmo faktor popunjenosti
α (eng. load faktor) kao omjer α = n/m. On pokazuje koliko prosječno ima simbola u jednom
buketu. Ukupno vrijeme za pretraživanje tablice, uključujući vrijeme za proračun hash funkcije,
iznosi O(1+α). Ako uzmemo da je broj buketa m barem proporcionalan broju simbola u tablici
n, dobije se n = cm= O(m), i iz toga α = n/m=O(m)/m=O(1). Dakle, pretraživanje u prosjeku
ima složenost O(1).

20.2.2 Zatvorena hash tablica
Kada otprilike znamo koliko će elemenata sadržavati tablica, tada se ona može efikasno
realizirati kao niz koji sadržava simbole, a ne pokazivače na bukete simbola. Ideja je da se
simbol upiše u niz na indeksu koji je određen s hash vrijednošću. Pošto više simbola može imati
istu hash vrijednost, ta pozicija može biti zauzeta s prethodno upisanim simbolom. U tom
slučaju, koji se naziva kolizija, ispituje se da li se simbol može upisati na lokacijama koje
sukscesivno, ili po nekom drugom zakonu, slijede iza te lokacije. Na isti način se vrši traženje u
tablici. Niz, koji sadrži ovakvu tablicu, mora imati kapacitet m veći od broja simbola n, pa je
faktor popunjenosti zatvorene tablice uvijek manji od jedinice (α = n/m <1).

Općenito, indeks je funkcija ključa k i broja ispitivanja i. Koriste se tri načina određivanja
indeksa:

    1. Linerano ispitivanje :          indeks(k, i) = (hash(k) +i ) % m




                                                                                              326
2. Kvadratično ispitivanje:        indeks(k, i) = (hash(k) +i2 ) % m
    3. Dvostruko "heširanje" :         indeks(k, i) = (hash1(k) +i⋅ hash2(k) ) % m

U sva tri slučaja ispitivanje počinje s indeks(k, 0) = hash(k), a operacije ADT TABLE se mogu
realizirati na sljedeći način:

        insert (k,v) - umeće simbol <k,v> u niz htable, prema algoritmu:
             Pronađi najmanji i=0,1,.m. za koji htable[indeks(k,i)] nije popunjen
                Na tu poziciju upiši simbol <k,v>
        find(k) - vraća vrijednost simbola koji ima ključ k, ili null, prema algoritmu:
             Algoritam:
             Za sve i za koje je redom htable[indeks(k,i)] popunjen,
                         Ako je ključ simbola htable[indeks(k,i)] jednak k,
                              Vrati vrijednost simbola
                Vrati null
        delete(k) - briše simbol koji ima ključ k, prema algoritmu:
             Pronađi simbol ključa k
             Ako je pronađen, označi da je taj simbol izbrisan

Ilustrirajmo proces linearnog ispitivanja:

U ovom primeru uzet ćemo niz veličine m=10, ključ je cijeli broj i hash funkcija je oblika
hash(k) = k % 10. Početno su sve pozicije prazne, što je označeno simbolom ∅;
    0         1         2         3        4         5         6         7           8    9
    ∅        ∅         ∅          ∅        ∅         ∅         ∅         ∅           ∅    ∅


Ako redom umećemo ključeve 15, 17 i 8, ne postoji preklapanje i rezultat je:
    0         1         2         3        4         5         6         7           8    9
    ∅        ∅         ∅          ∅        ∅         15        ∅         17          8    ∅


Umetnimo sada ključ 35. Računamo indeks(k,0) = 35 %10 = 5. Pozicija 5 je zauzeta (s ključem
15), pa moramo ispitati sljedeću poziciju. Indeks(k,1) = (5+1) %10 = 6. Pozicija 6 je slobodna
pa u nju umećemo ključ 35.
    0         1         2         3        4         5         6         7           8    9
    ∅        ∅         ∅          ∅        ∅         15        35        17          8    ∅


Umetnimo sada ključ 25. Najprije probamo poziciju (25 % 10). Ona je zauzeta. Zatim linearno
ispitujemo sljedeću poziciju 6 ((5+1) % 10). Ona je također zauzeta. Nastavljamo ispitivati
poziciju 7 ((5+2) % 10); ona je zauzeta kao i sljedeća pozicija 8 ((5+3)% 10). Slobodna je tek
pozicija 9 ((5+4) % 10) i u nju umećemo ključ 25.
    0         1         2         3        4         5         6         7           8    9
    ∅        ∅         ∅          ∅        ∅         15        35        17          8    25


Umetnimo sada ključ 75. Ponovo se ispituje počevši od pozicije 5 (75 % 10). Ispitivanje
pozicija 6, 7, 8 i 9, pokazuje da su one zauzete. Ključ 75 umećemo tek na poziciji 0 ((5+5) %
10), koja je slobodna.
    0         1         2         3        4         5         6         7           8    9
   75        ∅         ∅          ∅        ∅         15        35        17          8    25




                                                                                               327
Analiza efikasnosti pristupa zatvorenoj hash tablici
Prethodni primjer pokazuje problem kod linearnog ispitivanja: kada više ključeva ima istu hash
vrijednost tada oni kumulativno povećavaju grupiranje oko primarnih pozicija (eng. clustering).
Problem se može umanjiti upotrebom kvadratičnog ispitivanja ili još bolje dvostukim
"heširanjem", ali i tada postoje problemi:
     o Problem je kvadratičnog ispitivanja da se njime sigurno pronalazi slobodna pozicija
        samo ako je tablica ispunjena manje od polovine i ako je kapacitet tablice m prosti broj.
     o Problem dvostrukog heširanja dolazi do izražaja kada je ključ tipa string. Tada
        dvostruko računanje hash vrijednosti često zazima više vremena nego višestruko
        ispitivanje slobodnog mjesta u tablicu. Kada je ključ cjelobrojnog tipa, dvostruko
        haširanje daje izvrsne rezultate, jer se može provesti jednostavnim funkcijama hash1(k)
        = k % m i hash2(k) = 1+ k %(m-1) uz uvjet da je m prosti broj.

Uz pretpostavku da hash funkcija uniformno raspodjeljuje ključeve u tablici, može se izvesti da
se pri operaciji insert, u tablici s faktorom popunjenosti α, u prosječnom slučaju vrši 1/(1-α)
ispitivanja. To slijedi iz činjenice da faktor popunjenosti α = n/m predstavlja vjerojatnost
pronalaženja popunjenog mjesta u tablici. Zbog toga, vjerojatnost nalaženja slobodnog mjesta
iznosi (1-α), pa zaključujemo da očekivani broj ispitivanja pri traženju slobodnog mjesta iznosi
1/(1-α). Analogno, kod bacanja kocke vjerojatnost pojave broja tri iznosi 1/6, pa se može
očekivati da se taj broj pojavi prosječno u 6 bacanja.
     Prethodno razmatranje pokazuje da pri popunjenosti tablice 50% prosječno treba izvršiti 2
ispitivanja, a pri popunjenosti 90% treba izvršiti 10 ispitivanja.
     Prije opisani problem grupiranja kod linearnog ispitivanja znatno pogoršava ovu
idealiziranu procjenu. Knuth je statitičkom analizom izveo da tada prosječno stvarni broj
ispitivanja iznosi (1 + 1/(1 - α)2)/2. To znači da pri popunjenosti tablice 50% prosječno treba
izvršiti 2,5 ispitivanja, a pri popunjenosti 90% treba izvršiti 50,5 ispitivanja.
Implementacija hash tablice metodom kvadratičnog ispitivanja
Specifikacija za implementaciju tablice je u prije opisanoj datoteci "table.h", a koriste se
prethodno definirani algoritmi. Tablica je definirana kao dinamički niz koji sadrži strukture tipa
Symbol. Ta struktura pored vrijednosti ključa (key) i vrijednost (val), sadrži podatak o statusu
simbola. Ako je status = 0 (EMPTY) simbol tek treba bit definiran. Ako je status = 1 (FULL)
simbol je definiran, a ako je status = -1 (DELETED) simbol je izbrisan.

Kod kvadratično ispitivanja indeks se računa prema izrazu

    indeks(k, i) = (hash(k) +i2 ) % m

Kvadriranje je spora operacije, pa se za efikasni proračun indeksa koristi iterativni izraz:

    indeks(k, i) = (indeks(k, i-1) + 2*i - 1 ) % m

Lako je pokazati da su dva izraza ekvivalentna; dovoljno je uvrstiti prvi izraz u desnu stranu
dugog izraza (za indeks i-1).

Kod kvadratičnog ispitivanja se traži da veličina tablice bude duplo veća od broja elemenata, te
da veličina tablice bude prosti broj. Zbog toga se pri inicijalizaciji tablice poziva funkcija
get_next_prime(M); koja daje prvi prosti broj koji je veći od M.

/* Datoteka htable2.c */
/* Zatvorena hash tablica s kvadratičnim ispitivanjem*/




                                                                                               328
#include   <stdlib.h>
#include   <string.h>
#include   <assert.h>
#include   "table.h"

typedef struct _symbol {
    void *key;              /*   pokazivač ključa simbola          */
    void *val;              /*   pokazivač vrijednosti simbola     */
    short status;           /*   status: prazno(0),                */
}Symbol;                    /*   popunjeno(1) ili izbrisano (-1)   */

typedef struct _table {
    unsigned M;                  /*   veličina tablice                    */
    unsigned N;                  /*   broj elemenata u tablici            */
    Symbol *htable;              /*   dinamièki alocirani niz simbola     */
    CompareFuncT compare;        /*   funkcije usporedbe kljuèa           */
    HashFuncT hash;              /*   hash funkcija                       */
    CopyFuncT copy_key;          /*   funkcija alociranja kljuca          */
    CopyFuncT copy_val;          /*   funkcija alociranja vrrijednosti    */
    FreeFuncT free_key;          /*   funkcija dealociranja kljuca        */
    FreeFuncT free_val;          /*   funkcija dealociranja vrijednosti   */
} Table, *TABLE;

#define   KEY(i)           (T->htable[(i)].key)
#define   VAL(i)           (T->htable[(i)].val)
#define   EMPTY(i)         (T->htable[(i)].status   == 0)
#define   SET_EMPTY(i)     (T->htable[(i)].status   = 0)
#define   FULL(i)          (T->htable[(i)].status   == 1)
#define   SET_FULL(i)      (T->htable[(i)].status   = 1)
#define   DELETED(i)       (T->htable[(i)].status   == -1)
#define   SET_DELETED(i)   (T->htable[(i)].status   = -1)
#define   NOT_FOUND         -1

static int compareInternal(void *a, void *b)
{
    if( (int)a > (int)b ) return 1;
    else if( (int)a < (int)b ) return -1;
    else return 0;
}
static unsigned hashInternal(void *p, int m)
{/* daje hash za integer*/
    int k = (int)p;
    return (k > 0)? (k % m) : (-k % m);
}

static unsigned prime_list[] =
{ /*niz prostih brojeva*/
   7u,         11u,         13u,         17u,
   19u,        23u,         29u,         31u,
   53u,        97u,         193u,        389u,
   769u,       1543u,       3079u,       6151u,
   12289u,     24593u,      49157u,      98317u,
   196613u,    393241u,     786433u,     1572869u,
   3145739u,   6291469u,    12582917u,   25165843u,
   50331653u, 100663319u, 201326611u, 402653189u,
   805306457u, 1610612741u, 3221225473u, 4294967291u
};

static unsigned get_next_prime(unsigned M)




                                                                               329
{ /*vraca sljedeci prosti broj*/
    int i, len = sizeof(prime_list)/sizeof(unsigned);
    for(i=0; i<len; i++) {
       if(prime_list[i] >= M)
             return prime_list[i];
    }
    return prime_list[len-1];
}

TABLE table_new(unsigned M, CompareFuncT compare, HashFuncT hash)
{
    TABLE T = (TABLE) malloc(sizeof(Table));
    if (!T) return NULL;
    T->M = get_next_prime(M);
    T->N = 0;
    T->htable = (Symbol *)calloc(T->M, sizeof(Symbol) );
    T->hash = hash ? hash : hashInternal;
    T->compare = compare? compare : compareInternal;
    T->free_key = T->free_val = NULL;
    T->copy_key = T->copy_val = NULL;
    return T;
}

void table_set_alloc(TABLE T, CopyFuncT copykey, FreeFuncT freekey,
                     CopyFuncT copyval, FreeFuncT freeval)
{
    T->copy_key = copykey;
    T->copy_val = copyval;
    T->free_key = freekey;
    T->free_val = freeval;
}

void table_free(TABLE T)
{
    unsigned i;
     short status;
     assert(T);
     for(i = 0; i < T->M; i++) {
          if(FULL(i)) {
             if(T->free_key) (*T->free_key)(KEY(i));
             if(T->free_val) (*T->free_val)(VAL(i));
       }
     }
    free(T->htable);
    free(T);
}

int find_sym_idx(TABLE T, void *key)
{
    unsigned i = 0;
    unsigned idx = T->hash(key, T->M);
    while (1) {
        if(EMPTY(idx))
            return NOT_FOUND;
        if(FULL(idx) && T->compare(KEY(idx), key) == 0)
            return idx;     /*pronaðen simbol*/
        i++;
       idx = (idx + (2*i-1)) % T->M;
    }




                                                                      330
return NOT_FOUND;
}

SYMBOL table_find(TABLE T, void *key)
{
    int i;
     assert(T);
     i = find_sym_idx (T, key);
    if(i != NOT_FOUND)
        return &T->htable[i];
    else
        return NULL;       /* nije pronaðen */
}

void *table_symbol_value(SYMBOL s)
{   if(s) return s->val;
    else return NULL;
}

void *table_symbol_key(SYMBOL s)
{   if(s) return s->key;
    else return NULL;
}

int table_insert(TABLE T, void *key, char *val)
{
    unsigned idx, i = 0;
    assert(T);
    if(T->N >= T->M/2)
          return 0; /*ovdje primijeni rehash funkciju*/
    idx = T->hash(key, T->M);
    for(;;) /* probaj sljedeći index*/
    {
         if(!FULL(idx)) {
              KEY(idx) = (T->copy_key)? T->copy_key(key) : key;
              VAL(idx) = (T->copy_val)? T->copy_val(val) : val;
           SET_FULL(idx);
              T->N++;
              return 1;
         }
       else if(T->compare(KEY(idx), key) == 0) {
              return 0;      /* već postoji simbol*/
       }
       i++;
       idx = (idx + (2*i-1)) % T->M;
    }
    return 0;
}

int table_delete(TABLE T, void *key)
{
    int i;
    assert(T);
    i = find_sym_idx (T, key) ;
    if(i != NOT_FOUND && !(DELETED(i))) {
       if(T->free_key) T->free_key(KEY(i));
       if(T->free_val) T->free_val(VAL(i));
        SET_DELETED(i);
        --T->N;




                                                                  331
return 1;
     }
     return 0;             /* nije pronađen */
}

unsigned table_size(TABLE T)
{
   assert(T);
   return T->N;
}

Testiranje ovog ADT provodimo s istim programom kojim je testirana prethodna
implementacija otvorene hash tablice.

Zadatak: Usporedite brzinu izvršena otvorene i zatvorene hash tablice u programu
table_timer_test.c. U tom programu su pomoću funkcije clock() mjeri vrijeme
izvršenja petlje u kojoj se umeću i brišu cjelobrojni slučajno odabrani ključevi simbola. Petlja se
ponavlja 1000000 puta.

/*Datoteka: table_timer_test.c*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <time.h>
#include "table.h"

int main()
{
    float sec;
    long start = 0L, end = 0L;
    int maxnum, maxrand, status, i;
    int val= 9;
    int key;
    TABLE T;

    /* procesiraj 100000 slučajnih brojeva */
     maxnum = 100000;
     maxrand = 1501;
     printf("maxnum = %dn", maxnum);

     T = table_new(maxrand*2, 0, 0);

     start = clock ();
     for (i = maxnum; i>0; i-- ){
         key = rand() % maxrand;
         if ( table_find(T,(void *)key) ) {
             status = table_delete(T,(void *)key);
             if (!status) printf("fail: status = %dn", status);
         } else {
             status = table_insert(T, (void *)key,(void *) val);
             if (!status) printf("fail: status = %dn", status);
         }
     }
     end = clock ();
     sec=(float)(end-start)/(float)CLOCKS_PER_SEC;
     printf("n %d kljuceva procesirano u %f ms",maxnum, sec*1000);




                                                                                               332
table_free(T);
     return 0;
}

Napomena: Otvorenu hash tablicu inicijalizirajte na veličinu maxrand/2, a zatvorenu tablicu
na veličinu maxrand*2. Mijenjajte veličinu maxrand od 127 do 10000. Primjetit ćete da se
operacije s otvorenom hash tablicom, brže izvršavaju od operacija s zatvorenom tablicom.

Zadatak: U prethodnoj implementaciji zatvorene hash tablice metodom kvadratičnog
ispitivanja izvršte sljedeću izmjenu:
     Ukoliko broj simbola u tablice n premaši vrijednost m/2, treba udvostručiti vrijednost
tablice. Na taj način tablica može primiti proizvoljan broj simbola. Za tu svrhu koristite funkciju
table_rehash(T):

      TABLE table_rehash(TABLE T )
      /* Vraća pokazivač tablice koja ima dvostuko veći kapacitet
       * od ulazne tablice T, i koja sadrži simbole it T
       */
      {
          unsigned int i;
          TABLE Tnew;
          /* Formiraj uvećanu tablicu i umetni vrijednosti iz T*/
          Tnew = table_new( get_next_prime(2*T->M));
          for( i=0; i<T->M; i++ )
             if(T->htable[i].key != NULL )
                 table_insert( Tnew, T->htable[i].key,
                                     T->htable[i].val );
          free(T->htable ); free(T);
          return Tnew;
      }


Analizom programerske prakse vidljivo je da se znatno više koristi otvorene tablice. One daju
znatno bolju efikasnost kod visoke popunjenosti tablice.


20.3 BST - binarno stablo traženja
BST je kratica za naziv binarno stablo traženja (eng. binary search tree). Temeljna
karakteristika binarnog stabla traženja je da uvijek sadrži sortirani raspored čvorova. Jedan od
elemenata BST čvora predstavlja jedinstveni ključ. Usporedbom vrijednosti ključa određuje se
red kojim se umeću ili pretražuju čvorovi stabla, prema sljedećoj rekurzivnoj definiciji.

Definicija: Binarno stablo traženja (BST) je:
    1. Prazno stablo, ili
    2. Stablo koje se sastoji od čvora koji se naziva korijen i dva čvora koji se nazivaju lijevo
         i desno dijete, a oni su također binarna stabla (podstabla). Svaki čvor sadrži ključ čija
         vrijednost je veća od vrijednosti ključa čvora lijevog podstabla, a manja ili jednaka
         vrijednosti ključa čvora desnog podstabla.




                                                                                               333
a)                                              b)


Slika 20.2 Sortirano binarno stablo, koje nastaje kad je redosljed unošenja elemenata: a) 6, 9,
3, 8, 5, 2. b) 3, 2, 5, 6, 8, 9. Najljeviji čvor sadrži najmanji ključ (2), a najdesniji čvor sadrži
najveći ključ (9). Strelice pokazuju INORDER obilazak stabla.

      Slika 20.2a) prikazuje binarno stablo u kojem su elementi uneseni redom: 6, 9, 3, 8, 5, 2,
prema prethodnoj definiciji BST. Strelice pokazuju da se INORDER obilaskom stabla dobije
sortirani raspored elemenata(2, 3, 5, 6, 8, 9). Slika 20.2b) prikazuje binarno stablo u kojem su
elementi uneseni redom: 3,2,5,6,8,9.
      Iako stabla sa slika 20.2a) i 20.3b) sadrže iste elemente, oblik stabala je različite jer oblik
ovisi o redoslijedu elemenata. Najveća visina stabla nastaje kada se unose elementi u sortiranom
rasporedu, a najmanja kada se elementi unose po slučajnom uzorku. U najboljem slučaju dobije
se potpuno stablo. Tada je potreban minimalan broj operacija za prijeći stazu od korijena do
lista, bilo za traženje ili unošenje elementa u stablo. Nešto kompleksnija statistička analiza
pokazuje da se u najboljem i u prosječnom slučaju vrši O(log2 n) operacija. U najgorem
slučaju, kod sortiranog unosa, vrši se O(n) operacija, jer stablo degenerira u "kosu" listu.
Može se zaključiti da BST predstavlja manje efikasno rješenje za izradu tablica ili rječnika od
hash tablice koji nudi efikasnost O(1). Ipak, BST nudi neke mogućnosti koje se ne može
ostvariti hash tablicom. To se u prvom redu odnosi na formiranje dinamičkih sortiranih skupova
u kojima se lako određuje raspored elemenata. Za tu svrhu definira se skup operacija:

  ADT BSTREE
  insert (T, k, v)      - umeće simbol, kojem je ključ k i vrijednost v u T, ako već nije u T.
  find (T, k)           - vraća simbol ako u T postoji simbol ključa k, ili NULL
  delete(T, k)          - briše simbol kojem je ključ k, ako postoji u T.
  minimum(T)            - vraća simbol iz T koji ima minimalnu vrijednost ključa.
  maximum(T)            - vraća simbol iz T koji ima maksimalnu vrijednost ključa.
  predecessor(T, x)     - vraća simbol iz T koji prethodi simbolu x
  succcessor(T, x)      - vraća simbol iz T koji prethodi simbolu x


Ove operacije definiraju ADT BSTREE, kojem je specifikacija operacija u C jeziku zapisana u
datoteci "bst.h".

#ifndef     _RBT_BST_H_
#define     _RBT_BST_H_

/* odstrani komentar ako zelis raditi s RED-BLACK stablom*/
/* #define RED_BLACK */

typedef struct _symbol *SYMBOL;
typedef struct bs_tree *BSTREE;




                                                                                                 334
typedef int (*CompareFuncT)(void *, void *);
typedef void *(*CopyFuncT)(void *);
typedef void (*FreeFuncT)(void *);

BSTREE bst_new(CompareFuncT compare);
/*   Stvara ADT BSTREE.
 *   Argument: compare - pokazivač funkcije za usporedbu dva ključa
 *             Ako je NULL, uzima se da stablo sadrži cijele brojeve
 *   Vraća: pokazivač ADT-a
 */

void bst_set_alloc(BSTREE T, CopyFuncT copykey, FreeFuncT freekey,
                                 CopyFuncT copyval, FreeFuncT
freeval);
/* Postavlja funkcije za alociranje (kopiranje) i dealociranje simbola
 * Argumenti: T - ADT stablo (mora biti formiran)
 *            copykey - pokazivač funkcije za kopiranje ključa
 *            copyval - pokazivač funkcija za kopiranje vrijednosti
 *            freekey - pokazivač funkcija za dealociranje ključa
 *            freeval - pokazivač funkcija za dealociranje vrijednosti
 */

void bst_free(BSTREE T);
/* Dealocira cijelo stablo T*/

int bst_size(BSTREE T);
/* Vraća broj simbola u stablu T*/

int bst_insert(BSTREE T, void * key, void *val);
/* Umeće simbol <key,val> u stablo T,
   ali samo ako ključ key nije u stablu
 * Argumenti: T - ADT stablo (mora bit formiran)
 *            key - pokazivač ključa (ili cijeli broj)
 *            val - pokazivač vrijednosti (ili cijeli broj)
 * Vraća: 1 ako je izvršeno umetanje, inače vraća 0
 */

int bst_delete(BSTREE T, void * key);
/* Briše simbol koji ima ključ key, iz stabla T
 * Argumenti: T - ADT stablo (mora bit formiran)
 *            key - pokazivač ključa (ili cijeli broj)
 * Vraća: 1 ako je izvršeno brisanje, inače vraća 0
 */

SYMBOL bst_find(BSTREE T, void * key);
/* Vraća pokazivač simbola, ako ključ postoji */

void *bst_symbol_value(SYMBOL S);
/* Vraća pokazivač vrijednosti simbola S (ili cijeli broj)*/

void *bst_symbol_key(SYMBOL s);
/* Vraća pokazivač ključa simbola S (ili cijeli broj)*/

SYMBOL bst_minimum(BSTREE T);
/* Vraća pokazivač simbola koji ima minimalni ključa u stablu T*/

SYMBOL bst_maximum(BSTREE T);
/* Vraća pokazivač simbola koji ima najveæi ključ u stablu T*/




                                                                       335
SYMBOL bst_succesor(SYMBOL S);
/* Vraća pokazivač simbola koji slijedi iza simbola S
 * PRE: S mora biti pokazivač čvora stabla
 */
SYMBOL bst_predecessor(SYMBOL S);
/* Vraća pokazivač simbola koji prethodi simbolu S
 * PRE: S mora biti pokazivač čvora stabla
 */

#endif

Primijetite da je na početku ove datoteke, unutar komentara definirano:

      #define RED_BLACK

Ako se primijeni ova definicija tada će se navedene operacije provoditi po algoritmu za tzv.
crveno-crna stabla (eng. red-black trees). Ideja i algoritmi za crveno-crna stabla bit će opisani u
sljedećem odjeljku. Ukratko, kod crveno-crnog stabla svakom se čvoru pridjeljuje boja (eng.
color). Taj se podatak koristi u algoritmima za uravnoteženje stabla, s ciljem da sve staze
stabla, i u najgorem slučaju, budu dostupne sa složenošću O(log2 n). Prije nego se objasni
postupak izgradnje crveno-crnog stabla neće se analizirati dio kôda koji je napisan unutar

      #ifdef RED_BLACK
          ........                      /* kod za RED-BLACK stabla */
      #endif

     Implementacija ADT BSTREE je u dana u datoteci "bst.c". Najprije je izvršeno definiranje
struktura za implementaciju ADT BSTREE. Prva struktura, struct _simbol, opisuje čvor
binarnog stabla. Također je s typedef definiran i sinonim za ovu strukturu imena Node.
Definirana su tri pokazivača: na lijevo dijete, na desno dijete i na roditelja. Pokazivač na
roditelja je potreban za implementaciju operacija successor() i predecessor(). Ako ove
operacije nisu potrebne, tada se može izvršiti implementacija ADT-a i bez pokazivača na
roditelja.
     Simboli su određeni ključem (key) i vrijednošću (val). U ovoj implementaciji key i val
su definirani tipom void *, što znači da će se dinamičkim alociranjem memorije moći
registirati simbole bilo kojeg tipa ključa i vrijednosti. Primijetimo da se u strukturi Node
opciono bilježi "boja" čvora u članu Color, koji može imati samo dvije pobrojane vrijednosti:
RED i BLACK.
     Za potpunu generičku implementaciju pri inicijalizaciji ADT-a bit će potrebno registrirati
funkcije koje se koriste za dinamičko alociranje i dealociranje memorije. Pokazivači na ove
funkcije zajedno s pokazivačem korijena stabla se bilježe u strukturi bs_tree. Tip pokazivača
na ovu strukturu - BSTREE služi za označavanje ADT-a.

/* Datoteka: bst.c
 * Implementacija bst ili red-black stabla (ako je definirano
RED_BLACK)
 */
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include "bst.h"

typedef enum { BLACK, RED } nodeColor;

typedef struct _symbol Node;




                                                                                               336
struct _symbol {
    struct _symbol *left;                   /*   lijevo dijete          */
    struct _symbol *right;                  /*   desno dijete           */
    struct _symbol *parent;                 /*   roditelj               */
    void * key;                             /*   ključ simbola          */
    void * val;                             /*   vrijednost simbola     */
#ifdef RED_BLACK                            /*                          */
    nodeColor color;                        /*   boja (BLACK, RED)      */
#endif
};

struct bs_tree {
    int N;
    Node *root;
    CompareFuncT compare;          /*   funkcija    za   usporedbe ključa               */
    CopyFuncT copy_key;            /*   funkcija    za   alociranje ključa              */
    CopyFuncT copy_val;            /*   funkcija    za   alociranje vrijednosti         */
    FreeFuncT free_key;            /*   funkcija    za   dealociranje ključa            */
    FreeFuncT free_val;            /*   funkcija    za   dealociranje vrijednosti       */
};




Slika 20. 3. Standardni i prošireni oblik binarnog stabla. Kod standardnog oblika listovi
nemaju djece i pokazuju na NULL. Kod proširenog oblika stabla svi su podaci smješteni u
unutarnjim čvorovima. Listovi ne sadrže podatke već su to virtualni čvorovi na koje pokazuje
jedinstveni NIL pokazivač.

Zbog lakše izvedbe algoritama koristit će se prošireni oblik binarnog stabla, u kojem se svi
podaci nalaze u unutarnjim čvorovima, a vanjski čvorovi su tzv. NIL čvorovi (slika 20.2), tj.
čvorovi na koje pokazuje NIL pokazivač. Roditelj korijena stabla također je NIL čvor. Vanjski
čvorovi ne sadrže podatke, oni služe jedino kao graničnici u nekim algoritmima, pa se za vezu s
vanjskim čvorovima koristi jedinstveni pokazivač - NIL - koji pokazuje na jedan globalni čvor
kojeg se naziva sentinel. Sentinel pokazuje na samog sebe, jer mu se svi pokazivači (left,
right i parent) inicijaliziraju na vrijednost NIL. To je ostvareno definicijama:

      #define NIL &sentinel             /* svi listovi su NIL sentinel */

      #ifdef RED_BLACK
      static Node sentinel = { NIL, NIL, NIL, 0, 0, BLACK};
      #else
      static Node sentinel = { NIL, NIL, NIL, 0, 0};
      #endif

Zbog preglednijeg zapisa programa koristit će se sljedeće definicije:

        #define     Left(x)        (x)->left




                                                                                             337
#define     Right(x)       (x)->right
       #define     Parent(x)      (x)->parent
       #define     Color(x)       (x)->color
       #define     Key(x)         (x)->key
       #define     Val(x)         (x)->val
       #define     Root(x)        (x)->root

Prve operacije koje treba definirati su operacije kojima se inicijalizira ADT i kojima se briše
objekt ADT iz memorije. Funkcija bst_new() prima za argument pokazivač na funkciju za
usporedbu ključeva. Ako je ovaj argument jednak nuli postavlja se interna funkcija
CompareInternal() kojo se vrši usporedba cijelih brojeva. Korijen stabla je početno prazan i
njemu se pridjeljuje vrijednost NIL pokazivača.

      static int CompareInternal(void *a, void *b)
      { /*funkcija usporedbe cijelih brojeva*/
          if( (int)a > (int)b ) return 1;
          else if( (int)a < (int)b ) return -1;
              else return 0;
      }

      BSTREE bst_new(CompareFuncT compare)
      {
          BSTREE T =(BSTREE)malloc(sizeof(struct bs_tree));
          if(T){
              Root(T) = NIL;
              T->N = 0;
              T->compare = compare? compare : compareInternal;
              T->free_key = T->free_val = NULL;
              T->copy_key = T->copy_val = NULL;
          }
          return T;
      }

      void bst_set_alloc(BSTREE T, CopyFuncT copykey, FreeFuncT freekey,
                         CopyFuncT copyval, FreeFuncT freeval)
      {
          /*registriraj funkcije za kopiranje i dealociranje */
          T->copy_key = copykey;
          T->copy_val = copyval;
          T->free_key = freekey;
          T->free_val = freeval;
      }

    Dvije interne funkcije newNode() i freeNode() služe za formiranje i dealociranje novog
čvora, Funkcija deletaAll() , koristeći "postorder" obilazak stabla, briše sve čvorove. Nju
koristi javna funkcija bst_free() kojom se dealocira cijelo stablo.

      static Node *newNode(BSTREE T, void * key, void *val, Node
      *parent)
      {
          Node *x = (Node *) malloc (sizeof(Node));
          if (x == 0) return 0;
          Left(x) = NIL;
          Right(x) = NIL;
          Key(x) = (T->copy_key)? T->copy_key(key) : key;
          Val(x) = (T->copy_val)? T->copy_val(val) : val;
          Parent(x) = parent;
      #ifdef RED_BLACK




                                                                                           338
Color(x) = RED;
      #endif
          return x;
      }

      static void freeNode(BSTREE T, Node *n)
      {
          if(n != NIL && n != NULL) {
              if(T->free_key) T->free_key(n->key);
              if(T->free_val) T->free_val(n->val);
              free(n);
          }
      }

      static void deletaAll(BSTREE T, Node *n )
      {/* dealocira čvorove */
          if( n != NIL && n != NULL) {
              deletaAll(T, Left(n));
              deletaAll(T, Right(n));
              freeNode(T, n);
          }
      }

      void bst_free(BSTREE T)
      {/* dealocira cijelo stablo*/
          assert(T);
          deletaAll(T, Root(T));
          free(T);
      }

Najednostavnija je operacija size() koja vraća broj simbola u stablu:

      int bst_size(BSTREE T)
      {   assert (T);
          return T->N;
      }

Operacija traženja se jednostavno implementira funkcijom bst_find(). Polazi se od korijena
stabla i ispituje da li čvor sadrži ključ koji je veći, manji ili jednak argumentu key. Ako je
jednak, traženje je završeno i funkcija vraća pokazivač čvora. Ako je argument key veći od
ključa u čvoru, traženje se nastavlja u desnom stablu, inače se traženje nastavlja u lijevom
čvoru. Traženje se ponavlja sve do čvor ne pokazuje NIL. Ako se ne pronađe traženi ključ
funkcija vraća NULL.

      SYMBOL bst_find(BSTREE T, void * key)
      {/* Vraca pokazivač simbola ako pronađe key, ili NULL */
          Node *n;
          assert(T);
          n = Root(T);
          while(n != NIL) {
              int cmp = T->compare(key, Key(n));
              if(cmp == 0)
                  return n;
              else if(cmp > 0)
                  n = Right(n);
              else /*(cmp < 0)*/
                  n = Left(n);
          }




                                                                                          339
return NULL;
      }


    Funkcija bst_find() vraća vrijednost tipa SYMBOL. Iako u implementaciji SYMBOL
predstavlja pokazivač čvora, tom se čvoru ne može pristupiti jer je implementacija skrivena od
korisnika. Za dobivanje ključa i vrijednosti simbola definirane su funkcije bst_symbol_key()
i bst_symbol_value().

          void *bst_symbol_value(SYMBOL s)
          {   if(s) return s->val;
              else return NULL;
          }
          void *bst_symbol_key(SYMBOL s)
          {   if(s) return s->key;
              else return NULL;
          }

    Pronalaženje maksimalne vrijednosti ključa je trivijalno; počevši od korijena stabla ispituje
se desni čvor, sve do lišća stabla, jer najdesniji čvor ima maksimalni ključ. Kod traženja
najmanjeg elementa ispituje se lijevi čvor jer najljeviji čvor ima minimalni ključ.

      SYMBOL bst_maximum(BSTREE T)
      {/* vraća simbol s najvećim ključem u sortiranom stablu T */
          Node *n;
          assert(T);
          n = T->root;
          while (Right(n) != NIL) n = Right(n);
          return (n == NIL)? NULL : n;
      }

      SYMBOL bst_minimum(BSTREE T)
      { /* vraća simbol s minimalnim ključem u sortiranom stablu T*/
          Node *n;
          assert(T);
          n = T->root;
          while (Left(n) != NIL) n = Left(n);
          return (n == NIL)? NULL : n;
      }

Slijedi opis funkcija bst_predecessor(x) i bst_successor(x). Sljednik čvora x (successor) je čvor
s najmanjim ključem koji je veći od Key(x). Analogno tome je definiran prethodnik čvora x
(predecessor) kao čvor s najvećim ključem koji je manji od Key(x).

      SYMBOL bst_succesor(SYMBOL x)
      { /* Vraća sljednika od čvora x
         * ili NULL ako sljednik ne postoji*/
        /* PRE: x mora biti čvor stabla */
          Node *y;
          if(x == NIL || x == NULL) return NULL;
          if((y = Right(x)) != NIL){
              while (Left(y) != NIL) y = Left(y);
              return y; /* minimum desnog stabla od x*/
          }
          y = Parent(x);
          while (y != NULL && x == Right(y)) {
              x = y;




                                                                                             340
y = Parent(y);
           }
           return y;
      }


Funkcija successor(x) vrši dvije analize. Prvo ako, čvor x ima desno dijete, znači da postoji
veća vrijednost u desnoj stazi. Nju se dobije kao minimum(Right(x)). U drugom slučaju, ako
je Right(x) == NULL, tada treba unatrag analizirati roditelja čvora i tražiti njegovo lijevo
dijete (ukoliko je x desno dijete). Na analogan način se implementira funkcija
predecessor(x).

      SYMBOL bst_predecessor(SYMBOL x)
      {/* Vraća prethodnika od čvora x ili NULL ako ne postoji*/
       /* PRE: x mora biti čvor stabla */
          Node * y;
          if(x == NIL || x == NULL) return NULL;
          if ((y = Left(x)) != NIL) {
              while (Right(y) != NIL) y = Right(y);
              return y; /* maksimum lijevog stabla od x*/
          }
          else {
              y = Parent(x);
              while(y != NULL && x == Left(y)) {
                  x = y;
                  y = Parent(y);
              }
              return(y);
          }
      }

Umetanje novog simbola (čvora) stabla se vrši prema sljedećem algoritmu:

   1) Analiziraj stablo od korijena do lista.
          Ako je zadani ključ manji od ključa u čvoru, analiziraj čvor lijevo, inače analiziraj
          čvor desno.
          Ako je zadani ključ jednak ključu u čvoru, prekini ispitivanje, jer se simbol već
          postoji.
          Ako dođeš do lista, zapamti njegovog roditelja i prekini ispitivanje.
   2) Alociraj novi čvor s zadanim ključem, vrijednošću i roditeljem.
   3) Ako je roditelj jednak NULL novi čvor postaje korijen stabla inače postaje lijevo ili
      desno dijete, ovisno o tome da li je ključ bio manji ili veći od ključa u čvoru roditelja.

Ovaj algoritam je implementiran u funkciji bst_insert().

      int bst_insert(BSTREE T, void * key, void *val)
      {/*Vraća 1 ako je umetnut simbol <key, val> u stablo T, ili 0*/
          Node *parent = 0;
          Node *x = Root(T);
          while (x != NIL) {
              int cmp = T->compare(key, Key(x));
              if(cmp == 0) /*ako postoji ključ završi i vrati 0*/
                  return 0;
              parent = x;
              if(cmp > 0)    x = Right(x);
              else /*(<0)*/ x = Left(x);
          }




                                                                                            341
/* stvori novi čvor */
           x = newNode(T, key, val, parent);
           if (x == 0) return 0;

           /* umeti čvor stablo */
           if(parent) {
               int cmp = T->compare(key, Key(parent));
               if(cmp < 0) Left(parent) = x;
               else         Right(parent) = x;
           } else {
               Root(T) = x;
           }

      #ifdef RED_BLACK
          insertFixup(T,x);
      #endif
          T->N++;
          return 1;
      }

Operacija brisanja čvora je implementirana u funkciji bst_delete(). Treba uočiti tri slučaja:
   1) čvor koji treba izbrisati je list
   2) čvor koji treba izbrisati ima samo jedno dijete
   3) čvor koji treba izbrisati ima dvoje djece

Ilustrirani su na slici 20.4. Prva dva slučaja su jednostavna: čvor odstranjujemo na način da
roditelju čvora pridijelimo pokazivač na dijete čvora (koje može biti NIL), a zatim dealociramo
čvor. U trećem slučaju čvor ima dva djeteta. Njega možemo odstraniti, tako da u njega upišemo
sadržaj čvora s prvim većim ključem (to je čvor s najmanjim ključem u desnom podstablu), a da
zatim odstranimo taj prvi veći čvor. Pošto je prvi veći čvor najmanji čvor u desnom podstablu,
on ne može imati lijevo dijete, pa se njega briše jednostavno, kao u prva dva slučaja.




        Slika 20.4 Tri slučaja brisanja čvora (podebljeno je prikazan čvor koji se briše)




                                                                                            342
int bst_delete(BSTREE T, void * key)
      {
          Node *x, *y, *z;

           z = bst_find(T, key);
           if (!z) return 0;

           if (Left(z) == NIL || Right(z) == NIL) {
               /* y ima NIL djecu - mozeš ga premostiti */
               y = z;
           } else { /* ako postoje oba djeteta - nađi prvog većeg    */
               /* to je minimalni s desna - on ima samo jedno dijete */
               y = Right(z);
               while (Left(y) != NIL) y = Left(y);
           }
               /* y ćemo kasnije odstraniti */
           /* y ima jedno dijete, spremi to dijete u x */
           x = (Left(y) != NIL)? Left(y) : Right(y);

           /* odstrani y iz lanca roditelja */
           /* tako da x postane lijevo ili desno dijete od Parent(y)*/
           Parent(x) = Parent(y);
           if (Parent(y) == NIL)           Root(T) = x;
           else if (y == Left(Parent(y))) Left(Parent(y)) = x;
           else                            Right(Parent(y)) = x;

          if (y != z) { /*zamijeni sadržaj y i z*/
              void *tmp = Key(z); Key(z) = Key(y); Key(y) = tmp;
              tmp = Val(z); Val(z) = Val(y); Val(y) = tmp;
          }
       #ifdef RED_BLACK
          if (Color(y) == BLACK)
              deleteFixup (T,x);
      #endif
          freeNode(T, y);
          T->N--;
          return 1;
      }

Testiranje ADT BSTREE se može provesti programom "bst_test.c". Demonstrira se umetanje,
traženje i brisanje simbola s cjelobrojnim ključem, te ispis u sortiranom rasporedu od manjeg
prema većem ključu i obrnuto.


      /*Datoteka: bst_test.c*/
      #include <stdio.h>
      #include <stdlib.h>
      #include <string.h>
      #include <assert.h>
      #include "bst.h"

      int main() {
          int maxnum, ct, status;
          int key, val= 9;
              SYMBOL s;
          BSTREE T = bst_new(0);




                                                                                         343
/* procesiraj 100 slučajnih brojeva */
           maxnum = 100;
           printf("maxnum = %dn", maxnum);

                for (ct = maxnum; ct; ct-- ) {
                key = rand() % 19 + 1;
                if ( bst_find(T,(void *)key) ) {
                    status = bst_delete(T,(void *)key);
                    if (!status) printf("fail: status = %dn", status);
                } else {
                    status = bst_insert(T, (void *)key,(void *) val);
                    if (!status) printf("fail: status = %dn", status);
                }
           }

               printf("nU stablu ima %d simbola.n", bst_size(T));
           s = bst_minimum(T);
           while(s) {
                  printf("%d ", (int)bst_symbol_key(s) );
              s = bst_succesor(s);
               }

           printf("nReverzni ispis:n");
               s = bst_maximum(T);
               while(s){
                  printf("%d ", (int)bst_symbol_key(s) );
                  s = bst_predecessor(s);
               }
               bst_free(T);
               return 0;
      }


Nakon izvršenja programa dobije se ispis:

      U stablu ima 8 simbola.
      1 5 6 7 12 16 17 19
      Reverzni ispis:
      19 17 16 12 7 6 5 1



20.4 Crveno-crna stabla
     Crveno-crno stablo (eng. red-black tree) je modificirano binarno stablo traženja kojem
svaki čvor ima jedan dodatni atribut: može biti crven ili crn. Crveno-crno stablo je određeno
sljedećim svojstvima:

    Definicija: Crveno-crno stablo je binarno stablo sa svojstvima:
    1. Svaki čvor je ili crn ili crven.
    2. Korijen je crn
    3. Svaki list (NIL) je crn.
    4. Ako je čvor crven, njegova oba djeteta su crni (to znači da roditelj crvenog čvora ne
       smije biti crven).
    5. Svaka prosta staza od nekog čvora do listova sadrži jednak broj crnih čvorova.




                                                                                         344
Posljedica ove definicije je a) da ni jedna staza od korijena do NIL čvorova nije duplo dulja od
ostalih staza i b) visina crveno-crnog stabla s n čvorova iznosi najviše 2log2(n+1). Ovo svojstvo
se lako dokazuje.

Dokaz: Prvo ćemo dokazati da bilo koje podstablo čvora x sadrži barem 2bh(x)-1 unutarnjih
čvorova. Funkcija bh(x) daje broj crnih čvorova u bilo kojoj stazi od čvora x (ne uključujući x)
do listova. Ako je visina 0, x je list (NIL) - tada ima 2bh(x)-1 = 20-1= 0 unutarnjih čvorova.
Razmotrimo sada unutarnji čvor s dva djeteta. Njegova visina je bh(x) ili bh(x)-1 ovisno o tome
da li je dijete crveno ili crno. Pošto je to dijete od x ima manju visinu od x, možemo zaključiti
da dijete ima barem 2bh(x)-1-1 unutarnjih čvorova, pa podstablo od x ima barem (2bh(x)-1-1)+
(2bh(x)-1-1)+1 = 2bh(x)-1 unutarnjih čvorova, čime je dokazan početni stav.
     Ako je visina stabla h, a prema svojstvu 4 bar pola čvorova mora biti crno, tada bh visina
korijena mora biti barem h/2, pa broj čvorova ispod korijena iznosi n ≥ 2h/2-1. Logaritmiraući
ovaj izraz dobijemo da je h ≤ 2 log2(n+1).

     Ovaj dokaz pokazuje da operacije traženja u crveno-crnom stablu uvijek ima složenost
O(logn). Postavlja se pitanje, kako izvršiti operacije umetanja i brisanja, uz uvjet da se uvijek
zadrži crveno-crno svojstvo stabla. Ideja je jednostavna, nakon umetanja čvora (i nakon brisanja
čvora) treba provjeriti raspored crnih i crvenih čvorova. Ako raspored ne zadovoljava prethodnu
definiciju svojstava crveno-crnog stabla, treba izvršiti transformaciju stabla.
     Transformacije stabla se vrše pomoću dva temeljna postupka - lijeve i desne rotacije
čvorova. Ti postupci su ilustrirani na slici 20.5. Primjetite da nakon obje rotacije raspored
čvorova ostaje nepromijenjen: Ključ(A) < Ključ(B) < Ključ (C), dakle rotacije mijenjaju oblik
stabla, ali ne mijenjaju "inorder" raspored čvorova.




                           Slika 20.5 Desna i lijeva rotacija čvorova

Rotacije čvorova sa slike 20.5 vrše se pomoću funkcija rotateLeft() i rotateRight().

      #ifdef RED_BLACK

      static void rotateLeft(BSTREE T, Node *x)
      {
          Node *y; /* rotira čvor x u lijevo */
          assert (x);
          assert(Right(x));
          y = Right(x);
          /* veza s Right(x) */
          Right(x) = Left(y);
          if (Left(y) != NIL) Parent(Left(y)) = x;
          /* poveži s Parent(y) */
          if (y != NIL) Parent(y) = Parent(x);
          if (Parent(x)) {
              if (x == Left(Parent(x)))
                   Left(Parent(x)) = y;
              else




                                                                                             345
Right(Parent(x)) = y;
            } else {
                Root(T) = y;
            }
            /* poveži x i y */
            Left(y) = x; Parent(x) = y;
      }

      static void rotateRight(BSTREE T, Node *x)
      {
          Node *y;   /* rotira čvor x u desno */
          assert (x);
          y = Left(x);
          /* poveži s Left(x) */
          Left(x) = Right(y);
          if (Right(y) != NIL) Parent(Right(y))= x;
          /* poveži s Parent(y) */
          if (y != NIL) Parent(y) = Parent(x);
          if (Parent(x)) {
              if (x == Right(Parent(x))) Right(Parent(x)) = y;
              else                       Left(Parent(x)) = y;
          } else {
              Root(T) = y;
          }
          /* poveži x i y */
          Right(y) = x; Parent(x) = y;
      }

      #endif

     Ove funkcije će biti korištene u funkciji insertFixup(), kojom se ispravlja ravnoteža
stabla nakon umetanja čvora, i u funkciji deleteFixup(), kojom se ispravlja ravnoteža stabla
nakon brisanja čvora. Funkcija insertFixup() se poziva na završetku funkcije
bst_insert(), a funkcija deleteFixup()                se poziva na završetku funkcije
bst_delete(). Najprije će biti opisan algoritam koji se koristi u funkciji insertFixup().


Uravnoteženje stabla nakon umetanja čvora
     Prilikom umetanja čvora, u funkciji bst_insert(), uvijek se umeće crveni čvor. Ako je i
roditelj umetnutog čvora crven, nastaje neuravnoteženo stablo jer više nije zadovoljeno svojstvo
4 iz definicije crveno-crnog stabla. Ta se neuravnoteženost ispravlja pozivom funkcije
insertFixup().
     Slike 20.6 i 20.7 omogućuju objašnjenje algoritma iz ove funkcije. Ukoliko umetnuti čvor
nije korijen stabla, funkcija se izvršava unutar petlje sve dok je roditelj umetnutog čvora crven.
Analizira se i ispravlja uravnoteženost stabla. Prvo se analiziraju ujaci umetnutog čvora:
(Left(Parent(Parent(x))) i Right(Parent(Parent(x))). Ako su oni crveni, dovoljno
je promijeniti boju čvorova, kao na slici 20.6, a ako su crni tada se vrši jednostruka ili dvostruka
rotacija, uz promjenu boje čvorova, prema slici 20.7. U analizi nas ne zanimaju djeca umetnutog
čvora, jer ako je roditelj umetnutog čvora crven, tada je, prema svojstvu 4, sigurno da su mu
djeca crna.




                                                                                                346
Slika 20.6 Prvi slučaj analize umetanja čvora x. Crni čvorovi su prikazani tamnije. Nije
zadovoljeno svojstvo 4 jer su crveno obojani i x i njegov roditelj. Ujak od x, odnosno
y=Right(Parent(Parent(x))) je također crven. Svako podstablo A,B,C,D i E ima crni korijen i
jednaku crnu-visinu. Nisu nužne rotacije već samo izmjena boje čvorova. Nakon prikazane
promjene boje čvorova while petlja nastavlja s čvorom "Novi x" i ispituje mogući problem, tj.
da li je njegov roditelj crven.

      #ifdef RED_BLACK

      static void insertFixup(BSTREE T, Node *x)
      {
          /* stvara Red-Black ravnotežu nakon umetanja čvora x */
          /* podrazumjeva se da je Color(x) = RED;             */
          while (x != Root(T) && Color(Parent(x)) == RED)
          {
              if (Parent(x) == Left(Parent(Parent(x)))) {
                  Node *y = Right(Parent(Parent(x)));
                  if (Color(y) == RED) {/* ujak je RED - slučaj 1 */
                      Color(Parent(x)) = BLACK;
                      Color(y) = BLACK;
                      Color(Parent(Parent(x))) = RED;
                      x = Parent(Parent(x));
                  } else { /* ujak je BLACK - slučaj 2*/
                      if (x == Right(Parent(x))) {
                          /* postavi x za lijevo dijete */
                          x = Parent(x);
                          rotateLeft(T, x);
                      }
                      /* oboji i rotiraj - slučaj 3 */
                      Color(Parent(x)) = BLACK;
                      Color(Parent(Parent(x)))= RED;
                      rotateRight(T, Parent(Parent(x)));
                  }
              } else {/* slika u zrcalu prethodnog slučaja*/
                  Node *y = Left(Parent(Parent(x)));
                  if (y->color == RED) {/* ujak je RED - slučaj 1 */
                      Color(Parent(x)) = BLACK;




                                                                                           347
y->color = BLACK;
                                     Color(Parent(Parent(x))) = RED;
                                     x = Parent(Parent(x));
                                 } else { /* ujak je BLACK - slučaj 2*/
                                     if (x == Left(Parent(x))) {
                                         x = Parent(x);
                                         rotateRight(T, x);
                                     }
                                     /* oboji i rotiraj - slučaj 3 */
                                     Color(Parent(x)) = BLACK;
                                     Color(Parent(Parent(x))) = RED;
                                     rotateLeft(T, Parent(Parent(x)));
                                 }
                    }
                }
                Color(Root(T))= BLACK;
      }
      #endif




                                                                            z
                                                                                         y
                                                                                 D
                                                                   u
                                                                                             Slučaj 2
                                                           A               v         x
                                rotateLeft(u)

                                                                       B         C

                            z                                                                                           u
                                    y
                                                                                                                    y
                                D                                           v
                    v                                                                                               A           v
                                        rotateRight(z)
            x                                                                                       rotateLeft(u)                           x
                                                           x       u             z
                u       C                                                                                                   B           z

                                                               A           B C           D
        A           B                                                                                                               C       D

            Slučaj 3                                                                                                                Slučaj 3
                                                                            v
                                                                   y
                                                                   A                 z                         rotateRight(z)


                                                                       x    u                D
                                                Slučaj 2

                                                                       B         C


Slika 20.7 Drugi i treći slučaj analize umetanja čvora x. Sada su nužne jednostruke i dvostruke
rotacije. Svako podstablo A,B,C,D i E ima crni korijen i jednaku crnu-visinu. Slovom x je
označen čvor koji se analizira unutar while petlje (to je umetnuti čvor), a y označava njegovog
lijevog ili desnog ujaka. Uočite da se slučaj 2 i 3 razlikuju od slučaja 1 po boji ujaka. U slučaju
2 vrši se rotacija Parent(x) i dobije se slučaj 3. Zatim se vrši rotacija Parent(Parent(x) u
konačni oblik.

Vremensku složenosti umetanja čvora procjenjujemo na sljedeći način:




                                                                                                                                                348
1. Funkcija bst_insert() se izvršava u O(lg n) vremena jer se mjesto za umetanje
       čvora traži u uravnoteženom stablu.
   2. U funkciji insertFixup() svaka iteracija uzima O(1) vremena, i pri svakoj iteraciji
       se pomičemo dvije razine naviše. Pošto ima O(log2n) razina, vrijeme izvršenja
       insertFixup() je također O(log2n)
Ukupno, umetanje u crveno-crno stablo ima vremensku složenost O(log2n).
Uravnoteženje stabla nakon brisanja čvora
Nešto kompliciraniji zahtjevi za uravnoteženjem stabla se javljaju kada se u funkciji
bst_delete() izbriše čvor y. Problem nastaje ako se izbriše crni čvor jer tada može nastati
slučaj da su čvor i njegovo dijete crveni. To je u suprotnosti sa svojstvom 3 crveno-crnog stabla.
Zbog toga se na kraju funkcije bst_delete(T, y) poziva funkcija deleteFixup(T,x)
koja vrši uravnoteženje stabla. Čvor x je jedino dijete izbrisanog čvora y (možda jednak NIL).

Unutar ove funkcije se prvo analizira boja od x, sa sljedećim posljedicama:
   1. Ako je boja od x crvena, ili ako je x korijen stabla, tada se ne izvršava petlja, već se
       jedino boja od x promijeni u crnu boju. U tom slučaju nije potrebno analizirati
       uravnoteženost stabla, jer ako smo izbacili crni čvor (y), sada sada povratili crni čvor (x)
       Time smo osigurali da je crna-visina nepromijenjena. Pošto smo izbacili i crveni čvor
       (x) osigurali smo se da neće biti dva crvena čvora u odnosu dijete-roditelj.
   2. Ako je boja od x crna, i ako x nije korijen stabla izvršava se petlja u kojoj se analizira
       uravnoteženost stabla. Ideja je da tražimo uzlaznom stazom,od čvora x, crveni čvor
       koji bi mogli pretvoriti u crni čvor. Na taj način bi se održala ravnoteža crnih čvorova
       (jer je prije izbrisan crni čvor y).

Slika 20. 8 pokazuje četiri slučaja kod kojih treba izvršiti promjenu boje i/ili izvršiti potrebne
rotacije u stablu, pri traženju tog crvenog čvora. Analiziraju se brat od x, to je čvor w, i roditelj
od čvora x.
Moguće je 8 slučajeva, 4 kad je brat desno od roditelja i 4 kad je brat lijevo od roditelja. Na slici
20.8 prikazana su 4 slučaja kada je brat desno dijete roditelja x. Slučajeve razlikujemo po boji
djece od w. Čvor w je zgodan za analizu jer ne može biti NIL, inače bi imali odstupanje od
svojstva 5 za čvor Parent(x) koji je i roditelj od w. (Ako je x crne boje, on mora imati brata, jer
staza od Parent(x) do lista, na lijevo i na desno mora imati bar jedan crni čvor koji nije NIL).




                                                                                                 349
b                                                                       d


      x       a                   d   w                                           b                            e
                                                      Slučaj 1   x
                      c                       e                      a           Novi w       c        E           F
          A       B
                  C           D       E           F              A           B        C                D




                      b   s                                                  Novi x           b        s

      x       a                   d       w                                       a                            d

                      c                       e       Slučaj 2                                    c                    e
          A       B                                                      A            B
                  C           D       E           F                                       C                D       E        F




                      b   s                                                                   b        s

                                      w                                                                            Novi w
      x       a                   d                                      x        a                            c
                                                      Slučaj 3
       A          B   c                       e                          A            B           C                    d

                  C           D       E           F                                                                D            e

                                                                                                                            E       F


                          s                                                                            s
                      b                                                                       d

      x       a                   d       w
                                                                                  b                            e
                                                      Slučaj 4                                    S'

       A          B   c   S'                  e                      a                        c        E           F
                  C           D       E           F              A           B        C                D           Novi x = Root(T)




Slika 20.8 Slučajevi koji se obrađuju u while-petlji funkcije deleteFixup().Crni čvorovi su
prikazani tamnije, crveni čvorovi s zadebljanim rubom,a ostali čvorovi, koji mogu biti ili crni
ili crveni prikazani su s tanjim rubom i označeni slovima s i s'. Slova A,B,C,.. označavaju
podstabla. Petlja se ponavlja jedino u drugom slučaju

      #ifdef RED_BLACK

      static void deleteFixup(BSTREE T, Node *x)
      {
         /* stvara Red-Black ravnotežu nakon brisanja čvora*/
         while (x != Root(T) && Color(x) == BLACK) {
            if (x == Left(Parent(x))) {
               Node *w = Right(Parent(x));
               if (Color(w) == RED) {          /*slučaj 1*/
                  Color(w) = BLACK;
                  Color(Parent(x)) = RED;
                  rotateLeft(T, Parent(x));
                  w = Right(Parent(x));
               }
               if (Color(Left(w)) == BLACK
                  && Color(Right(w)) == BLACK) {
                  Color(w) = RED;            /* slučaj 2*/




                                                                                                                                    350
x = Parent(x);
                 } else {
                    if (Color(Right(w)) == BLACK) {
                       Color(Left(w)) = BLACK;   /*slučaj 3*/
                       Color(w) = RED;
                       rotateRight(T, w);
                       w = Right(Parent(x));
                    }
                    Color(w) = Color(Parent(x)); /*slučaj 4*/
                    Color(Parent(x)) = BLACK;
                    Color(Right(w)) = BLACK;
                    rotateLeft(T, Parent(x));
                    x = Root(T);
                 }
             } else {/*slika u zrcalu (zamijeni lijevi <-> desni)*/
                 Node *w = Left(Parent(x));
                 if (Color(w) == RED) {
                     Color(w) = BLACK;
                     Color(Parent(x)) = RED;
                     rotateRight(T, Parent(x));
                     w = Left(Parent(x));
                 }
                 if (Color(Right(w)) == BLACK
                     && Color(Left(w)) == BLACK) {
                     Color(w) = RED;
                     x = Parent(x);
                 } else {
                     if (Color(Left(w)) == BLACK) {
                        Color(Right(w)) = BLACK;
                        Color(w) = RED;
                        rotateLeft(T, w);
                        w = Left(Parent(x));
                     }
                     Color(w) = Color(Parent(x));
                     Color(Parent(x)) = BLACK;
                     Color(Left(w)) = BLACK;
                     rotateRight(T, Parent(x));
                     x = Root(T);
                 }
              }
          }
          Color(x) = BLACK;
       }
       #endif

Slučaj 1: w je crven




   o    w mora imati crnu djecu (prema svojstvu 5)
   o    Postavi boju w crno i Parent(x) crveno (to kasnije može značiti kraj ispitivanja)
   o    Lijevo rotiraj Parent(x)




                                                                                            351
o   Novi brat od x je bio dijete od w prije rotacije, pa mora bit crn
    o   Nastavi razmatrati stanje prelaskom na slučajeve 2,3 ili4, u kojima će se, ovisno o
        položaju Novog w odrediti da li se Parent(x), koji je crven, pretvara u crni čvor.
Slučaj 2: w je crn i oba djeteta od w su crna




             [Napomena: Čvor nepoznate boje označen tankim rubom i slovom s]

    o   Promijeni boju od w u crveno
    o   Pomakni promatranje na viši čvor tako da Parent(x) postane Novi x
    o   Ponovi iteraciju. Iteracija prestaje ako je Novi x = Parent(x) crven (to je uvijek slučaj
        ako smo došli iz slučaja 1) Tada se Novi x konačno oboji u crno. Kraj!
    Primjetite da je zadržan broj crnih čvorova u stazi od x i u stazi od w. U obje staze najprije
    je smanjen broj crnih čvorova, a zatim je na kraju dodan crni čvor.
Slučaj 3: w je crn, lijevo dijete od w je crveno, a desno dijete od w je crno




    o   Pomijeni boju od w u crveno i boju lijevog djeteta od w u crno
    o   Desno rotiraj w
    o   Novi w, tj. novi brat od x je crn s crvenim desnim djetetom
    o   Proslijedi analizu na slučaj 4
Slučaj 4: w je crn, lijevo dijete od w je crno, a desno dijete od w je crveno




               [Napomena: Dva čvora nepoznate boje označeni su slovima s i s'.]

    o   Postavi boju w jednaku boji Parent(x)
    o   Postavi boju Parent(x) crno i boju desnog djeteta od w crno
    o   Zatim lijevo rotiraj Parent(x)
    o   Pošto je jedan crveni čvor pretvoren u crni (Right(w)), cilj je ispunjen. Završi petlju na
        način da postaviš da je x jednak Root(T). Kraj!

Vremensku složenost ovih operacija određuje složenost tri procesa:




                                                                                              352
1. Složenost bst_delete() je O(log2n), jer se traženje vrši u zravnoteženom stablu.
    2. Složenost deleteFixup() je određena slučajem 2 jer se jedino u njemu može javiti
       potreba za više iteracije. U svakoj iteraciji se analiza podiže jednu razina više, pa je
       maksimalno moguće O(log2n) iteracija
    3. Slučajevi 1, 3 i 4 imaju samo 1 rotaciju, pa ukupno ne može biti više od 3 rotacije.

Ukupna vremenska složenost je O(log2n) i to s malim konstantnim faktorom. Upravo brzina
operacije brisanja daje crveno-crnom stablu malu prednost nad drugim metodama uravnoteženja
stabla (primjerice, AVL metoda, u najgoram slučaju, kod brisanja vrši O(log2n) rotacija).

Zadatak: Napišite specijaliziranu verziju ADT binarnog stabla koja se od ADT BSTREE
razlikuje po tome da simboli nisu određeni parom <ključ, vrijednost>, već samo sadrže ključ.
Rezultirajući ADT nazovite imenom MULTISET, čime se aludira na skup podataka koji može
sadržavati više istih objekata. Definirajte sve operacije koje postoje kod BST, ali promijenite
imena operacija tako da sva imena počinju prefiksom mset_. Testirajte ADT MULTISET na
problemu sortiranja niza pomoću stabla traženja. Koristite algoritam za sortiranje stablom koji
se obično naziva Treesort, a sastoji se od sljedećih koraka.

Problem: Sotiraj niz A koji sadrži od n objekta tipa T, tako da vrijedi A[i] < A[i+1]
Algoritam:
        Inicijaliziraj ADT MULTISET imena Skup, za tip podataka T
        Ponavljaj za sve elemente niza A[i], i=0,1, n-1
             mset_insert(Skup, A[i])
        SYMBOL s ← mset_minimum(Skup);
        Ponavljaj za i=0,1, n-1
                 A[i] ← mset_symbol_key(s)
                 s ← mset_sucessor(s)
        Kraj!

Primjetite da umetanje jednog podatka u stablo traje O(log2n). Pošto ukupno postoji n podataka,
unos u stablo traje O(n log2n). Ispis iz stabla traje O(n). Zaključujemo da sortiranje stablom
(treesort) ima vremensku složenost O(n log2n) + O(n) = O(n log2n). Teorijski ovo je isti
rezultat kao kod Mergesort ili Heapsort metode, ili kao prosječni slučaj složenosti kod
Quicksort metode. U praksi, zbog manjeg konstantnog faktora, preferira se Quicksort metoda.




                                                                                           353
Literatura


[1] American National Standards Institute: American national standard for information
systems—Programming language - C, ANSI X3.159-1989, 1989.

[2] International Standard ISO/IEC 9899: Programming Language C, 1999.

[3] B. W. Kernighan and D. M. Ritchie: The C programming language (2nd ed.), Englewood
Cliffs, NJ: Prentice -Hall, 1988.

[4] D. M. Ritchie: The development of the C language. Second ACM HOPL Conference,
Cambridge, MA. 1993.

[5] R. Sedgewick: Algorithms in C, Addison-Wesley, 1998.

[6] M. A. Weiss: Data Structures and Algorithm Analysis in C, Addison-Wesley, 1997.

[7] T. H. Cormen, C. E. Leiserson and R. L. Rivest: Introduction to Algorithms, MIT Press,
1990.

[8] D. E. Knuth: Sorting and Searching, volume 3 of The Art of Computer Programming,
Addison-Wesley, 1973.

[9] A. V. Aho, J. E. Hopcroft and J. D. Ullman: Data Structures and Algorithms, Addison-
Wesley, 1983.

[10] A. V. Aho, J. E. Hopcroft and J. D. Ullman: Compilers, Addison-Wesley, 1985.

[11] E. Horowitz and S. Sahni: Fundamentals of Computer Algorithms, Computer Science
Press, 1978.

[12] N. Wirth: Algorithms and Data Structures, Prentice Hall, 1986.

[13] A. M. Berman: Data Structures via C++, Oxford Press, 1997.

[13] W. J. Collins: Data Structures and Standard Template Library, McGraw-Hill, 2003.




                                                                                        354
Dodatak



Dodatak A - Elementi dijagrama toka




                                      355
Dodatak B - Gramatika C jezika
Sintaktička i leksička pravila C-jezika su zapisana na sljedeći način:
      Neterminalni simboli su zapisani kurzivom.
      Terminalni simboli su zapisani na isti način kao u ciljnom jeziku
      Opcioni simboli su označeni indeksom opt (Simbolopt ili Simbolopt).
      Pravila imaju oblik:
                 neterminalni-simbol :
                         niz-terminalnih-i-neterminalnih-simbola
        Alternativna pravila su zapisana u odvojenim redovima. Iznimno, kada su zapisana u
        jednom retku, odvojena su okomitom crtom

 Sintaksa deklaracija i definicija C jezika
  kompilacijaska-jedinica:
      definicija-funkcije
      deklaracija
      kompilacijaska-jedinica deklaracija
      kompilacijaska-jedinica definicija-funkcije
 definicija-funkcije:
      deklaracija-specifikatoraopt deklarator lista-deklaracijaopt složena-naredba
 lista-deklaracija:
      deklaracija
      lista-deklaracija deklaracija
 deklaracija:
      deklaracija-specifikatora lista-init-deklaratoraopt;
 deklaracija-specifikatora:
      specifikacija-klase-spremanja deklaracija-specifikatoraopt
      specifikacija-tipa deklaracija-specifikatoraopt
      specifikacija-kvalifikacije deklaracija-specifikatoraopt
 specifikacija-klase-spremanja :
      auto | register | static | extern | typedef
 specifikacija-tipa:
     void | char | short | int | long
   float | double | signed | unsigned
     struct-ili-union-specifikator
     enum-specifikator
     typedef-ime
 specifikacija-kvalifikacije:
      const | volatile
 struct-ili-union-specifikator:
      struct-ili-union identifikatoropt { struct-lista-deklaracija }
      struct-ili-union identifikator
 struct-ili-union:
      struct | union
 struct-lista-deklaracija:
      struct-deklaracija
      struct-lista-deklaracija struct-deklaracija
 lista-init-deklaratora:
      init-deklarator
     lista-init-deklaratora, init-deklarator
 init-deklarator:
      deklarator
      deklarator = inicijalizacija
 struct-deklaracija:
      lista-specifikatora lista-struct-deklaratora;
 lista-specifikatora:
      specifikacija-tipa lista-specifikatoraopt




                                                                                      356
specifikacija-kvalifikacije lista-specifikatoraopt
 lista-struct-deklaratora:
    struct-deklarator
     lista-struct-deklaratora , struct-deklarator
struct-deklarator:
     deklarator
     deklaratoropt : konstanti-izraz
 enum-specifikator:
     enum identifikatoropt { enumerator-lista }
     enum identifikator
 enumerator-lista:
     enumerator
     enumerator-lista , enumerator
 enumerator:
     identifikator
     identifikator = konstanti-izraz
 deklarator:
     pokazivačopt direktni-deklarator
 direktni-deklarator:
     identifikator
     (deklarator)
     direktni-deklarator [ konstanti-izrazopt ]
     direktni-deklarator ( parametari-funkcije )
     direktni-deklarator ( lista-identifikatoraopt )
 pokazivač:
     * lista-specifikacija-kvalifikacijeopt
     * lista-specifikacija-kvalifikacijeopt pokazivač
lista-specifikacija-kvalifikacije:
     specifikacija-kvalifikacije
     lista-specifikacija-kvalifikacije specifikacija-kvalifikacije
 parametari-funkcije:
     lista-parametara
     lista-parametara , ...
 lista-parametara:
     deklaracija-parametra
     lista-parametara , deklaracija-parametra
 deklaracija-parametra:
     deklaracija-specifikatora deklarator
     deklaracija-specifikatora apstraktni-deklaratoropt
lista-identifikatora:
     identifikator
     lista-identifikatora , identifikator
inicijalizacija:
     izraz-pridjele
     { lista-inicijalizacije }
     { lista-inicijalizacije , }
lista-inicijalizacije:
     inicijalizacija
     lista-inicijalizacije , inicijalizacija
 ime-tipa:
     lista-specifikatora apstraktni-deklaratoropt
 apstraktni-deklarator:
     pokazivač
     pokazivačopt direktni-apstraktni-deklarator
 direktni-apstraktni-deklarator:
     ( apstraktni-deklarator )
     direktni-apstraktni-deklaratoropt [konstanti-izrazopt ]
     direktni-apstraktni-deklaratoropt ( parametari-funkcijeopt )
typedef-ime:
     identifikator




                                                                     357
Sintaksa naredbi
naredba:                                           naredba-iteracije:
       naredbeni izraz                                  while ( izraz ) naredba
       složena-naredba                                  do naredba while ( izraz ) ;
       naredba-selekcije                                for ( izrazopt ; izrazopt ; izrazopt ) naredba
       naredba-iteracije
       naredba-skoka                               naredba-selekcije:
       označena-naredba                                 if ( izraz ) naredba
naredbeni izraz :                                       if ( izraz ) naredba else naredba
     izrazopt ;                                         switch ( izraz ) naredba
složena-naredba :                                  naredba-skoka :
   { lista-deklaracijaopt niz-naredbiopt }              goto identifikator ;
lista-deklaracija :                                     continue;
       deklaracija                                      break;
       lista-deklaracija deklaracija                    return izrazopt ;
niz-naredbi:                                       označena-naredba:
        naredba                                        identifikator: naredba
       niz-naredbi naredba                              case konstanti-izraz : naredba
                                                        default : naredba
Sintaksa izraza
primarni-izraz:                                    relacijski-izraz:
          identifikator                                      posmačni-izraz
           konstanta                                         relacijski-izraz < posmačni-izraz
           literalni-string                                  relacijski-izraz > posmačni-izraz
           (izraz )                                          relacijski-izraz <= posmačni-izraz
postfiks-izraz:                                              relacijski-izraz >= posmačni-izraz
         primarni-izraz                            izraz-jednakosti:
         postfiks-izraz [ izraz ]                            relacijski-izraz
         postfiks-izraz ( lista-argumenata-opt )             izraz-jednakosti == relacijski-izraz
         postfiks-izraz . identifikator                      izraz-jednakosti != relacijski-izraz
         postfiks-izraz -> identifikator
                                                   AND-izraz:
         postfiks-izraz ++                                   izraz-jednakosti
         postfiks-izraz –                                    AND-izraz & izraz-jednakosti
        ( ime-tipa ) { lista-inicijalizacije }      XOR-izraz:
        ( ime-tipa ) { lista-inicijalizacije , }             AND-izraz
lista-argumenata:                                            XOR-izraz ^ AND-izraz
         izraz-pridjele                             OR-izraz:
         lista-argumenata , izraz-pridjele                   XOR-izraz
unarni-izraz:                                                OR-izraz | XOR-izraz
          postfiks-izraz
                                                   logički-AND-izraz:
          ++ unarni-izraz
                                                             OR-izraz
          -- unarni-izraz                                   logički-AND-izraz && OR-izraz
          unarni-operator cast-izraz               logički-OR-izraz:
         sizeof unarni-izraz                                logički-AND-izraz
          sizeof ( ime-tipa )                               logički-OR-izraz || logički-AND-izraz
unarni-operator:                                    uvjetni-izraz:
         & | * | + | - | ~ | !                              logički-OR-izraz
cast-izraz:                                                 logički-OR-izraz ? expr : uvjetni-izraz
          unarni-izraz                             izraz-pridjele:
         ( ime-tipa ) cast-izraz                             uvjetni-izraz
multiplikativni-izrazr:                                      unarni-izraz operator-pridjele izraz-pridjele
         cast-izraz                                 operator-pridjele:
         multiplikativni-izrazr * cast-izraz                = | *= | /= | %= | += | -=
         multiplikativni-izrazr / cast-izraz                | <<= | >>= | &= | ^= | |=
         multiplikativni-izrazr % cast-izraz       izraz:
aditivni-izraz:                                              izraz-pridjele
          multiplikativni-izraz                              izraz , izraz-pridjele
          aditivni-izraz + multiplikativni-izraz   konstantni-izraz:
          aditivni-izraz - multiplikativni-izraz             uvjetni-izraz
posmačni-izraz:
          aditivni-izraz
          posmačni-izraz << aditivni-izraz
          posmačni-izraz >> aditivni-izraz




                                                                                                         358
Regularna gramatika za zapis literalnih konstanti
     konstanta:
       integer-konstanta                            nenulta-znamenka:
       float-konstanta                                    1|2|3|4|5|6|7|8|9
       char-konstanta
       enum-konstanta                               znamenka :
       literalni-string                                0|nenulta-znamenka

     integer-konstanta:                             heksa-znamenka:
        0                                             znamenka
        decimalna-konstanta int-sufiksopt              |A|B|C|D|E|F|a|b|c|d|e|f
        oktalna-konstanta int-sufiksopt
        hexadecimalna-konstanta int-sufiksopt       oktalna-znamenka :
                                                       0|1|2|3|4|5|6|7
     decimalna-konstanta:
       nenulta-znamenka                             niz-znamenki:
       decimalna-konstanta znamenka                    znamenka
                                                       niz-znamenki znamenka
     oktalna-konstanta:
       0oktalna_znamenka
       oktalna-konstanta oktalna-namenka            char-konstanta:
                                                      'char'
     heksa-konstanta:                               L 'char'
       0xheksa-znamenka
       0Xheksa-znamenka                             char:
       heksa-konstanta heksa-znamenka                  reg-char
                                                       escape-sequence
     int-sufiks:
        unsigned-sufiks long-sufiksopt              reg-char:
        long-sufiks unsigned-sufiksopt              bilo koji ASCII znak osim apostrofa ('),
                                                    backslash() i znaka za novu liniju.
     unsigned-sufiks: u | U
     long-sufiks: l | L
                                                    escape-sekvenca:
     float-konstanta:                                  '        | "      |      |   d
       floating-konstanta float-sufiksopt           | dd        | ddd | xd       |   xdd
                                                    | xddd | a           | b     |   f
     float-sufiks: L | l | F | f                    | n         | r      | t     |   v
                                                               (d je znamenka)
     floating-konstanta:                            enum-konstanta:
        frakciona-konstanta exponent                  identifikator
        frakciona-konstanta
        niz-znamenki eksponent                      literalni_string:
                                                       ""
     frakciona-konstanta:                              "char-sekvenca"
        niz-znamenki.niz-znamenki                   L"char-sekvenca"
        .niz-znamenki
        niz-znamenki.                               char-sekvenca:
                                                      char
     eksponent:                                       char-sekvenca char
       e predznak niz-znamenki
       E predznak niz-znamenki
       e niz-znamenki                               Napomena:
       E niz-znamenki                               Znak navodnika(") se ne može direktno unositi
                                                    u string, već kao escape-sekvenca ".
     predznak:     +|-                              Prefiks L označava prošireni znakove (wchar_t)




                                                                                           359
Klučne riječi C - jezika
auto                   double            int                struct
break                  else              long               switch
case                   enum              register           typedef
char                   extern            return             union
const                  float             short              unsigned
continue               for               signed             void
default                goto              sizeof             volatile
do                     if                static             while




Tip operatora          Operator                                 Asocijativnost
primarni               [] . -> ()                               s lijeva na desno
postfiks –unarni       ++ --                                    s lijeva na desno
prefiks – unarni       ++ -- & * + - ~ ! sizeof cast            s desna na lijevo
multiplikativni        */%                                      s lijeva na desno
aditivni               +-                                       s lijeva na desno
posmačni               << >>                                    s lijeva na desno
relacijski             < > <= >=                                s lijeva na desno
jednakost              == !=                                    s lijeva na desno
bitznačajni "i"        &                                        s lijeva na desno
ekskluzivni "ili"      ^                                        s lijeva na desno
bitznačajni "ili"      |                                        s lijeva na desno
logički "i"            &&                                       s lijeva na desno
logički "ili"          ||                                       s lijeva na desno
ternarni uvjetni op.   ?:                                       s desna na lijevo
pridjela vrijednosti   = *= /= %= += -= <<= >>= &= ^= |=        s desna na lijevo
zarez                  ,                                        s lijeva na desno
                         Prioritet i asocijativnost operatora




                                                                                    360
Dodatak C - Standardna biblioteka C jezika

Standardna biblioteka C jezika definirana je dokumentom: American national standard for
information systems - Programming language C, ANSI X3.159-1989, American National
Standards Institute. 1989. Sadrži niz korisnih funkcija, konstanti (makro naredbi), typedef tipova
i globalnih varijabli (pr. stdin, stdout). Deklaracija tih objekata zapisana je u datotekama
koje se nazivaju zaglavlja ili h-datoteke (eng. headers), a koriste se kao početne #include
datoteke u gotovo svakom izvornom programu.


            Funkcionalna podjela                             h-datoteke (zaglavlja)
temeljne definicije                             <stddef.h>
rad s ulazno izlaznim uređajima i datotekama    <stdio.h>
radnje s znakovima i stringovima                <string.h>, <ctype.h>, <stdlib.h>, <stdio.h>
alociranje memorije                             <stdlib.h>
matematičke funkcije                            <math.h>
datum i vrijeme                                 <time.h>
promjenljivi broj argumenata                    <stdarg.h>
globalni skokovi                                <setjmp.h>
asertacija                                      <assert.h>
prihvat signala stanja računalnog procesa       <signal.h>
prihvat i dojava pogreške                       <errno.h>, <stdio.h>, <string.h>
minimalne i maksimalne vrijednosti tipova       <limits.h>, <float.h>
lokalizacija znakovnih zapisa                   <locale.h>, <stdlib.h>, <wchar.h>, <wctype.h>
sučelje s operativnim sustavom                  <stdlib.h>

Slijedi opis funkcija definiranih standardom. Za svaku funkciju navodi se deklaracija, opis
argumenata funkcije i tip vrijednosti koju funkcija vraća.

C 1 <stdio.h>
U zaglavlju <stdio.h>, definiran je niz funkcija za rad s ulazno/izlaznim tokovima. Svakom
toku je pridijeljen pokazivač na strukturu FILE koja je također definirana u <stdio.h>.
Standardni ulaz, standardni izlaz i standardni tok dojave greške se automatski inicijaliziraju pri
pokretanju programa, a njihov pokazivač na strukturu FILE je u globalnim varijablama:

        FILE *stdin;           /* pokazivač toka standardnog ulaza */
        FILE *stdout;          /* pokazivač toka standardnog izlaza */
        FILE *stderr;          /* pokazivač toka dojave greške */

Iniciranje pokazivača datotečnih tokova vrši se pomoću funkcije fopen(). Kada se završi rad s
datotekom treba zatvoriti njen tok pomoću funkcije fclose().

U zaglavlju <stdio.h> definirana je i simbolička konstanta EOF koja služi kao oznaka kraja
datoteke.
Otvaranje i zatvaranje toka
fopen
        FILE *fopen(const char *name, const char *mode);




                                                                                              361
fopen() otvara tok za datoteku imena name. Ako je operacija uspješna, funkcija vraća
pokazivač toka, a ako je neuspješna tada vraća NULL. String mode označava način na koji će
biti otvorena datoteka.
Prvi znak tog stringa mora biti 'r', 'w', ili 'a', što označava da se datoteka otvara za čitanje,
pisanje ili dopunjavanje. Ako se nakon prvog znaka napiše znak '+' tada se datoteka otvara za
čitanje i pisanje.
Dodatno se može napisati i znak 'b'. To je oznaka da se na MS-DOS sustavima datoteka tretira
kao binarna datoteka. U suprotnom datoteka se otvara u tekstualnom modu. MS-DOS /Windows
sustavi znak za kraj retka 'n' u tekstualnoj datoteci pretvaraju u dva znaka 'rn', također se
znak Ctrl-Z (ASCII vrijednost 26) tretira kao oznaka kraja datoteke. Ove pretvorbe ne postoje
na Unix sustavima.

freopen
      FILE *freopen(const char *name, const char *mode, FILE *fp);

freopen() služi kao i fopen(), ali na način da se datoteka imena name poveže s
postojećim pokazivačem toka fp. Tipično se koristi za redirekciju sa standardnih tokova,
primjerice, naredbom

      freopen("output.log", "w", stdout);

budući izlaz, koji generiraju funkcije koje djeluju na stdin (primjerice putchar i printf),
bit će preusmjeren u datoteku output.log.

fclose
      int fclose(FILE *fp);
Funkcija fclose() zatvara tok fp. U slučaju greške vraća EOF, inače vraća 0. Funkcija
fclose() se automatski primjenjuje na sve otvorene tokove pri normalnom završetku
programa.

fflush
      int fflush(FILE *fp);
Ako je otvoren tok fp, funkcijom fflush() svi se podaci iz privremenog datotečnog
spremnika zapišu u datoteku. Funkcija vraća 0, ili EOF ako nastupi greška.

Ulazno izlazne opracije
getchar, getc, fgetc
      int getchar();
      int getc(FILE *fp);
      int fgetc(FILE *fp);
Funkcija getc() vraća sljedeći dobavljivi znak iz toka fp, ili EOF ako je kraj datoteke. Obično
EOF ima vrijednost -1, kako bi se razlikovao od ostalih znakova. Zbog toga ova funkcija vraća
tip int, a ne tip char. Funkcija getchar() je ekvivalentna getc(stdin). Funkcija
fgetc() je ekvivalentna getc(), ali može biti implementirana kao makro naredba.

putchar, putc, fputc
      int putchar(int c);




                                                                                             362
int putc(int c, FILE *fp);
        int fputc(int c, FILE *fp);

Funkcija putc() upisuje znak c u tok fp. Funkcija putchar(c) je ekvivalentna putc(c,
stdout). Funkcija fputc() je ekvivalentna putc(),ali može biti implementirana kao makro
naredba. Sve tri funkcije vraćaju upisani znak, ili EOF u slučaju greške.

printf, fprintf
        int printf(const char *format, ...);
        int fprintf(FILE *fp, const char *format, ...);

Funkcija fprintf() upisuje tekstualno formatirane argumente u tok fp. Tri točkice
označavaju listu argumenata. Format se određuje u stringu format, koji sadrži znakove koji se
direktno upisuju na izlazni tok i specifikatore formatiranog ispisa argumenata.

       printf(format, …) je ekvivalentno fprintf(stdout, format, …).
Specifikator formata se sastoji od znaka %, iza kojeg slijede oznake za širinu i preciznost
ispisa te tip argumenta, u sljedećem obliku:

        %[prefiks][širina_ispisa][. preciznost][veličina_tipa]tip_argumenta

Specifikator formata mora započeti znakom % i završiti s oznakom tipa argumenta. Sva ostala
polja su opciona (zbog toga su napisana unutar uglatih zagrada).

U polje širina_ispisa zadaje se minimalni broj kolona predviđenih za ispis vrijednosti. Ako ispis
sadrži manji broj znakova od zadane širine ispisa, na prazna mjesta se ispisuje razmak. Ako
ispis sadrži veći broj znakova od zadane širine, ispis se proširuje. Ako se u ovo polje upiše
znak * to znači da će se broj kolona indirektno očitati iz slijedećeg argumenta funkcije, koji
mora biti tipa int.

Polje prefiks može sadržavati jedan znak koji ima sljedeće značenje:

 -     Ispis se poravnava prema lijevoj granici ispisa određenog poljem širina_ispisa. (inače se
       poravnava s desne strane) U prazna mjesta se upisuje razmak
+      Pozitivnim se vrijednostima ispisuje i '+' predznak.
razmak Ako je vrijednost positivna, dodaje se razmak prije ispisa (tako se može poravnati kolone s
       pozitivnim i negativnim brojevima).
0      Mjesta razmaka ispunjuju se znakom 0.
#      Alternativni stil formatiranja

Polje . preciznost određuje broj decimalnih znamenki iza decimalne točke kod ispisa realnog
broja ili minimalni broj znamenki ispisa cijelog broja ili maksimalni broj znakova koji se
ispisuje iz nekog stringa. Ovo polje mora započeti znakom točke, a iza nje se navodi broj ili
znak *, koji znači da će se preciznost očitati iz slijedećeg argumenta tipa int. Ukoliko se ovo
polje ne koristi, tada se podrazumijeva da će realni brojevi biti ispisani s maksimalno šest
decimalnih znamenki iza decimalne točke.

Polje tip_argumenta može sadržavati samo jedan od sljedećih znakova znakova:

c         Argument se tretira kao int koji se ispisuje kao znak iz ASCII skupa.
d, i      Argument se tretira kao int, a ispisuje se decimalnim znamenkama.
e, E      Argument je float ili double, a ispis je u eksponentnom formatu.
f         Argument je float ili double, a ispis je prostom decimalnom formatu. Ako je prefiks # i




                                                                                              363
preciznost .0, tada se ne ispisuje decimalna točka.
g, G    Argument je float ili double, a ispis je prostom decimalnom formatu ili u
        eksponencijalnom formatu, ovisno o tome koji daje precizniji ispis u istoj širini ispisa.
o       Argument je unsigned int, a ispisuje se oktalnim znamenkama.
p       Argument se tretira kao pokazivač tipa void *, pa se na ovaj način može ispisati adresa bilo
        koje varijable. Adresa se obično ispisuje kao heksadecimalni broj.
s       Argument mora biti literalni string odnosno pokazivač tipa char *.
u       Argument je unsigned int, a ispisuje se decimalnim znamenkama.
x, X    Argument je unsigned int, a ispisuje se heksadecimalnim znamenkama. Ako se zada
        prefiks # , ispred heksadecimalnih znamenki se ispisuje 0x ili 0X.
%       Ne odnosi se na argument, već znači ispis znaka %
n       Ništa se ne ispisuje, a odgovarajući argument mora biti pokazivač na int, u kojega se upisuje
        broj do tada ispisanih znakova

Polje veličina_tipa može sadržavati samo jedan znak koji se upisuje neposredno ispred oznake
tipa.

h       Pripadni argument tipa int tretira se kao short int ili unsigned short int.
l       Pripadni argument je long int ili unsigned long int.
L       Pripadni argument realnog tipa je long double.


Funkcije printf() i fprintf() vraćaju broj ispisanih znakova. Ako nastupi greška, vraćaju
negativan broj.

scanf, fscanf
       int scanf(const char *format, ...);
       int fscanf(FILE *fp, const char *format, ...);

Funkcijom fscanf() dobavljaju se vrijednosti iz tekstualnog toka fp po pravilima
pretvorbe koji su određeni u stringu format. Te vrijednosti se zapisuju na memorijske
lokacije određene listom pokazivačkih argumenata. Lista argumenata (...) je niz izraza
odvojenih zarezom, čija vrijednost predstavlja adresu postojećeg memorijskog objekta. Tip
objekta mora odgovarati specifikaciji tipa prethodno zapisanog u stringu format.

        scanf(format, …) je ekvivalentno fscanf(stdin, format, …).

String format se formira od proizvoljnih znakova i specifikatora formata oblika:

       %[prefiks][širina_ispisa][veličina_tipa]tip_argumenta

Ako se pored specifikatora formata navedu i proizvoljni znakovi tada se očekuje da i oni budu
prisutni u ulaznom tekstu (osim tzv. bijelih znakova: razmak, tab i nova linija) . Osim u slučaju
specifikatora %c, %n, %[, i %% , za sve pretvorbe se podrazumijeva da se iz ulaznog teksta
odstrane bijeli znakovi.

Jedini opcioni prefiks je znak '*' koji znači da se pretvorena vrijednost ne dodjeljuje ni
jednom argumentu.

U polju širina_ispisa zadaje se maksimalni broj kolona predviđenih za dobavu vrijednosti.




                                                                                                 364
Polje veličina_tipa može sadržavati samo jedan znak koji se upisuje neposredno ispred oznake
tipa pokazivačkog argumenta. To su sljedeći znakovi:

h       Pripadni argument tipa int tretira se kao short int ili unsigned short int.
l       Pripadni argument je long int ili unsigned long int.
L       Pripadni argument realnog tipa je long double.


Polje tip_argumenta može sadržavati samo jedan od sljedećih znakova:

c       Dobavlja se jedan znak, a argument je tipa char *. Ako je definirano polje širina_unosa, tada
        se unosi onoliko znakova kolika je vrijednost širina_unosa.
d       Dobavlja se cijeli broj, a argument je tipa int * ( ili short int * ili long int *, ako
        je zadana veličina tipa h ili l).
 i      Dobavlja se cijeli broj, a argument je tipa int * ( ili short int * ili long int *, ako
        je zadana veličina tipa h ili l). Pretvorba se vrši i ako je broj zapisan oktalno (počinje
        znamenkom 0) ili heksadecimalno (počinje s 0x ili 0X),
e, E, f,
        Dobavlja se realni broj. Argument je tipa float * ili double *.
g, G
o       Dobavlja se cijeli broj zapisan oktalnim znamenkama. Argument je tipa unsigned int* (ili
        unsigned short int * ili unsigned long int *, ako je zadana veličina tipa h ili
        l).
p       Dobavlja se pokazivač, a argument mora biti tipa void **.
s       Dobavlja se string koji ne sadrži bijele znakove. Argument mora biti tipa char *.
u       Dobavlja se cijeli broj, a argument je tipa unsigned int * ( ili unsigned short int
        * ili unsigned long int *, ako je zadana veličina tipa h ili l).
x, X     Dobavlja se cijeli broj zapisan heksadecimalno. Argument je tipa unsigned int*
        Argument je tipa unsigned int* (ili unsigned short int * ili unsigned long
        int *, ako je zadana veličina tipa h ili l).
%       Ne odnosi se na argument, već znači dobavu znaka %
n       Ne vrši se pretvorba. Odgovarajući argument mora biti tipa int *. Upisuje se broj do tada
        učitanih znakova
[       Za format oblika %[...] dobavlja se string koji sadrži znakove koji su unutar zagrada, prema
        sljedećem obrascu:
        %[abc] znači da se dobavlja string koji može sadržavati znakove a,b ili c.
        %[a-d] znači da se dobavlja string koji može sadržavati znakove a,b,c ili d.
        %[^abc] znači da se dobavlja string koji može sadržavati sve znakove osim a,b ili c.
        %[^a-d] znači da se dobavlja string koji može sadržavati znakove sve znakove osim a,b,c ili d.
        Argument je tipa char *.


Funkcije scanf()i fscanf() vraćaju broj uspješno izvršenih pretvorbi (bez %n i %*). Ako se
ne izvrši ni jedna konverzija, zbog dosegnutog kraja datoteke, vraća se vrijednost EOF.

gets, fgets
      char *fgets(char *buf, int n, FILE *fp);
      char *gets(char *buf);

Funkcije fgets() čita liniju teksta (koja završava znakom 'n') iz toka fp, i sprema taj
tekst (uključujući 'n' i zaključni znak '0') u znakovni niz buf. Veličina tog niza je n
znakova. Ako u liniji ima više od n-2 znakova, tada neće biti dobavljeni svi znakovi. Funkcije
gets() je ekvivalentna funkciji fgets(), ali isključivo služi za dobavu stringa s tipkovnice.




                                                                                                  365
Ona ne prenosi znak nove linije i ne vrši kontrolu broja unesenih znakova. Obje funkcije
vraćaju pokazivač na dobavljeni string ili NULL ako je greška ili kraj datoteke.

puts, fputs
     int puts(char *str);
     int fputs(char *str, FILE *fp);
Funkcija fputs() ispisuje string str u tok fp. Funkcija puts() ispisuje string str na
stdout i dodaje znak nove linije. Obje funkcije vraćaju pozitivnu vrijednost ili EOF ako
nastane greška.

fread
        size_t fread(void *buf, size_t elsize, size_t n, FILE *fp);
Funkcija fread() iz toka fp čita blok veličine n x elsize bajta i upisuje u spremnik buf.
Za dobavu n elemenata nekog tipa, elsize treba biti jednak sizeof(tip elementa). Kod
čitanja znakova je elsize = 1. Funkcija vraća broj dobavljenih elemenata ili EOF u slučaju
greške.

fwrite
        size_t fwrite(void *buf, size_t elsize, size_t n, FILE *fp);
Funkcija fwrite() upisuje u tok fp blok veličine n x elsize bajta iz spremnika buf. Za
upis n elemenata nekog tipa, elsize treba biti jednak sizeof(tip elementa). Kod upisa n
znakova elsize = 1. Funkcija vraća broj upisanih elemenata ili EOF u slučaju greške.

ungetc
        int ungetc(int c, FILE *fp);
Funkcija ungetc() umeće u tok fp znak c, tako da će pri sljedećem čitanju taj znak biti prvi
očitan. Ova operacija se može obaviti samo nakon operacije čitanja iz toka. Funkcija vraća
znak c ili EOF u slučaju greške.

Pozicioniranje toka
Mjesto na kojem se vrši čitanje/pisanje u tok naziva se trenutna pozicija toka (eng. stream
position). U radu s binarnim datotekama može se kontrolirati i postavljati trenutnu poziciju toka.

ftell
        long int ftell(FILE *fp);

Funkcija ftell() vraća long int vrijednost koja je jednaka trenutnoj poziciji toka fp (kod
binarnih datoteka to je pomak u bajtima od početka datoteke). Ako se radi s velikom
datotekama, u kojima pozicija može biti veće od long int, tada treba koristiti funkciju
fgetpos().

fseek, rewind
        int fseek(FILE *fp, long int pos, int from);
        void rewind(FILE *fp);




                                                                                              366
Funkcija fseek() postavlja trenutnu poziciju toka fp na poziciju pos, relativno prema
vrijednosti from argumenta, koji može imati tri vrijednosti: SEEK_SET (početak datoteke) ,
SEEK_CUR (trenutna pozicija) i SEEK_END (kraj datoteke). Argument pos može biti negativna
vrijednost. Funkcija vraća 0, ako je operacija uspješna.

        Ako je argument from jednak SEEK_SET,
                nova_pozicija = pos;

       Ako je argument from jednak SEEK_CUR,
             nova_pozicija = trenutna_pozicija + pos;

       Ako je argument from jednak SEEK_END
             nova_pozicija = pozicija_kraja_datoteke + pos;

Nova pozicija mora biti veća ili jednaka nuli, i može biti veća od trenutne pozicije.
Funkcija rewind(fp) postavlja poziciju na 0, što je ekvivalentno fseek(fp,0,SEEK_SET).
Ovu funkciju se može koristiti i s tekstualnim datotekama.

fgetpos, fsetpos
       int fgetpos(FILE *fp, fpos_t *pos);
       int fsetpos(FILE *fp, const fpos_t *pos);

Funkcija fgetpos() zapisuje trenutnu poziciju toka fp u fpos_t objekt na kojega pokazuje
pos. Funkcija fsetpos() postavlja trenutnu poziciju toka fp na vrijednost fpos_t objekta na
kojeg pokazuje pos (to mora biti vrijednost prethodno dobivena funkcijom fgetpos). Obje
funkcije vraćaju 0 ako je operacija uspješna.

Kontrola ulazno/izlaznog spremnika
setbuf, setvbuf
       void setbuf(FILE *fp, char *buf);
       void setvbuf(FILE *fp, char *buf, int mode, size_t size);

Pomoću ovih funkcija postavlja se korisnički definirani spremnik buf kao spremnik
ulazno/izlaznih operacija. Primjenjuju se prije poziva ulazno/izlaznih operacija na otvoreni tok
fp. Kod funkcije setbuf() veličina spremnika mora biti jednaka BUFSIZE (definiran u
stdio.h). Ako je buf==NULL tada se ne koristi spremnik. Kod funkcije setvbuf() veličina
spremnika se postavlja argumentom size, a način korištenja spremnika se postavlja
argumentom mode, koji može imati tri predefinirane vrijednosti:

_IONBF         Ne koristi se spremnik, već se vrši neposredan pristup datoteci.
_IOFBF         Spremnik se koristi potpuno, tj. spremnik se prazni tek kada je popunjen.
_IOLBF         Spremnik se koristi djelomično (uvijek se prazni kada se ispisuje znak nove linije 'n' ).


Dojava i prihvat greške
feof
       int feof(FILE *fp);

Funkcija feof() vraća nenultu vrijednost ako je tok u poziciji kraj datoteke, inače vraća 0.




                                                                                                       367
ferror
      int ferror(FILE *fp);

Funkcija ferror() vraća nenultu vrijednost ako je nastala greška u radu s tokom, inače vraća
0.

clearerr
      void clearerr(FILE *fp);

Funkcija clearerr() briše indikatore greške ili kraj datoteke za tok fp.

perror
      void perror(const char *prefix);

Funkcija perror() ispisuje poruku na stderr o trenutno nastaloj greški. Tip greške se bilježi
u globalnoj varijabli errno. Poruka je ista kao i poruka koja se dobije pozivom funkcije
strerror(errno). Drugim riječima, perror(p) je otprilike ekvivalentna pozivu

      fprintf(stderr, "%s: %s", p == NULL ? "" : p, strerror(errno));

Argument prefix je proizvoljni string koji se ispisuje ispred poruke.

Operacije s formatiranim stringovima
sprintf, sscanf
      int sprintf(char *buf, const char *format, ...);
      int sscanf(const char *buf, const char *format, ...);
Funkcije sprintf() i sscanf() su varijante od printf() i scanf() koje umjesto
ulazno/izlaznog toka koriste proizvoljno odabrani string buf. Znak kraja stringa '0' se tretira
kao znak za kraj datoteke. Korisnik mora voditi računa o tome da veličina stringa buf bude
dovoljno velika, da se može izvršiti sve pretvorbe formata u sprintf() funkciji.

Operacije s promjenjljivom listom argumenata
vprintf, vfprintf, vsprintf
      int vprintf(const char *format, va_list argp);
      int vfprintf(FILE *fp, const char *format, va_list argp);
      int vsprintf(char *buf, const char *format, va_list argp);

Ove funkcije omogućuju definiranje funkcija s promjenljivim brojem argumenata, koje imaju
funkcionalnost kao printf(), fprintf() i sprintf() funkcije.
Posljednji argument ovih funkcija argp je pokazivač tipa va_list. To omogućuje rad s
promjenljivim brojem argumenata. U sljedećem programu definirana je funkcija za dojavu
greške, koju se može koristiti za dojavu greške pri analizi izvornog programa. Ona koristi dvije
globalne varijable koje sadrže ime datoteke (filename) i broj linije izvornog programa
(lineno). U dojavi greške se uvijek prije izvještaja o tipu greške ispisuje to ime i broj linije.


      #include <stdio.h>




                                                                                             368
#include <stdarg.h>
      extern char *filename;              /* current input file name   */
      extern int lineno;                  /* current input line number */

      void error(char *msg,...)
      {
             va_list argp;
             va_start(argp, msg);
             fprintf(stderr, "%s:, line %d: ",filename, lineno);
             vfprintf(stderr, msg, argp);
             fprintf(stderr, "n");
             va_end(argp);
      }


Manipuliranje s datotekama
rename
      int rename(const char *origname, const char *newname);

Funkcija rename() vrši promjenu imena datoteke origname,               u ime newname. Ako je
operacija uspješna funkcija vraća 0, inaće vraća nenultu vrijednost.

remove
      int remove(const char *name);

Funkcija remove() briše datoteku imena name. Ako je operacija uspješna funkcija vraća 0,
inaće vraća nenultu vrijednost.

tmpfile, tmpnam
      FILE *tmpfile(void);
      char *tmpnam(char *buf);

Funkcija tmpfile() stvara privremenu datoteku i otvara je u "wb+" modu. Po izlasku iz
programa ova se datoteka automatski briše.
Funkcija tmpnam() generira jedinstveno ime u string buf, koji mora biti duljine L_tmpnam
(predefinirana konstanta). To se ime može koristiti za stvaranje datoteke. Ako je buf==0, ime
se generira u internom statičkom spremniku. Funkcija vraća pokazivač na taj spremnik.

C 2 <string.h>
Funkcije koje su deklarirane u <string.h> uglavnom služe za rad s ASCIIZ stringovima.
Pored njih, definirano je nekoliko funkcija, čije ime počinje s mem, za rad s memorijskim
blokovima (nizovima bajta).

Rad sa stringovima

      size_t strlen(const char *s)

Vraća duljinu stringa s.

      char *strcpy(char *s, const char *t)

Kopira string t u string s, uključujući '0'; vraća s.




                                                                                         369
char *strncpy(char *s, const char *t, size_t n)

Kopira najviše n znakova stringa t u s; vraća s. Dopunja string s sa znakovima '0', ako
t ima manje od n znakova. Napomena: ako u stringu t ima n ili više znakova, tada string s
neće biti zaključen s '0'.

      char *strcat(char *s, const char *t)

Dodaje string t na kraj stringa s; vraća s.

      char *strncat(char *s, const char *t, size_t n)

Dodaje najviše n znakova stringa t na string s, i znak '0'; vraća s.

      int strcmp(const char *s, const char *t)

Uspoređuje string s sa stringom t, vraća <0 ako je s<t, 0 ako je s==t, ili >0 ako je s>t.
Usporedba je leksikografska, prema ASCII rasporedu.

      int strncmp(const char *s, const char *t, size_t n)

Uspoređuje najviše n znakova stringa s sa stringom t; vraća <0 ako je s<t, 0 ako je s==t, ili
>0 ako je s>t.

      int strcoll(const char *s, const char *t);

Uspoređuje dva stringa s1 and s2, poput strcmp(), ali se usporedba vrši prema
multinacionalnom znakovnom rasporedu (koji je određen konstantom LC_COLLATE ). Vraća
<0 ako je s<t, 0 ako je s==t, ili >0 ako je s>t.

      size_t strxfrm(char *s, const char *t, size_t n);

Stvara modificiranu kopiju n znakova stringa t u stringu s (uključujući '0') , tako da
strcmp(s,t) daje istu ocjenu kao i strcoll(s,n) na originalnom stringu. Vraća broj
znakova u stringu s.

      char *strchr(const char *s, int c)

Vraća pokazivač na prvu pojavnost znaka c u stringu s, ili NULL znak ako c nije sadržan u
stringu s.

      char *strrchr(const char *s, int c)

Vraća pokazivač na zadnju pojavnost znaka c u stringu s, ili NULL znak ako c nije sadržan u
stringu s.

      char *strstr(const char *s, const char *t)

Vraća pokazivač na prvu pojavu stringa t u stringu s, ili NULL ako string s ne sadrži string t.

      size_t strspn(const char *s, const char *t)

Vraća duljinu prefiksa stringa s koji sadrži znakove koji čine string t.

      size_t strcspn(const char *s, const char *t)

Vraća duljinu prefiksa stringa s koji sadrži znakove koji nisu prisutni u stringu t.




                                                                                              370
char *strpbrk(const char *s, const char *t)

Vraća pokazivač na prvu pojavu bilo kojeg znaka iz string t u stringu s, ili NULL ako nije
prisutan ni jedan znak iz string t u stringu s.

      char *strerror(int n)

Vraća pokazivač na string koji se interno generira, a služi za dojavu greške u nekim sistemskim
operacijama. Argument je obično globalna varijabla errno, čiju vrijednost određuje izvršenje
funkcija iz standardne biblioteke.

      char *strtok(char *s, const char *sep)

strtok() je funkcija     kojom se može izvršiti razlaganje stringa na niz leksema koji su
razdvojeni znakovima-separatorima. Skup znakova-separatora se zadaje u stringu sep. Funkcija
vraća pokazivač na leksem ili NULL ako nema leksema.

Korištenje funkcije strtok() je specifično jer u stringu može biti više leksema, a ona vraća
pokazivač na jedan leksem. Da bi se dobili slijedeći leksemi treba iznova pozvati ovu funkciju,
ali s prvim argumentom jednakim NULL. Primjerice, za string

      char *s = "Prvi         drugi,treci";

ako odaberemo znakove separatore: razmak, tab i zarez, tada sljedeći iskazi daju ispis tri
leksema (Prvi drugi i treci):


      char *leksem = strtoken(s, " ,t");             /*   dobavi prvi leksem */
      while( leksem != NULL) {                        /*   ukoliko postoji */
          printf("", leksem);                         /*   ispiši ga i */
          lexem = strtok(NULL," ,t");                /*   dobavi sljedeći leksem */
      }                                               /*   pa ponovi postupak */



Operacije s memorijskim blokovima (nizovima)
memcpy, memmove
      void *memcpy(void *dest, const void *src, size_t n);
      void *memmove(void *dest, const void *src, size_t n);

Ove funkcije kopiraju točno n bajta s lokacije na koju pokazuje src na lokaciju koju pokazuje
dest. Ukoliko se blokovi preklapaju tada treba koristiti funkciju memmove(). Funkcije vraćaju
pokazivač dest.

memcmp
      int memcmp(const void *p1, const void *p2, size_t n);

Uspoređuje točno n znakova s lokacija na koje pokazuju p1 i p2, na isti način kao strcnmp(),
ali se usporedba ne prekida ako je dostignut znak '0'.

memchr
      void *memchr(const void *p, int c, size_t n);




                                                                                           371
Traži prvu pojavu znaka c u n znakova bloka na koji pokazuje p. Vraća pokazivač na
pronađeni znak ili NULL ako znak nije pronađen.

memset
      void *memset(void *p, int c, size_t n);

Postavlja n bajta bloka na koji pokazuje p na vrijednost znaka c, i vraća p.

C 3 <ctype.h>
Funkcije iz <ctype.h> omogućuju klasifikaciju znakova te pretvorbu velikih u mala slova i
obratno.

Klasifikacija znakova

      int isupper(int c);

vraća vrijednost različitu od nule ako je znak c veliko slovo, inače vraća 0.

      int islower(int c);

vraća vrijednost različitu od nule ako je znak c malo slovo, inače vraća 0.

      int isalpha(int c);

vraća vrijednost različitu od nule ako je znak c veliko ili malo slovo, inače vraća 0.

      int iscntrl(int c);

vraća vrijednost različitu od nule ako je znak c kontrolni znak, inače vraća 0.

      int isalnum(int c);

vraća vrijednost različitu od nule ako je znak c slovo ili znamenka, inače vraća 0.

      int isdigit(int c);

vraća vrijednost različitu od nule ako je znak c decimalna znamenka, inače vraća 0.

      int isxdigit(int c);

vraća vrijednost različitu od nule ako je znak c heksadecimalna znamanka, inače vraća 0.

      int isgraph(int c);

vraća vrijednost različitu od nule ako je znak c tiskani znak osim razmaka, inače vraća 0.

      int isprint(int c);

vraća vrijednost različitu od nule ako je znak c tiskani znak uključujući razmak, inače vraća 0.

      int ispunct(int c);

vraća vrijednost različitu od nule ako je znak c tiskani znak osim razmaka, slova ili znamanke,
inače vraća 0.




                                                                                             372
int isspace(int c);

vraća vrijednost različitu od nule ako je znak c razmak, tab, vert. tab, nova linija, povrat ili nova
stranica, inače vraća 0.
Pretvorba znaka

       int toupper(int c);
       int tolower(int c);

Funkcija toupper() pretvara malo slovo u ekvivalentno veliko slovo, ostala slova ostaju
nepromijenjena. Slično, tolower() pretvara veliko slovo u ekvivalentno malo slovo.

C 4 <stdlib.h>
U zaglavlju <stdlib.h> definirano je nekoliko temeljnih funkcija za alokaciju memorije,
pretvorbu stringa u brojeve, manipuliranje s multibajtnim znakovnim skupom, itd.

Alokacija memorije
malloc, calloc
       void *malloc(size_t n);
       void *calloc(size_t n, size_t elsize);

Funkcija malloc() alocira se n bajta slobodne memorije. Ako je alociranje uspješno funkcija
vraća pokazivač na tu memoriju, u suprotnom vraća NULL pokazivač. Primjerice, naredbom

       double *dp = malloc(10 * sizeof(double));

dobije se pokazivač dp, koji pokazuje na niz od 10 elemenata tipa double.
Funkcija calloc(n, elsize) je ekvivalentna malloc(n * elsize), uz dodatni uvjet da
calloc() inicijalizira sve bitove alocirane memorije na vrijednost nula.

free
       void free(void *p);

Funkcija free() prima kao argument pokazivač p. Uz pretpostavku da p pokazuje na
memoriju koja je prethodno alocirana funkcijom malloc(), calloc() ili realloc(), ova
funkcija dealocira tu memoriju.

realloc
       void *realloc(void *oldptr, size_t newsize);

Funkcija realloc() vrši promjenu veličine prethodno alocirane memorije, koja je
pridijeljena pokazivaču ptr, na veličinu newsize. Funkcija realloc() vraća pokazivač na
tu memoriju. Vrijednost toga pokazivača može biti ista kao i vrijednost od ptr, ako memorijski
alokator može prilagoditi veličinu zahtijevanog području slobodne memorije veličini newsize.
Ukoliko se to ne može ostvariti funkcija realloc() alocira novo područje memorije pa u
njega kopira i zatim oslobađa dio memorije na koju pokazuje ptr. Ukoliko se ne može izvršiti
alokacija memorije funkcija realloc() vraća NULL.
Napomena: poziv realloc(p, 0) je ekvivalentan pozivu free(p), a poziv realloc(0,
n) je ekvivalentan pozivu malloc(n).




                                                                                                 373
Pretvorba stringa u numeričku vrijednost
atoi, atol, strtol, strtoul
        int atoi(const char *s);
        long int atol(const char *s);
        long int strtol(const char *s, char **endp, int baza);
        unsigned long int strtoul(const char *s, char **endp, int baza);

Ove funkcije pretvaraju numerički string s u odgovarajuću numeričku vrijednost.
Funkcija strtol() u stringu s preskače bijele znakove, pretvara s u broj i vraća vrijednost
tipa long int. Ona omogućuje pretvorbu iz sustava različite baze. Ako je baza 10 tada se iz
ulaznog stringa s prihvaćaju znakovi od 0 do 10, a ako je baza 16 prihvaćaju se i znakovi a-f, A-
F. Ako je baza manja od 10 prihvaćaju se znakovi od 0 do baza-1. Ako je baza 0, tada se koristi
pravilo da oktalni brojevi počinju s nulom, a heksadecimalni s 0x ili 0X.

        atoi(s) je ekvivalntno strtol(s,NULL,0);

Argument endp ja pokazivač na pokazivač na char. Njega se koristi za tako da ako endp !=
NULL, tada strtol() sprema u *endp pokazivač na prvi znak koji nije obrađen. Ako je to
znak '0', pretvorba je uspješna. Ako je jednak s, pretvorba uopće nije izvršena. Funkcija
vraća vrijednost koja je pretvoreni broj ili 0 ako pretvorba nije izvršena ili konstanta LONG_MAX
ili LONG_MIN ako se broj ne može predstaviti kao long int. U slučaju prekoračenja postavlja
se errno = ERANGE.
Funkcija strtoul() je slična strtol() osim što vraća tip unsigned long int, ili
vrijednost ULONG_MAX kod prekoračenja.
Poziv atol(s) je ekvivalentan pozivu strtoul(s,NULL,0).

atof, strtod
        double atof(const char *s);
        double strtod(const char *s, char **endp);

Ove funkcije pretvaraju numerički string s u odgovarajuću numeričku vrijednost realnog broja.
Funkcija strtod() u stringu s preskače bijele znakove, pretvara s u broj i vraća vrijednost
tipa double. Prihvaća prosti i eksponentni zapis realnog broja.
Argument endp ja pokazivač na pokazivač na char. Njega se koristi za tako da ako endp !=
NULL, tada strtod() sprema u *endp pokazivač na prvi znak koji nije obrađen. Ako je to
znak '0', pretvorba je uspješna. U slučaju prekoračenja vraća konstantu HUGE_VAL,
i postavlja globalnu varijablu errno = ERANGE.
Napomena: atof(s) je ekvivalentno strtod(s, NULL), osim što rezultat nije definiran u
slučaju prekoračenja.

Generator slučajnih brojeva
rand
        int rand(void);

Funkcija rand() vraća slučajni cijeli broj iz intervala      0 do RAND_MAX ( RAND_MAX je
konstanta definirana u <stdlib.h>).

srand




                                                                                             374
void srand(unsigned int seed);

Funkcija srand() postavlja početnu vrijednost seed generatora slučajnih brojeva.

Sortiranje i traženje
qsort
         void qsort(void *a, size_t n, size_t elsize, int (*cmpfunc)());

Funkcija qsort() sortira niz a, koji ima n elemenata veličine elsize (u bajtima), prema
kriteriju koji je određen funkcijom na koju pokazuje cmpfunc. Ta funkcija mora biti
deklarirana u obliku:

         int name(const void *p1, const void *p2);

i mora vratiti cijeli broj koji je manji, veći ili jedanak nuli, ovosno o tome da li je objekt na
kojeg pokazuje p1 manji, veći, ili jednak objektu na kojeg pokazuje p2.

bsearch
         bsearch(const void *pObj, const void *a, size_t n, size_t elsize,
                              int (*cmpfunc)());

Funkcija bsearch() vrši binarno traženje u sortiranom nizu a, koji ima n elemenata veličine
elsize (u bajtima), tražeći element koji je jednak objektu na kojeg pokazuje pObj.
Pri traženju se za usporedbu koristi funkcija na koju pokazuje cmpfunc. Deklaracija te funkcije
je ista kao kod qsort().

Interakcija s operativnim sustavom
getenv
         char *getenv(const char *name);

"Environment" sadrži postavke operativnog sustava u sistemskim varijablama (pr. path).
Funkcija getenv() traži "environment varijablu" imena name i vraća njenu vrijednost u obliku
stringa. Ako ne može pronaći tu varijablu, tada vraća NULL.

atexit
         int atexit(void (*func)(void));

Funkcija atexit() prima kao argument pokazivač na funkciju func, koja će biti pozvana pri
normalnim završetku programa. Vraća 0 ako je funkcija uspješno registrirana. Može se
registrirati do 32 funkcije, koje će biti pozvane u obrnutom redoslijedu od reda registriranja.

exit
         void exit(int status);

Funkcija exit() vrši normalni završetak programa, poziva sve funkcije koje su registrirane s
atexit() funkcijom i zatvara sve tokove. Argument status se prosljeđuje operativnom
sustavu. Mogu se koristiti i dvije simboličke konstante EXIT_SUCCESS i EXIT_FAILURE. Po
dogovoru, status=0 (EXIT_SUCCESS) znači uspješan završetak programa.




                                                                                             375
system
        int system(const char *s);

Funkcija system() prima kao argument string koji sadrži komandu operativnog sustava.
Funkcija vraća vrijednost koju vrati operativni sustav po završetku procesa.

abort
        void abort(void);

Funkcija abort() predstavlja zahtjev za neposrednim prekidom programa, na isti način kao da
je izvršen poziv raise(SIGABRT).

Cjelobrojne aritmetičke funkcije
abs, labs
        int abs(int x);
        long int abs(long int x);

Obje funkcije vraćaju apsolutnu vrijednost argumenta x..

div, ldiv
        div_t div(int num, int denom);
        ldiv_t div(long int num, long int denom);

Ove funkcije vrše dijeljenje num/denum na način da se istovremeno dobije rezultat dijeljenja i
ostatak cjelobrojnog dijeljenja. Rezultat se vraća u vrijednost koja je tipa sljedeće strukture:

        typedef struct
        {    int quot;       /* rezultat dijeljenja */
             int rem;        /* ostatak dijeljenja */
        }div_t;


Multibajt znakovne sekvence i stringovi
Za zapis znakova po ASCII standardu koristi se tip char. Za zapis znakova se također može
koristiti prošireni znakovni tip wchar_t, koji podržava 16-bitni Unicode standard, i multibajt
znakovne sekvence MBCS (za jezike poput japanskog).

mblen
        int mblen(const char *s, size_t n);

Funkcija mblen() vraća broj znakova u stringu koji sadrži multibajt znakovne sekvence.
Analizira se maksimalno n znakova.

mbtowc, wctomb
        int mbtowc(wchar_t dest, const char *src, size_t n);
        int wctomb(char *dest, wchar_t src);




                                                                                            376
Ove funkcije pretvaraju multibajt znakovnu sekvencu u tip wchar_t i obrnuto. Funkcija
mbtowc() analizira maksimalno n bajta iz stringa src i pretvara ih u wchar_t i sprema u
dest, te vraća broj bajta ili -1 ako pretvorba nije uspješna.
Funkcija wctomb() pretvara prošireni znak src u multibajt sekvencu dest, i vraća broj bajta u
toj sekvenci.. (Ovaj broj neće nikada biti veći od konstante MB_CUR_MAX, definirane u
<stdlib.h>.)

mbstowcs, wcstombs
      size_t mbstowcs(wchar_t *dest, const char *src, size_t n);
      size_t wcstombs(char *dest, wchar_t src);

Ove funkcije vrše pretvorbu višestruke multibajt sequence i niza proširenih znakova. Funkcija
mbtowcs() pretvara multibajt sekvencu src i u niz proširenih znakova i sprema u dest, ali
maksimalno n znakova. Vraća broj pretvorenih znakova. Funkcija wcstombs() vrši obrnutu
radnju.

C 5 <math.h>
Standardne matematičke funkcije su deklarirane u zaglavlju <math.h>. Njihov opis je dan
sljedećoj tablici:

Funkcija                                 Vraća vrijednost
double     sin(double x);                sin(x) , sinus kuta x u radijanima
double     cos(double x);                cos(x) , kosinus kuta x u radijanima
double     tan(double x);                tg(x) , tangens kuta x u radijanima
double     asin(double x);               arcsin(x), vraća vrijednost [-π/2, π/2], za x ∈ [-1,1].
double     acos(double x);               arccos(x), vraća vrijednost [0, π], za x ∈ [-1,1].
double     atan(double x);               arctg(x), vraća vrijednost [-π/2, π/2].
double     atan2(double y,               arctan(y / x), vraća vrijednost [-π,π].
                 double x);
double     sinh(double x);               sh(x), sinus hiperbolni kuta x
double     cosh(double x);               ch(x) , kosinus hiperbolni kuta x
double     tanh(double x);               th(x) , tangens hiperbolni kuta x
double     exp(double x);                ex , x potencija broja e = 2,781
double     log(double x);                prirodni logaritam ln(x), x>0.
double     log10(double x);              logaritam baze 10, log10(x), x>0.
double     pow(double x,                 xy, potenciranje x s eksponentom y. Nastaje greška ako je
               double y);               x=0 i y <= 0, ili ako je x<0, a y nije cijeli broj.
double     sqrt(double x);              √x, za x>=0, dugi korijen pozitivnog broja
double     ceil(double x);               najmanji cijeli broj (tipa double) koji nije manji od x
double     floor(double x);              najveći cijeli broj (tipa double) koji nije veći od x
double     fabs(double x);               |x|, apsolutna vrijednost od x
double     ldexp(double x,int n);        x*2n
double     frexp(double x,              rastavlja x na frakcioni dio i eksponent broja 2. Vraća
                 int *exp);             normaliziranu frakciju od x (iz intervala [1/2,1)] i
                                        exponent od 2 u *exp. Ako je x jednak nuli, oba dijela su
                                        jednaka nuli. Primjerice, frexp(2.5, &i) vraća 0.625
                                        (odnosno 0.101 baze 2) i postavlja i na 2, tako da
                                        ldexp(0.625, 2) ponovo daje vrijednost 2.5.
double modf(double x,                   rastavlja x na integralni i frakcioni dio, tako da oba imaju
            double *ip);                predznak od x. Vraća frakcioni dio, a integralni dio smješta
                                        u *ip.
double fmod(double x,                    ostatak dijeljenja realnih brojeva x/y, s predznakom od x.
            double y);




                                                                                                   377
C 6 <time.h>
U zaglavlju <time.h> definirane su funkcije i strukture za rad s datumima i vremenom.
Posebni typedef tip time_t, služi za bilježenje vremena. Obično se uzima da ovaj tip
predstavlja broj sekundi počevši od 01.01.1970. godine. Za detaljniji zapis vremena i datuma
koristi se struktura tm, koja je definirana na sljedeći način:


       struct tm                       /*   opisuje vrijeme i datum */
       {      int    tm_sec;           /*   sekunde           0..61 */
              int    tm_min;           /*   minute            0..59 */
              int    tm_hour;          /*   sat               0..23 */
              int    tm_mday;          /*   dan               1..31 */
              int    tm_mon;           /*   mjesec            0..11 */
              int    tm_year;          /*   broj godina nakon 1900 */
              int    tm_wday;          /*   dan u sedmici      0..6 */
              int    tm_yday;          /*   dan u godini     0..365 */
              int    tm_isdst;         /*   da li je dan promjene sata 0..1 */
       };

Napomena: ako dan promjene sata nije implementiran, tada tm_isdst ima negativnu
vrijednost.
Broj sekundi može biti veći od 59 u slučaju prestupnog vremena. Mjeseci su kodiranu tako da 0
označava siječanj, 1 veljaču itd. Dani u sedmici su kodirani tako da 0 označava nedjelju, 1
ponedjeljak itd. Stvarna godina se dobije tako da se članu tm_year doda vrijednost 1900
(primjerice u godini 2002. godini član tm_year sadrži vrijednost 102).

time

       time_t time(time_t *tp);

Funkcija time() vraća time_t vrijednost – kardinalni broj koji predstavlja trenutno vrijeme
(obično je to broj sekundi od 1.1.1970.). Parametar tp, ako nije NULL, također prihvaća
trenutno vrijeme u *tp.

localtime, gmtime

       struct tm *localtime(const time_t *t);
       struct tm *gmtime(const time_t *t);

Pretvorbu vremena iz formata time_t u struct tm vrši se funkcijom localtime(), kada se
želi dobiti lokalno vrijeme, ili funkcijom gmtime() za univerzalno vrijeme u nultom
meridijanu.
Obje funkcije primaju adresu varijable koja sadrži vrijeme u formatu time_t, a vraćaju
pokazivač na statičku strukturu tipa tm (sadržaj se obnavlja pri svakom pozivu ovih funkcija) .

ctime, asctime
Ako se želi dobiti zapis vremena u obliku stringa, mogu se koristiti funkcije

       char *ctime(const time_t *t);
       char *asctime(const struct tm *tp);

Funkcija ctime() za argument koristi adresu varijable koja sadrži vrijeme u formatu time_t,
a funkcija asctime() za argument koristi pokazivač na strukturu tm. Obje funkcije vraćaju




                                                                                           378
pokazivač statičkog stringa koji sadrži zapis vremena u standardnom formatu. Primjerice,
sekvenca naredbi

      time_t t = time(NULL);
      char *s = ctime(&t);
      puts(s);

generira ispis:

      Sat May 11 14:21:20 2002

Uočite da je rezultat poziva ctime(&t) ekvivalentan pozivu asctime(localtime(&t)) .
Standardna verzija je prilagođena američkim standardima.

Ako se želi napisati vrijeme u formatu

      11.05.2002        14:21

tada se može koristiti sljedeće iskaze:

      /* ispisuje datum i vrijeme u formatu                      11.05.2002 14:21 */
      time_t t = time(NULL);
      struct tm *p = localtime(&t);
      printf("%.2d.%.2d.%.2d %2d:%.2dn",
             p->tm_mday, p->tm_mon + 1,
             p->tm_year +1900,
             p->tm_hour, p->tm_min);


strftime

      size_t strftime(char *buf, size_t bufsize,
                            const char *fmt, const struct tm *tp);

Funkcija strftime() se koristi za formatirani ispis vremena. Format se zadaje kao kod
printf() funkcije. Prvi argument je string str u koji se vrši formatirani zapis. Drugi
argument (bufsize) ograničava broj znakova stringa. Treći parametar je string u kojem se
zapisuje format ispisa nizom specifikatora oblika %x (kao kod printf() funkcije). Posljednji
argument je pokazivač strukture tm. Funkcija vraća broj znakova u stringu ili 0 ako nije moguće
generirati formatirani string. Specifikatori formata su:

           %a     kratica od tri slova za ime dana u sedmici (eng. Sun, Mon, Tue,..)
           %A     puno ime dana u sedmici (eng.)
           %b     kratica od tri slova za ime mjeseca (eng. Jan, Feb, Mar,...)
           %B     puno ime mjeseca (eng.)
           %c     kompletni zapis vremena i datuma
           %d     dan u mjesecu (1..31)
           %H     sat u formatu (1..24)
           %I     sat u formatu (1..12)
           %j     dan u godini (1..365)
           %m     mjesec u godini (1..12)
           %M     minute
           %p     AM/PM (eng.) string koji označava jutro ili popodne
           %S     sekunde
           %U     broj za sedmicu u godini (1..52) - 1 određen prvom nedjeljom
           %w     broj za dan u sedmici (0-nedjelja)
           %W     broj za sedmicu u godini (1..52) - 1 određen prvim ponedjeljkom




                                                                                           379
%x     kompletni zapis datuma
           %X     kompletni zapis vremena
           %y     zadnje dvije znamenke godine
           %Y     godina u 4-znamenkastom formatu
           %Z     ime vremenske zone (ako postoji )
           %%     znak %


mktime

        time_t mktime(struct tm *tp);

Funkcija mktime() pretvara zapisa iz strukture tm u time_t format. Korisna je u tzv.
kalendarskim proračunima. Kada je potrebno dodati nekom datumu n dana, tada se može upisati
datum u tm strukturu, povećati član tm_mday za n, zatim pozivom mktime() se dobije
time_t vrijednost koja odgovara novom datumu.


difftime
        double difftime(time_t t1, time_t t2);

Funkcija difftime() vraća realnu vrijednost koja je jednaka razlici vremena t1 i t1 u
sekundama.

clock

        clock_t clock(void);

Funkcija clock() služi za preciznije mjerenje vremena nego je to moguće sa prethodnim
funkcijama. Ona vraća vrijednost procesorskog mjerača vremena, koji starta na početku
programa, u jedinicama koje su znatno manje od sekunde (nekoliko milisekundi). Koliko je tih
jedinica u jednoj sekundi određeno je konstantom CLOCKS_PER_SEC. To znači da izraz:

        (double)clock()/CLOCKS_PER_SEC

daje vrijednost koja je jednaka vremenu (u sekundama) od pokretanja programa.

C 7 <signal.h>
U zaglavlju <signal.h> deklarirane su dvije funkcije (signal() i raise()) za prihvat i
generiranje asinkronih prekida programa ili "signala". Za identificiranje nekoliko mogućih
signala, u ovom zaglavlju su definirane simboličke cjelobrojne konstante sa sljedećim imenima:

SIGABRT         Signal kojeg generira funkcija abort().
SIGFPE          Signal koji se generira kad nastane greška kod matematičkih operacija primjerice pri
                dijeljenju s nulom.
SIGILL          Signal koji se generira ako se pokušava izvršiti nepostojeća ili nedozvoljena instrukcija
                procesora.
SIGINT          Signal koji se generira s tipkovnice (primjerice, Ctrl-C tipkom).
SIGSEGV         Signal koji se generira ako se pristupa zaštićenoj ili nepostojećoj memoriji
                ("segmentation violations").
SIGTERM         Signal koji se generira kada je proces prekinut nekim vanjskim događajem.

Ovisno o implementaciji kompilatora, moguće su i dodatne definicije identifikatora signala.




                                                                                                       380
raise

         int raise(int sig);

Funkcija raise() šalje izvršnom programu signal sig.

signal

         void (*signal(int sig, void (*func)(int)))(int);

Funkcija signal() se koristi za definiranje akcije koja se treba izvršiti kada se pojavi neki
signal. Ukoliko nije definirana radnja koja se vrši nakon pojave signala, prekida se program.

Argument sig je signal kojeg treba prihvatiti (jedna od SIGxxx konstanti).
Argument func je ili konstanta SIG_IGN (kojom se zahtijeva ignoriranje signala) ili konstanta
SIG_DFL (kojom se postavlja predodređeni postupak prihvata signala) ili pokazivač na
korisnički definiranu funkciju koja će se izvršiti pojavom signala. Ta funkcija mora imati
prototip oblika

         void signalhandler(int sig);

Argument ove funkcije tipa int je broj signala koji se prihvaća.
    Funkcija signal() vraća prethodni oblik prihvata signala; SIG_DFL, SIG_IGN, ili
pokazivač na funkciju. Zbog navedenih svojstava, deklaracija funkcije signal() je
kompleksna. To je funkcija koja vraća pokazivač na funkciju koja prima jedan argument tipa
int i vraća void. Prvi argument je tipa int, a drugi argument je pokazivač na funkciju koja
prima jedan argument tipa int i vraća void.
Primjer: u sljedećem programskom odsječku pokazano je kako se postavlja poziv funkcije
exithandler() u slučaju pojave prekida (interrupt signal - SIGINT), ali samo ako taj signal
nije prethodno ignoriran:


         extern void exithandler(int);

         if(signal(SIGINT, SIG_IGN) != SIG_IGN)
                 signal(SIGINT, exithandler);

C 8 <setjmp.h>
U zaglavlju <setjmp.h> deklarirane su funkcije setjmp() i longjmp(), pomoću kojih se
može izvršiti skok u program i izvan funkcije. Mjesto u programu, na koju se vrši skok,
označava se funkcijom setjmp(), koja pamti stanje stoga, registre procesora i trenutnu adresu
programa u objektu tipa jmp_buf. Kasnije se s bilo kojeg mjesta u programu može skočiti na
ovu poziciju pozivom funkcije longjmp().

setjmp

         int setjmp(jmp_buf context);

Funkcija setjmp() sprema trenutnu programsku adresu i stanje procesora u objekt context,
koji je tipa jmp_buf, i vraća vrijednost 0. Kasnije, nakon poziva funkcije longjmp(), povratna
vrijednost se može promijeniti.

longjmp




                                                                                          381
void longjmp(jmp_buf context, int retval)

Funkcija longjmp() vrši skok na stanje opisano u objektu context, koji je prethodno
spremljen pozivom funkcija setjmp(). Skok se vrši na mjesto gdje je prethodno pozvana
funkcija setjmp(), pa se sada vrši povrat iz funkcije setjmp()s vrijednošću retval.

C 9 <locale.h>
U zaglavlju <locale.h> deklarirane su dvije funkcije za lokalizirane postavke. Početno,
program počinje u "C" lokalizaciji, koja se zatim može promijeniti sa setlocale() funkcijom.
Lokalno-specifične informacije se dijele u nekoliko kategorija, koje se označavaju sljedećim
konstantama:

      LC_COLLATE       Usporedba stringova se vrši pomoću funkcija strcoll() i strxfrm()
      LC_CTYPE         Klasiofikacija znakova pomoću funkcija iz <ctype.h>
      LC_MONETARY      Monetarne postavke se dobiju funkcijom localeconv()
      LC_NUMERIC       Koristi decimalnu točku u funkcijama printf(), scanf(), strtod(),
                       itd.
      LC_TIME          Lokalizirani format strftime() funkcije
      LC_ALL           Unija prethodnih postavki

setlocale

      char *setlocale(int cat, const char *locale);

Funkcija setlocale() ima dva argumenta. Prvi argument je oznaka kategorije koja se
postavlja, a drugi parametar locale je string za oznaku lokalizacije. Ako taj string je jednak
"C", tada se koristi predodređena lokalizacija. Prazni string "" također označava predodređenu
lokalizaciju. Sve ostale oznake su specifične za pojedinu implementaciju kompilatora. Funkcija
vraća pokazivač na string koji sadrži prethodnu locale postavku. Ako se setlocale()
pozove s locale == NULL tada funkcija vraća trenutnu postavku.

localeconv

      struct lconv *localeconv(void);

Funkcija localeconv() vraća pokazivač na strukturu lconv koja sadrži lokalno-specifične
informacije. Ta struktura je otprilike definirana ovako:


      struct lconv
      {
             char *decimal_point;
             char *thousands_sep;
             char *grouping;
             char *int_curr_symbol;
             char *currency_symbol;
             char *mon_decimal_point;
             char *mon_thousands_sep;
             char *mon_grouping;
             char *positive_sign, *negative_sign;
             char int_frac_digits;
             char frac_digits;
             char p_cs_precedes, p_sep_by_space;
             char n_cs_precedes, n_sep_by_space;
             char p_sign_posn, n_sign_posn;




                                                                                          382
};


      decimal_point je oznaka koja se koristi za decimalni zarez.
      thousands_sep je separator koji se koristi između grupe znamenki
      grouping je string koji definira veličinu grupe (primjerice "3" označava da se ponavlje
      grupa od 3 znaka ).

Ostali članovi opisuju monetarno-specifične informacije. Ukratko, int_curr_symbol i
currency_symbol su verzije (internacionalne i lokalne) za lokalnu valutu,
mon_decimal_point je decimalna točka, mon_thousands_sep i mon_grouping dopisuju
grupiranje znamenki (analogno s thousands_sep i grouping), positive_sign i
negative_sign su znakovi pozitivnog i negativnog predznaka, int_frac_digits i
frac_digits opisuju broj decimalnih znamenki koje se prikazuju. Ostali članovi opisuju
oznaku valute i indikatore predznaka.

C 10 <stdarg.h>
U zaglavlju <stdarg.h> su definirani makro naredbe pomoću kojih se omogućuje definiranje
funkcija s promjenjljivim brojem parametara.
Koristi se ideja da se argumentima neke funkcije pridijeli lista pokazivača koja ima apstraktni
tip va_list. U tu svrhu koristi se makro va_start. Zatim se iz ove liste mogu dobiti svi
argumenti pomoću makroa va_arg. Na kraju rada, unutar iste funkcije, treba pozvati makro
va_end.

va_start

      va_start(va_list argp, Lastarg);

va_start inicijalizira argp tako da se njime mogu dohvatiti argumenti. Lastarg je ime
posljednjeg fiksnog argumenta funkcije.

va_arg

      argtype va_arg(va_list argp, argtype);

va_arg dobavlja vrijednost sljedećeg argumenta koji je tipa argtype. Tip argtype se
specificira na isti način kako se definira argument sizeof operatora. Tip mora odgovarati tipu
sljedećeg argumenta.

va_end

      va_end(va_list argp);

va_end označava da je završen pristup promjenjljivoj listi argumenata.

Primjer: u sljedećem programu definirana je funkcija miniprintf(), kojom je pokazano
kako je implementirana printf() funkcija.

      #include <stdio.h>
      #include <stdarg.h>

      void miniprintf(const char *format, ...)
      {
          va_list argp;




                                                                                           383
const char *p;
           char tmpbuf[25];
           int i;

           va_start(argp, format);

           for(p = format; *p != '0'; p++) {
               if(*p != '%') {
                  putchar(*p);
                  continue;
               }
               switch(*++p)
               {
                  case 'c':
                      i = va_arg(argp, int); putchar(i);
                      break;
                  case 'd':
                      i = va_arg(argp, int);
                      sprintf(tmpbuf, "%d", i); fputs(tmpbuf, stdout);
                      break;
                  case 'o':
                      i = va_arg(argp, int);
                      sprintf(tmpbuf, "%o", i);
                      fputs(tmpbuf, stdout);
                      break;
                  case 's':
                      fputs(va_arg(argp, char *), stdout);
                      break;
                  case 'x':
                      i = va_arg(argp, int);
                      sprintf(tmpbuf, "%x", i); fputs(tmpbuf, stdout);
                      break;
                  case '%':
                      putchar('%');
                      break;
               }
           }
           va_end(argp);
      }

C 11 <stddef.h>
U zaglavlju <stddef.h> definirano je nekoliko tipova i makro naredbi.

NULL       Makro koji označava konstantu za nul pokazivač (vrijednost mu je 0 ili (void *)0).
size_t     Cjelobrojni unsigned tip koji se koristi za označavanje veličine memorijskog
           objekta.
ptrdiff_t Cjelobrojni tip koji označava vrijednosti koji nastaju kod oduzimanja pokazivača.
wchar_t    Tip “wide character” koji može imati znatno veći interval vrijednosti od tipa char.
           Koristi se za multinacionalni skup znakova (Unicode).
offsetof() Makro kojim se računa pomak (eng. offset) u bajtima nekog elementa strukture,
           primjerice offsetof(struct tm, tm_year).

Korištenje ovih tipova osigurava prenosivost programa.

C 12 <assert.h>
U zaglavlju <assert.h> definar je makro assert, koji omogućuje testiranje pograma.




                                                                                                 384
void assert(int test_izraz)

Ako je test_izraz jednak nuli, tada

      assert(test_izraz)

šalje na stdderr poruku, poput ove:

      Assertion failed: test_izraz, file filename, line nnn

i vrši poziv funkcije abort(), čime se prekida izvršenje programa. Ime izvorne datoteke
(filename) i broj linije u kojoj je assert, su dobijeni pretprocesorskih makroa: __FILE__ i
__LINE__.
Ako se pri kompiliranju definira makro NDEBUG (s bilo kojom vrijednošću) tada se ignorira
makro assert.

C 13 <errno.h>
U zaglavlju <errno.h> deklarirana je globalna varijable errno u kojoj se bilježi kôd greške
koja nastaje pri korištenju funkcija standardne biblioteke. Također, definirane su simboličke
konstante EDOM and ERANGE koje označavaju kôd pogreške kod matematičkih operacija.

C 14 <limits.h>
U zaglavlju <limits.h> definirane su simboličke konstante          koje označavaju interval
standardnih tipova. To su:

                  CHAR_BIT      broj bitova u tipu char
                  CHAR_MAX      maksimalna vrijednost char tipa
                  CHAR_MIN      maksimalna vrijednost char tipa
                  INT_MAX       maksimalna vrijednost int tipa
                  INT_MIN       minimalna vrijednost int tipa
                  LONG_MAX      maksimalna vrijednost long tipa
                  LONG_MIN      minimalna vrijednost long tipa
                  SCHAR_MAX     maksimalna vrijednost signed char tipa
                  SCHAR_MIN     minimalna vrijednost signed char tipa
                  SHRT_MAX      maksimalna vrijednost short tipa
                  SHRT_MIN      minimalna vrijednost short tipa
                  UCHAR_MAX     maksimalna vrijednost unsigned char tipa
                  UINT_MAX      maksimalna vrijednost unsigned int tipa
                  ULONG_MAX     maksimalna vrijednost unsigned long tipa
                  USHRT_MAX     maksimalna vrijednost unsigned short tipa
                  MB_LEN_MAX    broj bajta u multibajt znakovnoj sekvenci


C 15 <float.h>
U zaglavlju <float.h> definirane su simboličke konstante koje označavaju implementaciju
realnih brojeva s pomičnom točkom. To su:

          FLT_RADIX              FLT_ROUNDS
          FLT_MANT_DIG           DBL_MANT_DIG             LDBL_MANT_DIG
          FLT_DIG                DBL_DIG                  LDBL_DIG
          FLT_MIN_EXP            DBL_MIN_EXP              LDBL_MIN_EXP
          FLT_MIN_10_EXP         DBL_MIN_10_EXP           LDBL_MIN_10_EXP
          FLT_MAX_EXP            DBL_MAX_EXP              LDBL_MAX_EXP




                                                                                         385
FLT_MAX_10_EXP         DBL_MAX_10_EXP           LDBL_MAX_10_EXP
          FLT_MAX                DBL_MAX                  LDBL_MAX
          FLT_EPSILON            DBL_EPSILON              LDBL_EPSILON
          FLT_MIN                DBL_MIN                  LDBL_MIN


FLT_RADIX je baza “floating-point” modela (pr. 2, 16). FLT_ROUNDS je konstanta koja
pokazuje kako se zaokružuje rezultat pri zbrajanju: 0 ako je prema 0, 1 ako je prema najbližoj
vrijednosti, 2 ako je prema +∞, 3 ako je –∞, i -1 znači da nije definirano.
Ostali makroi daju svojstva tipova: float (FLT_), double (DBL_), i long double (LDBL_).
MANT_DIG je broj znamenki (baze FLT_RADIX) u mantisi. DIG daje približnu preciznost u
ekvivalentnoj bazi 10. MIN_EXP i MAX_EXP daju maksimalni i minimalni eksponent
(MIN_10_EXP i MAX_10_EXP daju njihov ekvivalent u bazi 10). MIN i MAX daju minimalnu i
maksimalnu vrijednost realnog broja. EPSILON je razlika između 1.0 i sljedećeg većeg broja.

C 16 <iso646.h>
U zaglavlju <iso646.h> definirani su makroi za zamjenu operatora koji možda nisu
implementirani na nekom mikro računalima. To se sljedeće definicije:

      #define   and &&
      #define   and_eq &=
      #define   bitand &
      #define   bitor |
      #define   compl ~
      #define   not !
      #define   not_eq !=
      #define   or ||
      #define   or_eq |=
      #define   xor ^
      #define   xor_eq ^=

C 17 <wchar.h>
U zaglavlju <wchar.h> definirane su gotovo sve funkcije za rad s znakovima i stringovima
koji su tipa wchar_t. Obično se ASCII znakove naziva prostim znakovima, a znakove tipa
wchar_t proširenim znakovima (eng. wide character). Evo kako se inicijalizira prošireni
znakovni tip i string:

      wchar_t c = L'A';
      wchar_t *s = L"Hello";

Pored wchar_t tipa definiran wint_t, integralni tip koji može sadržavati vrijednost wchar_t
tipa, te makro WEOF kao oznaka za kraj datoteke.

Operacije sa stringovima proširenih znakova

      size_t wcslen(const wchar_t *s);
      wchar_t *wcscpy(wchar_t *dest, const wchar_t *src);
      wchar_t *wcscat(wchar_t *dest, const wchar_t *src);
      wchar_t *wcsncpy(wchar_t *dest, const wchar_t *src, size_t n);
      wchar_t *wcsncat(wchar_t *dest, const wchar_t *src, size_t n);
      int wcscmp(const wchar_t *s1, const wchar_t *s2);
      int wcsncmp(const wchar_t *s1, const wchar_t *s2, size_t n);
      int wcscoll(const wchar_t *s1, const wchar_t *s2);
      size_t wcsxfrm(wchar_t *dest, const wchar_t *src, size_t n);




                                                                                          386
wchar_t *wcschr(const wchar_t *s, wchar_t c);
      wchar_t *wcsrchr(const wchar_t *s, wchar_t c);
      wchar_t *wcsstr(const wchar_t *s, const wchar_t *pat);
      size_t wcsspn(const wchar_t *s, const wchar_t *set);
      size_t wcscspn(const wchar_t *s, const wchar_t *set);
      wchar_t *wcspbrk(const wchar_t *s, const wchar_t *set);


Ove funkcije (wcsxxx) su ekvivalentne funkcijama za rad s ASCII stringovima (strxxx), osim što
se umjesto pokazivača na char koristi pokazivač na wchar_t, a broj n se intepretira kao broj
wchar_t znakova.


Operacije s nizovima proširenih znakova

      wchar_t *wmemcpy(wchar_t *dest, const wchar_t *src, size_t n);
      wchar_t *wmemmove(wchar_t *dest, const wchar_t *src, size_t n);
      int wmemcmp(const wchar_t *p1, const wchar_t *p2, size_t n);
      wchar_t *wmemchr(const wchar_t *p, wchar_t c, size_t n);
      wchar_t *wmemset(wchar_t *p, wchar_t c, size_t n);

Ove funkcije (wmemxxx) su ekvivalentne funkcijama za rad s ASCII nizovima (memxxx), osim
što se umjesto pokazivača na char koristi pokazivač na wchar_t, a broj n se intepretira kao
broj wchar_t znakova.

Pretvorba stringa proširenih znakova u numeričku vrijednost

      long int wcstol(const wchar_t *s, wchar_t **endp, int base)
      unsigned long int wcstoul(const wchar_t *s, wchar_t **endp, int
      base);
      double wcstod(const wchar_t *s, wchar_t **endp);

Ove funkcije (wcsxxx) su ekvivalentne funkcijama za rad s ASCII stringovima (strxxx), osim
što se umjesto pokazivača na char koristi pokazivač na wchar_t,

Pretvorba vremena

      size_t wcsftime(wchar_t *buf, size_t bufsize,
                    const wchar_t *format, const struct tm *tp);

wcsftime() izvršava operaciju analognu izvršenju strftime().


Rastav stringa proširenih znakova na lekseme

      wchar_t *wcstok(wchar_t *s, const wchar_t *sep, wchar_t **state);

Funkcija wcstok() vrši rastav stringa s ne lekseme koji su odvojeni znacima separatora (sep)
analogno funkciji strtok(), osim što je temeljni tip wchar_t, i dodan je treći argument
state, koji je pokazivač na objekt tipa wchar_t *; wcstok() koristi ovaj objekt za pohranu
stanja između uzastopnih poziva funkcije.

Ulazno izlazne operacije s proširenim znakovima
getwchar, getwc, fgetwc




                                                                                          387
wint_t getwchar(void);
      wint_t getwc(FILE *fp);
      wint_t fgetwc(FILE *fp);

Ove funkcije čitaju znakove iz toka fp ili stdin (implicitno se vrši pretvorba multibajt-
znakovnih sekvenci, kao da je pozvana funkcija mbrtowc).Ako je kraj datoteke funkcije vraćaju
WEOF. Funkcionalnost im je ista kao kod funkcija getchar(), getc(), i fgetc().

putwchar, putwc, fputwc
      wint_t putwchar(wchar_t c);
      wint_t putwc(wchar_t c, FILE *fp);
      wint_t fputwc(wchar_t c, FILE *fp);

Ove funkcije upisuju wchar_t znakove u toka fp ili stdin (implicitno se vrši pretvorba
multibajt-znakovnih sekvenci, kao da je pozvana funkcija mbrtowc). Funkcionalnost im je ista
kao kod funkcija putchar(), putc(), i fputc().

wprintf, fwprintf
      int wprintf(const wchar_t *, ...);
      int fwprintf(FILE *fp, const wchar_t *, ...);

Ove funkcije su ekvivalentne funkcijama printf() i fprintf(), osim što se u tok zapisuje
multibajt znakovna sekvenca, kao da je pozvan fputwc(). U format stringu specifikatori %c i
%s i dalje znače da se očekuje prosti znakovi, a da bi se ispisali prošireni znakovi treba koristiti
specifikatore %lc i %ls.

wscanf, fwscanf
      int wscanf(const wchar_t *, ...);
      int fwscanf(FILE *fp, const wchar_t *, ...);

Ove funkcije su ekvivalentne funkcijama scanf() i fscanf(), osim što se format string
tretira kao niz proširenih znakova, a tok koji se očitava tretira se kao multibajt znakovni niz. U
format stringu specifikatori %c, %s i %[ znače da se očekuje prosti znakovi, a da bi se unijeli
prošireni znakovi treba koristiti specifikatore %lc , %ls i %l[.

fgetws, fputws
      wchar_t *fgetws(wchar_t *, int, FILE *fp);
      int fputws(const wchar_t *, FILE *fp);

Ove funkcije služe čitanju ili zapisu linije teksta analogno funkcijama fgets() i fputs().

ungetwc
      wint_t ungetwc(wint_t c, FILE *fp);

Funkcija ungetwc() vraća prošireni znak c natrag u ulazni tok fp, analogno ungetc()
funkciji.

swprintf, swscanf




                                                                                                388
int swprintf(wchar_t *buf, size_t bufsize, const wchar_t *format,
        ...);
        int swscanf(const wchar_t *buf, const wchar_t *format, ...);

Funkcija swprintf() generira string buf, maksimalne veličine bufsize, a funkcija
swscanf() dobavlja podatke iz stringa buf, prema zadanom formatu, analogno funkcijama
sprintf() i sscanf().

vwprintf, vfwprintf, vswprintf
        int vwprintf(const wchar_t *format, va_list argp);
        int vfwprintf(FILE *fp, const wchar_t *format, va_list argp);
        int vswprintf(wchar_t *buf, size_t bufsize,
                                const wchar_t *format, va_list argp);

Ove funkcije su analogne funkcijama vprintf(), vfprintf(), and vsprintf(). Argument
vswprint() bufsize omogućuje kontrolu maksimalne duljine stringa kao kod swprintf().

fwide
        int fwide(FILE *fp, int mode);

Svaki tok ima "orijentaciju" koja pokazuje da li se on koristi s normalnim ili multibajt
znakovima (pomoću funkcija iz ove sekcije) Početno je tok “neorijentiran”, ali se nakon prve
upotrebe prebacuje u "bajt-orijentirani" ili "prošireno-orijentirani" mod obrade teksta.
Funkcijom fwide() može se postaviti orijentacija toka fp, tako da se argument mode postavi
na vrijednost veću od nule za "prošireno-orijentirani" mod, ili na vrijednost manju od nule za
"bajt-orijentirani" mod. Funkcija vraća vrijednost trenutne orijentacije (0 znači da je tok
neorijentiran).

Dodatne pretvorbe
btowc, wctob
       wint_t btowc(int c);
       int wctob(wint_t wc);
Funkcija btowc() pretvara normalni znak c u prošireni znak. Funkcija wctob() pretvara
prošireni znak wc u normalni znak. Vraća znak ili EOF ako pretvorba nije moguća.

mbrlen
        size_t mbrlen(const char *s, size_t n, mbstate_t *state);

Funkcija mbrlen() je slična funkciji mblen(). Njome se može dobiti i duljinu prekinute
multibajt sekvence. Početni se dio referira u objektu state, a na preostali dio pokazuje
pokazivač s. Vraća vrijednost kao funkcija mbrtowc().

mbrtowc, wcrtomb
        size_t mbrtowc(wchar_t *dest, const char *src, size_t n,
                                                mbstate_t *state);
        size_t wcrtomb(char *dest, wchar_t src, mbstate_t *state);




                                                                                          389
Ove su funkcije slične funkcijama mbtowc() i wctomb(), osim što mogu obraditi i dio
multibajt sekvence koja je prekinuta, uz uvjet da je stanje pretvorbe zabilježeno u objektu na
kojeg pokazuje state.
Funkcija mbrtowc() pretvara multibajt sekvencu src u proširen znak u *dest i vraća broj
bajta na koje pokazuje src koji tvore ispravnu multibajt sekvencu, ili 0 ako src==NULL, ili -1
ako nastane greška, ili -2 ako nije pronađena kompletna multbajt sekvenca (upotrebljena za
*state).
Funkcija wcrtomb() pretvara prošireni znak src u multibajt sekvencu dest i vraća broj bajta
zapisanih u dest, ili -1 ako nastane greška.

mbsrtowcs, wcsrtombs
      size_t mbsrtowcs(wchar_t *dest, const char **srcp, size_t n,
                                             mbstate_t *state);
      size_t wcsrtombs(char *dest, const wchar_t **srcp, size_t n,
                                              mbstate_t *state);

Ove su funkcije slične funkcijama mbtowcs() i wctombs(), osim što mogu obraditi i dio
multibajt sekvence koja je prekinuta, uz uvjet da je stanje pretvorbe zabilježeno u objektu na
kojeg pokazuje state.
String koji se pretvara prenosi se po referenci *srcp, kako bi se mogao ažurirati da pokazuje
na preostali dio nepretvorenog stringa. Ako je broj n nedovoljan (kao broj proširenih znakova za
mbsrtowcs() ili bajta za wcsrtombs()) za kapacitet odredišnog stringa rezultata, *srcp se
postavlja tako da pokazuje na nepretvoreni ulaz, a *state se ažurira da odrazi prekinuto stanje
pretvorbe.

mbsinit
      int mbsinit(const mbstate_t *p);

Funkcija mbsinit() vraća nenultu vrijednost ako je objekt stanja na kojeg pokazuje p u
početnom stanju, ili ako je p==NULL.

C 18 <wctype.h>
U zaglavlju <wctype.h> deklarirano je nekoliko funkcija za klasificiranje i pretvorbu znakova
tipa wchar_t, analognih funkcijama iz <ctype.h>.
Klasifikacija proširenih znakova

      int   iswupper(wint_t c);
      int   iswlower(wint_t c);
      int   iswalpha(wint_t c);
      int   iswdigit(wint_t c);
      int   iswalnum(wint_t c);
      int   iswxdigit(wint_t c);
      int   iswspace(wint_t c);
      int   iswpunct(wint_t c);
      int   iswprint(wint_t c);
      int   iswgraph(wint_t c);
      int   iswcntrl(wint_t c);


Funkcije (iswxxx) su analogne funkcijana (isxxx), osim što je argument ovih funkcija tipa
wint_t.




                                                                                            390
Dodatne funkcije za klasifikacija proširenih znakova

      wctype_t wctype(const char *classname);
      int iswctype(wint_t c, wctype_t classtok);

Funkcija wctype() prihvaća argument classname u kojem se indicira klasifikacija i vraća
token tipa wctype_t (definiran je u <wctype.h>). Funkcija wctype() prihvaća stringove:
"upper", "lower", "alpha", "digit", "alnum", "xdigit", "space", "punct", "print", "graph", i "cntrl"
(koji odgovaraju predefiniranoj klasifikaciji ), plus korisnički definirani string za klasifikaciju.
Funkcija iswctype() prihvaća argumente znak c i token classtok koji je prethodno dobiven
funkcijom wctype(), te vraće nenultu vrijednost ako znak ne pripada klasifikaciji

Funkcije za pretvorbu proširenih znakova i stringova

      wint_t towupper(wint_t c);
      wint_t towlower(wint_t c);

Ove funkcije su ekvivalentne funkcijana toupper() i tolower(), za normalne znakove.

      wctrans_t wctrans(const char *convname);
      wint_t towctrans(wint_t c, wctrans_t convtok);

Funkcija wctrans() prihvaća argument convname u kojem se indicira znakovna pretvorba i
vraća token tipa wctrans_t koji se koristi za izvršenje pretvorbe (wctrans_t je definiran u
<wctype.h>).
Funkcija towctrans() vrši pretvorbu proširenog znaka c prema tokenu convtok koji je
prethodno dobiven funkcijom wctrans(), i vraća pretvoreni prošireni znak.
Funkcija wctrans() prihvaća stringove "toupper" i "tolower" (koji označavaju predefinirani
način pretvorbe), plus korisnički definirani string za pretvorbu.




                                                                                                391
Index


#define, 47, 186                           biblioteke funkcija, 120
#elif, 190                                 binarne i tekstualne datoteke, 193
#else, 190                                 binarni brojevni sustav, 18
#endif, 190                                binarno pretraživanje, 229
#if, 190                                   binarno stablo, 278, 279
#ifdef, 190                                bit, 7
#ifndef, 190                               bit polja strukture, 179
#include, 186                              bitznačajni operatori, 75
#pragma pack, 177                          BNF notacija, 289
#undef, 189                                Booleova logika, 16
<assert.h>, 380                            break, 100
<ctype.h>, 368                             brojevni sustavi, 17
<errno.h>, 381                             bsearch, 371
<float.h>, 381                             bsearch(), 232
<iso646.h>, 382                            calloc(), 369
<limits.h>, 381                            centralna procesorska jedinica, 8
<locale.h>, 378                            char, 43
<math.h>, 373                              clearerr(), 364
<setjmp.h>, 377                            clock(), 376
<signal.h>, 376                            continue, 100
<stdarg.h>, 379                            COUNTER, 209
<stddef.h>, 380                            ctime(), 374
<stdio.h>, 357                             De Morganov teorem, 16
<stdlib.h>, 369                            decimalni brojevni sustav, 17
<string.h>, 365                            deklaracija funkcije, 27
<time.h>, 374                              deklaracija varijable, 44
<wchar.h>, 382                             dereferencirani pokazivač, 53
<wctype.h>, 386                            difftime(), 376
abort(), 372                               digitalna logika, 14
abs(), labs(), 372                         dinamičke matrice, 167
adresni operator &, 50                     dinamičko alociranje memorije, 159
ADT, 208                                   disjunkcija, 15
apstrakcija, 12                            div(), ldiv(), 372
apstraktni dinamički tipovi, 208           doseg identifikatora, 113
argumenti komandne linije, 157             double, 43
aritmetički izrazi, 72                     do-while petlja, 99
ASCII, 39                                  dvostruko vezana lista, 266
asctime(), 374                             egzistencijalni kvantifikator, 14
asocijativnost i prioritet operatora, 73   eksterne varijable, 117
assert, 210, 380                           ekvivalencija, 15
atexit(), 371                              enum, 180
atof(), 370                                errno, 381
atoi(), atol(), 370                        exit(), 371
automatske varijable, 114                  fclose(), 195, 358
bajt, 7                                    feof(), 363




                                                                                392
ferror(), 364                          komplement dvojke, 35
fflush(), 195, 358                     komplement jedinice, 35
fgetc(), 358                           konjunkcija, 15
fgetpos(), fsetpos(), 363              korijen stabla, 279
fgets(), 361                           kule Hanoia, 226
FIFO, 218                              leksemi, 84
FILE, 192                              LIFO, 213
float, 43                              linker, 22
fopen(), 194, 357                      lista stringova, 258
for petlja, 99                         listovi stabla, 279
fprintf(), 359                         literalne konstante, 82
fputc(), 358                           LL(1) analiza, 293
fputs(), 362                           localeconv(), 378
fread(), 201, 362                      localtime(), 374
free(), 369                            lokalne varijable, 114
freopen(), 358                         long, 43
fscanf(), 360                          longjmp(), 377
fseek(), 204, 362                      LSB, 7
ftell(), 203, 362                      makefile, 33
funkcije, 64                           makro, 187
fwrite(), 201, 362                     malloc(), 369
generator slučajnih brojeva, 156       memchr(), 367
getchar(), getc(), 358                 memcmp(), 367
getenv, 371                            memcpy(), 367
gets(), 361                            memmove(), 367
globalne varijable, 116                memset(), 368
gmtime(), 374                          merge sort, 240
goto, 90                               metajezik, 84
hash tablica, 316                      mktime(), 376
heksadecimalni brojevni sustav, 20     MSB, 7
identifikatori, 82                     multibajt znakovne sekvence, 372
if, 91                                 naredbe, 88
if-else, 92                            negacija, 15
implikacija, 15                        neterminalni simboli, 85
inicijalizacija varijable, 55          nibl, 7
INORDER, 284                           nizovi, 103
int, 43                                nizovi znakova, 146
Integrirana razvojna okolina, 22, 28   obilazak stabla, 283
intepreter, 22                         oktalni brojevnim sustav, 20
interpreter prefiksnih izraza, 288     općenito stablo, 278
invertor, 16                           operativni sustav, 9
izjavna logika, 14                     operator indirekcije, 51
izlazne jedinice, 8                    parser, 294
izrazi, 72                             perror(), 364
izvedivi program, 22                   petlje, 97
izvorni program, 6                     pobrojani tip, 180
jednodimenzionalni nizovi, 103         podijeli pa vladaj, 228
klasifikacija znakova, 368             pointeri, 51
kodiranje, 34                          pokazivači, 51
kodiranje realnih brojeva, 37          postfiksna notacija izraza, 215
kodiranje znakova, 39                  POSTORDER, 284
komentar, 83                           povezivač, 22
kompilator, 21                         poziv funkcije, 26




                                                                          393
pozivna funkcija, 26              složeni operatori, 77
pozvana funkcija, 26              složenost algoritama, 233
predikat, 14                      sortirane liste, 258
PREORDER, 284                     sortiranje, 237
pretprocesor, 186                 sortiranje umetanjem, 239
pretvorba tipova, 79              sprintf(), 364
pretvorba znaka, 369              srand(), 156, 370
printf(), 48, 359                 sscanf(), 364
privremene datoteke, 206          stablo (tree), 278
produkcije C-jezika, 85           statičke globalne varijable, 118
program, 6                        stderr, 192
proste naredbe, 88                stdin, 192
prototip funkcije, 27             stdout, 192
punjač, 22                        Stog ADT (stack), 213
putchar(), putc(), 358            strcmp(), 150
puts(), 362                       strcpy(), 149
qsort(), 371                      strdup(), 162
quicksort, 243                    strftime(), 375
računalo, 6                       string, 146
Računarski algoritam, 9           string operator, 188
radna memorija, 8, 41             strlen(), 147
raise(), 376                      strojni jezik, 21
RAM, 8                            strtod(), 370
rand(), 156, 370                  strtol(), strtoul(), 370
realloc(), 369                    struktura, 171
red (queue), 218                  switch-case, 95
referenca, 44                     system(), 372
rekurzivne funkcije, 223          tablice simbola, 314
rekurzivno silazni parser, 294    terminalni simboli, 84
relacijski i logički izrazi, 74   ternarni uvjetni izraz, 78
remove(), 206, 365                Tic-Tac-Toe, 121
rename(), 206, 365                tijelo funkcije, 26, 66
repna rekurzija, 231              time(), 374
rewind(), 203, 362                tipovi podataka, 43
rezidentni programi, 9            tmpfile(), 365
riječ, 7                          tmpnam(), 365
rječnici, 314                     točka operator, 172
ROM, 8                            tokeni, 84
samoreferentne strukture, 247     tokovi, 192
scanf(), 53, 360                  typedef, 81
selekcijsko sortiranje, 237       ulazne jedinice, 8
semantičke akcije, 297            ungetc(), 362
separatori, 82                    union, 178
setbuf(), setvbuf(), 363          univerzalni kvantifikator, 14
setjmp(), 377                     unsigned, 43
setlocale(), 378                  unutarnji čvorovi - podstabla, 279
short, 43                         uvlačenje redova, 93
signal(), 377                     va_arg, 379
sintaksa i leksika, 82            va_end, 379
sintaktički analizator, 294       va_start, 379
sizeof, 46                        vanjska memorija, 8
sklop-I, 16                       vanjski čvorovi, 279
sklop-ILI, 16                     veliki-O notacija, 233




                                                                       394
vezana lista, 248                        while petlja, 97
Visual Studio, 28                        zaglavlje funkcije, 26, 66
višedimenzionalni nizovi, 111            zakon asocijacije, 16
void, 53                                 zakon distribucije, 16
void funkcije, 68                        zakon idempotentnosti, 16
vprintf(), vfprintf(), vsprintf(), 364   zakon komutacije, 15
vrijeme i datum, 181                     znakovni ulaz/izlaz, 198




                                                                      395

Programiranje c jezikom

  • 1.
    PROGRAMIRANJE C JEZIKOM Nastavni materijalza studente FESB-a. Split, 2005/2006 Autor: Ivo Mateljan 1
  • 2.
    Sadržaj 1 Uvod........................................................................................................................................... 5 2Matematički i elektronički temelji računarstva ........................................................................ 13 2.1 Izjavna i digitalna logika ................................................................................................... 13 2.2 Brojevni sustavi i računska sposobnost računala .............................................................. 16 3 Izrada prvog C programa.......................................................................................................... 20 3.1 Strojni, asemblerski i viši programski jezici ..................................................................... 20 3.2 Prvi program u C jeziku .................................................................................................... 21 3.3 Struktura i kompiliranje C programa ................................................................................ 25 3.4 Integrirana razvojna okolina (IDE) ................................................................................... 27 3.5 Usmjeravanje procesa kompiliranja programom nmake................................................... 32 4 Kodiranje i tipovi podataka ...................................................................................................... 33 4.1 Kodiranje i zapis podataka ................................................................................................ 33 4.2 Memorija ........................................................................................................................... 40 4.3 Prosti tipovi podataka........................................................................................................ 42 4.4 Direktiva #define............................................................................................................... 46 4.5 Specifikatori printf funkcije .............................................................................................. 47 4.6 Pristup podacima pomoću pokazivača .............................................................................. 49 4.7 Unos podataka u memoriju računala ................................................................................. 52 4.8 Inicijalizacija varijabli....................................................................................................... 54 5 Uvod u programiranje C jezikom............................................................................................. 55 5.1 Postupak izrade programa ................................................................................................. 55 5.2 Algoritamska struktura C programa? ............................................................................... 57 5.3 Funkcije C jezika............................................................................................................... 63 5.4 Zaključak........................................................................................................................... 70 6 Izrazi i sintaksa C jezika........................................................................................................... 71 6.1 Izrazi.................................................................................................................................. 71 6.2 Automatska i explicitna pretvorba tipova ......................................................................... 78 6.3 Definiranje sinonima tipa pomoću typedef ....................................................................... 81 6.4 Formalni zapis sintakse C-jezika....................................................................................... 81 7 Proste i strukturalne naredbe C jezika...................................................................................... 87 7.1 Proste naredbe ................................................................................................................... 87 7.2 Strukturalne naredbe ......................................................................................................... 89 8 Nizovi..................................................................................................................................... 102 8.1 Jednodimenzionalni nizovi.............................................................................................. 102 8.2 Prijenos nizova u funkciju............................................................................................... 107 8.3 Višedimenzionalni nizovi................................................................................................ 110 9 Blokovi, moduli i dekompozicija programa........................................................................... 112 9.1 Blokovska struktura programa ........................................................................................ 112 9.2 Funkcionalna dekompozicija programa "od vrha prema dolje" ...................................... 120 9.3 Zaključak......................................................................................................................... 128 10 Rad s pokazivačima.............................................................................................................. 129 2
  • 3.
    10.1 Tip pokazivača.............................................................................................................. 129 10.2 Operacije s pokazivačima.............................................................................................. 130 10.3 Pokazivači kao argumenti funkcije ............................................................................... 131 10.4 Pokazivači i nizovi ........................................................................................................ 132 10.5 Pokazivači i argumenti funkcije tipa niza ..................................................................... 134 10.6 Patrametri funkcije tipa void pokazivača ...................................................................... 136 10.7 Pokazivači na funkcije .................................................................................................. 137 10.8 Kompleksnost deklaracija ............................................................................................. 139 10.9 Polimorfne funkcije....................................................................................................... 141 10.10 Zaključak..................................................................................................................... 144 11 Nizovi znakova - string ........................................................................................................ 146 11.1 Definicija stringa ........................................................................................................... 146 11.2 Standardne funkcije za rad sa stringovima.................................................................... 148 11.3 Ulazno-izlazne operacije sa stringovima....................................................................... 151 11.4 Korisnički definirane ulazne operacije sa stringovima ................................................. 152 11.5 Pretvorba stringa u numeričku vrijednost ..................................................................... 153 11.6 Nizovi stringova ............................................................................................................ 155 11.7 Generator slučajnih brojeva .......................................................................................... 157 11.8 Argumenti komandne linije operativnog sustava.......................................................... 158 12 Dinamičko alociranje memorije ........................................................................................... 160 12.1 Funkcije za dinamičko alociranje memorije ................................................................. 160 12.2 Kako se vrši alociranje memorije.................................................................................. 163 12.3 Alociranje višedimenzionalnih nizova .......................................................................... 165 12.4 Standardne funkcije za brzi pristup memoriji ............................................................... 171 13 Korisnički definirane strukture podataka ............................................................................. 172 13.1 Struktura (struct)...................................................................................................... 172 13.2 Union – zajednički memorijski objekt za različite tipova podataka.............................. 180 13.3 Bit-polja......................................................................................................................... 181 13.4 Pobrojanji tip (enum).................................................................................................... 182 13.5 Strukture i funkcije za očitanje vremena....................................................................... 183 14 Leksički pretprocesor ........................................................................................................... 188 14.1 Direktiva #include ......................................................................................................... 188 14.2 Direktiva #define za makro-supstitucije........................................................................ 188 14.3 String operatori # i ##.................................................................................................. 190 14.4 Direktiva #undef............................................................................................................ 191 14.5 Direktive za uvjetno kompiliranje................................................................................. 192 15 Rad s datotekama i tokovima ............................................................................................... 194 15.1 Ulazno-izlazni tokovi .................................................................................................... 194 15.2 Binarne i tekstualne datoteke ........................................................................................ 195 15.3 Pristup datotekama ........................................................................................................ 195 15.4 Formatirano pisanje podataka u datoteku...................................................................... 197 15.5 Formatirano čitanje podataka iz datoteke...................................................................... 199 15.6 Znakovni ulaz/izlaz ....................................................................................................... 200 15.7 Direktni ulaz/izlaz za memorijske objekte .................................................................... 203 15.8 Sekvencijani i proizvoljni pristup datotekama .............................................................. 206 15.9 Funkcije za održavanje datoteka ................................................................................... 208 16 Apstraktni tipovi podataka - ADT........................................................................................ 210 3
  • 4.
    16.1 Koncept apstraktnogdinamičkog tipa podataka ........................................................... 210 16.2 Stog i STACK ADT ...................................................................................................... 215 16.3 Primjena stoga za proračun izraza postfiksne notacije.................................................. 218 16.4 Red i QUEUE ADT....................................................................................................... 221 16.5 Zaključak....................................................................................................................... 224 17 Rekurzija i složenost algoritama .......................................................................................... 225 17.1 Rekurzivne funkcije ...................................................................................................... 225 17.2 Matematička indukcija .................................................................................................. 227 17.3 Kule Hanoja .................................................................................................................. 227 17.4 Metoda - podijeli pa vladaj (Divide and Conquer)........................................................ 230 17.5 Pretvorba rekurzije u iteraciju ....................................................................................... 232 17.6 Standardna bsearch() funkcija ....................................................................................... 234 17.7 Složenost algoritama - "Veliki - O" notacija................................................................. 236 17.8 Sortiranje ....................................................................................................................... 239 17.9 Zaključak....................................................................................................................... 248 18 Samoreferentne strukture i liste............................................................................................ 249 18.1 Samoreferentne strukture i lista..................................................................................... 249 18.2 Operacije s vezanom listom .......................................................................................... 250 18.3 Što može biti element liste ............................................................................................ 259 18.4 Lista sa sortiranim redoslijedom elemenata .................................................................. 260 18.5 Implementacija ADT STACK pomoću linearne liste ................................................... 265 18.6 Implementacija ADT QUEUE pomoću vezane liste..................................................... 267 18.7 Dvostruko vezana lista .................................................................................................. 269 18.8 Generički dvostrani red - ADT DEQUEUE.................................................................. 271 18.9 Zaključak....................................................................................................................... 280 19 Razgranate strukture - stabla ................................................................................................ 281 19.1 Definicija stabla ............................................................................................................ 281 19.2 Binarno stablo ............................................................................................................... 282 19.3 Interpreter prefiksnih izraza .......................................................................................... 291 19.4 Stabla s proizvoljnim brojem grana .............................................................................. 305 19.5 Prioritetni redovi i hrpe ................................................................................................. 309 19.6 Zaključak....................................................................................................................... 316 20 Strukture za brzo traženje podataka .................................................................................... 317 20.1 Tablice simbola i rječnici .............................................................................................. 317 20.2 Hash tablica ................................................................................................................... 318 20.3 BST - binarno stablo traženja........................................................................................ 333 20.4 Crveno-crna stabla......................................................................................................... 344 Literatura ................................................................................................................................... 354 Dodatak ..................................................................................................................................... 355 Dodatak A - Elementi dijagrama toka................................................................................... 355 Dodatak B - Gramatika C jezika ........................................................................................... 356 Dodatak C - Standardna biblioteka C jezika ......................................................................... 361 Index.......................................................................................................................................... 392 4
  • 5.
    1 Uvod Naglasci: • Što je računalo ? • Što je program ? • Kako se rješavaju problemi pomoću računala? • Računarski procesi i memorijski objekti • Apstrakcija, algoritam, program Računalo ili kompjuter (eng. computer) je naziv za uređaje koji obavljaju radnje prema programima koje izrađuje čovjek. Sastavni dijelovi računala nazivaju se hardver, a programi i njihova dokumentacija nazivaju se softver. Prvotno su računala služila za obavljanje numeričkih proračuna, odatle i potječe naziv računalo. Danas računala služe za obradu različitih problema. Korisnike računala zanima kako se koristi računalo, a one koji izučavaju računala zanima: • kako se izrađuje računalo, • kako se izrađuje program i • kako se rješavaju problemi pomoću računala. Ovdje će biti pokazano kako se izrađuju programi i kako se programiranjem rješavaju različiti problemi. Bit će opisana i unutarnja građa računala. Za pisanje programa koristit će se programski jeziku C i asemblerski jezik. Što je program? Program je zapis operacija koje računalo treba obaviti. Taj zapis može biti u obliku izvršnog programa ili u obliku izvornog programa. Izvršni program sadrži kôd operacija koje izvršava stroj računala, pa se naziva i strojni program. Izvorni program se zapisuje simboličkim jezikom koji se naziva programski jezik. Prevođenje izvornog programa u strojni program vrši se pomoću programa koji se nazivaju kompilatori (ili kompajleri). Stroj računala Postoje dva tipa elektroničkih računala: analogna i digitalna. Analognim računalima se obrađuju kontinuirani elektronički signali. Digitalnim računalom se obrađuju, prenose i pamte diskretni elektronički signali koji u jednom trenutku mogu imati samo jedno od dva moguća stanja. Ta stanja se označavaju znamenkama 0 i 1, odatle i naziv digitalna računala (eng. digit znači znamenka). Programere i korisnike ne zanimaju elektronički signali u računalu, već poruka koju oni prenose – digitalna informacija. Brojevni sustav, u kojem postoje samo dvije znamenke, naziva se binarni brojevni sustav. U tom se sustavu može kodirati različite informacije koristeći više binarnih znamenki. Znamenka binarnog brojevnog sustava se naziva bit (kratica od eng. binary digit), a može imati samo dvije vrijednosti 0 ili 1. Niz od više bitova predstavlja kodiranu informaciju koja može 5
  • 6.
    predstavljati operaciju kojuračunalo treba izvršiti ili neki smisleni podatak. Uobičajeno je za nizove bitova koristiti nazive iz Tablice 1.1. U binarnom nizu često se označava redoslijed bitova. Kratica LSB označava bit najmanjeg značaja (eng. least significant bit), a MSB označava bit najvećeg značaja (eng. most significant bit). Primjer je dan na slici 1.1. Bit je naziv za binarnu znamenku Nibl je naziv za skupinu od četiri bita (eng. nibble) s kojom se operira kao s cjelinom. Bajt ili oktet je naziv za skupinu od osam bita (eng. byte) s kojom se operira kao s cjelinom. Riječ je naziv za skupinu od više bajta (eng. word) s kojom se operira kao s cjelinom. Kod mikro računala za riječ se uzima skupina od 2 bajta. Kod većih računala za riječ se uzima skupina od 4 ili 8 bajta. Tablica 1.1 Nazivi temeljnih binarnih nizova MSB LSB značaj bitova 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 položaj bita 1 0 0 0 1 1 1 1 1 0 1 0 1 1 0 1 binarni niz nibl 3 nibl 2 nibl 1 nibl 0 niz nibla bajt 1 bajt 0 niz bajta Riječ riječ Slika 1.1 Označavanje binarnog niza Za označavanje većih nizova koriste se prefiksi: k (kilo) ⇔ × 1024 M (mega) ⇔ k × 1024 G (giga) ⇔ M × 1024 T (tera) ⇔ G × 1024 Primjerice, 2 kB (kilobajta) = 2048 bajta, 3 Mb (megabita) = 3145728 bita. Digitalno računalo može pamtiti i izvršavati programe, te dobavljati, pamtiti i prikazivati različite informacije. Te informacije, koje su na prikladan način pohranjene u računalu, su programski podaci. broj bita – n broj kombinacija – 2n 2 4 3 8 4 16 8 256 16 65536 32 4294967296 Tablica 1.2 Broj kombinacija s n bita Često se računala klasificiraju kao 8-bitna, 16-bitna, 32-bitna ili 64-bitna. Pod time se podrazumijeva da n-bitno računalo može operirati s nizom od n bita kao s cjelinom. Broj bita koji se koristi za opisivanje nekog podatka ovisi o veličini skupa kojem taj podatak pripada. 6
  • 7.
    Razmotrimo skup podatakakiji se kodira s tri bita. Taj skup može imati maksimalno 8 elemenata jer se s tri bita može kodirati maksimalno osam kombinacija: 000, 001, 010, 011, 100, 101, 110, 111. Lako je pokazati da se s n-bita može kodirati podatke iz skupa od maksimalno 2n elemenata. Tablica 1.2 pokazuje da se udvostručenjem broja bitova značajno povećava skup vrijednosti koje se mogu kodirati u računalu. Operacije se u računala nikada ne izvršavaju samo s jednim bitom, već se istovremeno prenosi i obrađuje više bita. Kod svih računala usvojeno je da najmanja jedinica digitalne informacije, koja se kao cjelina prenosi i pamti u računalu, sadrži 8 bita, tj. jedan bajt. Na slici 1.2 prikazani su sastavni dijelovi digitalnog računala. Centralna procesorska jedinica (CPU – central processing unit) - kontrolira izvršenje programa i aritmetičko-logičkih operacija. CPU je kod mikro i mini računala izveden kao jedinstveni integrirani elektronički sklop (čip) i naziva se mikroprocesor. Uobičajeno je koristiti naziv procesor, bilo da se radi o mikroprocesoru ili o skupini čipova koji obavljaju funkcije CPU, a programe koji se izvršavaju u računalu naziva se procesima. Slika 1.2. Opći prikaz digitalnog računala Radna memorija – pamti digitalne informacije za vrijeme dok je računalo u operativnom stanju. U memoriji se nalazi programski kôd i podaci s kojima operira procesor na temelju naredbi sadržanih u programskom kôdu. Memorija je napravljena od poluvodičkih elemenata u koje procesor može upisati i iz kojih može čitati digitalne informacije. Ta memorija se naziva RAM (eng. random access memory). Sa programerskog stajališta RAM predstavlja linearno uređen prostor u kojem se istovremeno može pristupiti grupi od 8 bita digitalne informacije (1 bajt). Položaj ove temeljne memorijske ćelije se označava prirodnim brojem i naziva se adresa. Jedan manji dio memorije je napravljen od poluvodičkih elemenata koji mogu trajno pamtiti digitalnu informaciju, a naziva se ROM (eng. read-only memory). U ROM-u je upisan program koji služi pokretanju osnovnih funkcija računala. U samom procesoru ugrađeno je nekoliko manjih memorijskih jedinica koje se nazivaju registri. Registri služe za privremeni smještaj programskog kôda i podataka iz radne memorije, te rezultata aritmetičko-logičkih operacije koje se izvršavaju u samom procesoru. Broj bita koji može biti pohranjen u jednom registru naziva se riječ procesora. Kod većine današnjih PC računala riječ procesora sadrži 32 bita (4 bajta), pa se kaže da su to 32-bitna računala. Vanjska memorija - služi za trajnu pohranu podataka. U tu svrhu koriste se magnetski i optički mediji (tvrdi disk, savitljive diskete, magnetske trake, optički diskovi,..). Podaci se na njima pohranjuju u organiziranom i imenovanom skupu podataka koji se nazivaju datoteka. Ulazne jedinice - služe za unos podataka (tipkovnica, miš, svjetlosna olovka, mikrofon,..). Standardna ulazna jedinica je tipkovnica. Izlazne jedinice - služe za prikaz informacija korisniku računala (video-monitor, pisač, zvučnik,...). Standardna izlazna jedinica je video-monitor. 7
  • 8.
    Računalo u operativnomstanju održava poseban program koji se naziva operativni sustav. On vrši temeljne funkcija računala: inicijalizaciju računala i priključenih vanjskih jedinica pri uključenju električnog napajanja, kontrolu i redoslijed izvođenja programa, kontrolu korištenja memorije, pohranjivanje i obradu podataka, vremensku raspodjelu funkcija računala, itd. Operativni sustav nije nužno jedinstven program, već se sastoji od više programskih cjelina. On se, jednim dijelom, trajno nalazi u ROM memoriji računala. Programi s novakvim svojstvom nazivaju se rezidentni programi. Svi ostali programi moraju se prije izvršenja upisati u memoriju računala. Može se izvršiti funkcionalna podjela softvera na sistemski i aplikativni softver. U sistemski softver spadaju programi operativnog sustava, razni jezični procesori (interpreteri, kompilatori, emulatori itd.), programi za testiranje programa (debugger), servisni i uslužni programi, te razni pomoćni programi (matematički, statistički, baze podataka i uređivači teksta). Aplikativni softver predstavljaju različiti korisnički programi. Kako se rješavaju problemi pomoću računala? Kada se rješava neki problem, do ideje za rješenje dolazi se analizom problema. Čovjeku je često dovoljno da već iz idejnog rješenja, koristeći svoju inteligenciju i predznanje, brzo dođe do potpunog rješenja problema. Računalo, samo po sebi, ne raspolaže s inteligencijom, već jedino može izvršavati određen broj jednostavnih operacija. Zbog toga, upute za rješenje problema pomoću računala moraju biti zapisane u obliku preciznog algoritma. Računarski algoritam je precizni opis postupka za rješenje nekog problema u konačnom broju koraka i u konačnom vremenskom intervalu. Pravila kako se piše algoritam nisu strogo određena. Algoritam se može definirati običnim govornim jezikom, tablicama i matematičkim formulama koje opisuju problem, te usmjerenim grafovima koji opisuju tok izvršenja programa. Primjer: Algoritam kojim se u pet koraka opisuje postupak zamjene točka na automobilu glasi: 1. ispitaj ispravnost rezervnog točka, 2. podigni auto, 3. skini točak, 4. postavi rezervni točak, 5. spusti auto. Ovaj algoritam je jasan svakome tko je bar jednom mijenjao točak, međutim, računalo je izvršitelj kojem upute, iskazane nizom naredbi, nisu dovoljno jasne, jer ono ne zna (1) gdje se nalazi rezervni točak, (2) kako se provjerava njegova ispravnost, (3) kako i čime podignuti auto, te (4) kojim alatom se skida i postavlja točak. Zbog toga se algoritam dorađuje preciziranjem pojedinog koraka algoritma. Primjerice, u prvom koraku treba predvidjeti sljedeće naredbe: 1. ispitaj ispravnost rezervnog točka, 1.1. otvori prtljažnik 1.2. izvadi rezervni točak 1.3. uzmi mjerač tlaka iz kutije s alatom 1.4. izmjeri razinu tlaka 1.5. dok je razina tlaka manja 1,6 ponavljaj pumpaj gumu 15 sekundi izmjeri razinu tlaka Podrazumijeva se da je naredba označena s 1. zamijenjena s nizom naredbi koje su označene s 1.1, 1.2,..1.5. Naredbe iskazane u koracima 1.1 do 1.4 su same po sebi jasne. Korak 1.5 treba dodatno pojasniti. Njime je opisan postupak pumpanja gume do neke razine tlaka. Pošto nitko ne može unaprijed znati koliko vremena treba pumpati gumu, da bi se postigla željena razina tlaka, predviđeno je da se dvije naredbe: "pumpaj gumu 15 sekundi" i "izmjeri razinu tlaka", višekratno ponavljaju, sve dok je razina tlaka manja od 1,6. Obje ove naredbe su 8
  • 9.
    zapisane uvlačenjem redakako bi se točno znalo koje naredbe treba ponavljati. Ovaj se tip naredbe naziva iteracija ili petlja. Uobičajeno se kaže da petlja ima zaglavlje, u kojem se ispituje uvjet ponavljanja petlje (dok je razina tlaka manja od 1,6 ponavljaj), i tijelo petlje, koje obuhvaća jednu ili više naredbi koje treba ponavljati. Naziv petlja podsjeća na činjenicu da se uvijek nakon izvršenja posljednje naredbe tijela petlje proces vraća na izvršenje prve naredbe, ali samo u slučaju ako je zadovoljen uvjet iskazan u zaglavlju petlje. Naredbe petlje nisu posebno numerirane jer su one povezane uz zaglavlje petlje, a izvršavaju se u kao jedinstvena složena naredba. Uobičajeno se niz naredbi koji predstavljaju jedinstvenu složenu naredbu naziva i blok naredbi ili samo blok. Uvjet ponavljanja petlje je izjava: "razina tlaka manja od 1,6". Odgovor na ovu izjavu može biti "Da" ili "Ne", ovisno o trenutno izmjerenoj razini tlaka. Ako je odgovor "Da", kažemo da je ispunjen uvjet ponavljanja petlje. Računarska se znanost koristi znanjima matematičke logike. U tom kontekstu ova izjava predstavlja tzv. predikatni izraz koji može imati samo dvije logičke vrijednosti: "istina" ili "laž", pa se kaže da je uvjet održanja petlje ispunjen ako je predikatni izraz istinit. Matematička logika je zapravo znanstveni temelj cijele računarske znanosti i o njoj će biti više govora u sljedećem poglavlju. Pokušajte dalje sami precizirati korake 2, 3 , 4 i 5. Ali pazite, kad pomislite da je problem ispravno riješen, moguće je da se opet potkrade neka greška. To se obično događa kada se ne predvide sve moguće situacije, odnosno stanja u koja se može doći. Primjerice, gornji algoritam nije predvidio slučaj da je guma probušena. Kakav bi razvoj događaja tada bio, ako bi se dosljedno poštovao postupak iz koraka 1.5? Pošto je kod probušene gume razina tlaka uvijek manja od 1,6, ispada da bi tada izvršitelj naredbi ponavljao postupak pumpanja gume beskonačan broj puta. Algoritam se može popraviti tako da korak 1.5 glasi: 1.5. ako je tlak manji od 0.1 tada ako je guma probušena onda odnesi točak na popravak inače dok je tlak manji od 1.6 ponavljaj pumpaj gumu 15 sekundi izmjeri razinu tlaka U ovom se zapisu koriste tzv. naredbe selekcije, prema sljedećoj logici izvršenja: ako je ispunjen uvjet tada izvrši prvi niz naredbi inače izvrši alternativni niz naredbi Ovaj se tip naredbe zove uvjetna selekcija ili grananje, jer se nakon ispitivanja logičkog uvjeta vrši selekcija jednog od dva moguća niza naredbi, odnosno program se grana u dva smjera. Specijalni oblik selekcije je uvjetna naredba tipa: ako je ispunjen uvjet tada izvrši naredbu Njome se određuje izvršenje neke naredbe samo ako je ispunjen neki uvjet. Koristeći naredbe selekcije, algoritam se može zapisati u obliku: 1. ispitaj ispravnost rezervnog točka, 1.1 otvori prtljažnik 1.1.1. uzmi najmanji od tri ključa 1.1.2. gurni ključ u bravu i lagano ga okreni na desno 1.1.3. podigni vrata prtljažnika 9
  • 10.
    1.2. izvadi rezervnitočak 1.2.1. podigni tapetu 1.2.2.ako je točak pričvršćen vijkom onda odvij vijak 1.2 .2. izvadi točak 1.3. uzmi kutiju s alatom 1.4. ispitaj razinu tlaka 1.4.1. izvadi mjerač tlaka iz kutije alata 1.4.2. postavi ga na zračnicu točka 1.4.3. očitaj razinu tlaka 1.5. ako je tlak manji od 0,1 onda 1.5.1. provjeri da li je guma probušena 1.5.2. ako je guma probušena onda odnesi točak na popravak inače, ako je tlak manji od 1,6 onda 1.5.3. otvori prednji poklopac motora 1.5.4. uzmi zračnu pumpu 1.5.5. dok je tlak < 1,6 ponavljaj postavi crijevo pumpe na zračnicu dvadeset puta pritisni pumpu na zračnicu postavi mjerač tlaka ispitaj razinu tlaka Očito da je potrebno dosta raditi i dosta razmišljati da bi se napisao kvalitetan algoritam. Nakon što je napisan precizan algoritam rješenja problema, pristupa se pisanju izvornog programa. Kako se to radi bit će objašnjeno u sljedećim poglavljima. Važno je uočiti da su u zapisu algoritma korištena četiri tipa iskaza: 1. proste ili primitivne naredbe – iskazi koji označavaju jednu operaciju 2. blok naredbi – iskazi koji opisuju niz naredbi koje se sekvencijalno izvršavaju jedna za drugom, a tretiramo ih kao jedinstvenu složenu operaciju. 3. naredbe selekcije – iskazi kojima se logički uvjetuje izvršenje bloka naredbi. 4. iterativne naredbe ili petlje – iskazi kojima se logički kontrolira ponovljeno izvršenje bloka naredbi. Računarski procesi i memorijski objekti Svaki proces rezultira promjenom stanja ili atributa objekata na koje procesi djeluju. Uobičajeno se stanje nekog promjenljivog objekta označava kao varijabla koja ima neko ime. U računalu se stanje objekta pamti u memoriji računala pa se algoritamske varijable mora tretirati kao memorijske objekte. Kada se u C jeziku napiše iskaz x = 5; on predstavlja naredbu da se memorijskom objektu, imena x, pridijeli vrijednost 5. Ako se pak napiše iskaz: x = 2*x +5; on predstavlja proces u kojem se najprije iz memorije očitava vrijednost memorijskog objekta x zapisanog na desnoj strani znaka =. Zatim se ta vrijednost množi s 2 i pribraja joj se numerička vrijednost konstante 5. Time je dobivena numerička vrijednost izraza s desne strane znaka =. Ta se vrijednost zatim pridjeljuje memorijskom objektu s lijeve strane znaka =. Konačni je rezultat ovog procesa da je varijabli x pridijeljena vrijednost 15. Ako bi prethodni iskaz tretirali kao matematički iskaz, on bi predstavljao jednadžbu s jednom varijablom, koja uvjetuje da je vrijednost varijable x jednaka 5. 10
  • 11.
    Znak = uC jeziku ne predstavlja znak jednakosti, kao u matematici već operator pridjele vrijednosti. Njegova upotreba označava naredbu da se vrijednost memorijskog objekta s lijeve strane znaka = postavi na vrijednost izraza koji je zapisan s desne strane znaka =. Takove naredbe se zovu naredbe pridjele vrijednosti. Zbog ove nekonzistentnosti upotrebe znaka = u matematici u odnosu na upotrebu u nekim programskim jezicima (C, Basic, Fortan, Java) često se u općim algoritamskim zapisima operator pridjele vrijednosti zapisuje znakom ←, primjerice: x ← 5 x ← 2*x +5 Operacija pridjele vrijednosti posljedica je načina kako procesor obrađuje podatke u računalu. Naime, procesor može vršiti operacije samo nad podacima koji se nalaze u registrima procesora, pa je prije svake operacije s memorijskim objektima prethodno potrebno njihov sadržaj (vrijednost) prenijeti u registre procesora, a nakon obavljene operacije se sadržaj iz registra, koji sadrži rezultat operacije, prebacuje u memorijski objekt označen s lijeve strane operatora pridjele vrijednosti. Kaže se da procesor funkcionira po principu: dobavi-izvrši-spremi (eng. fetch-execute-store). Što je to apstrakcija? Netko može primijetiti da je opisani proces zamjene točka loš primjer primjene računala. To je točno, jer ako bi se napravio robot, koji bi obavljao navedenu funkciju, onda bi to bila vrlo neefikasna i skupa upotreba računala. Međutim, malo iskusniji programer bi prema gornjem algoritmu mogao lako napraviti program kojim se animirano simulira proces zamjene točka. To je moguće jer, iako je prethodni algoritam apstraktan, on specificira procese u obliku koji se može ostvariti računarskim programom. Apstrakcija je temeljna mentalna aktivnost programiranja. U računarskoj se terminologiji pod pojmom apstrakcije podrazumijeva prikladan način zapisa o objektima i procesima koje se obrađuje pomoću računala, a da se pri tome ne vodi računa o tome kako je izvršena stvarna računarska implementacija, niti objekta niti procesa. Važna je samo ona pojavnost koja je određena apstrakcijom. Algoritam zapisan programskim jezikom predstavlja apstrakciju strojnog koda, a algoritam zapisan prirodnim jezikom predstavlja apstrakciju programskog jezika. Programski jezik služi da se formalnim jezikom zapiše procese i stanje memorijskih objekata u računalu, pa on predstavlja apstrakciju računarskih procesa i stanja memorije. Pomoću programskih jezika se piše program koji ponovo predstavlja neku novu apstrakciju, a u toku izvršenja programa moguće je daljnje usložnjavanje apstrakcije. Primjerice, korisnik CAD programa pokretima miša zadaje program za crtanje nekog geometrijskog oblika. S obzirom na način kako je izvršena apstrakcija računarskog procesa, može se izvršiti sljedeća klasifikacija programskih jezika: 1. Imperativni (proceduralni) programski jezici (C, Pascal, Modula-2, Basic, Fortran,..) 2. Objektno orijentirani programski jezici (C++, Java, C#, Eiffel, Objective C, Smaltalk, Modula-3, ..) 3. Funkcionalni programski jezici (Lisp, Sheme, ML, Haskel..) 4. Logički programski jezici (Prolog) 5. Jezici specijalne namjene: pretraživanje baza podataka (SQL), vizuelno programiranje (Delphi, Visual Basic), uređivanje teksta (Perl, TeX, HTML), matematički proračuni (Matlab). Imperativni programski jezici koriste iskaze koji su bliski naredbama procesora (to su naredbe pridjele vrijednosti, aritmetičko-logičke operacije, uvjetni i bezuvjetni skokovi te poziv 11
  • 12.
    potprograma). Kod objektnoorijentiranih jezika naglasak je na tome da varijable predstavljaju atribute nekog objekta, a funkcije predstavljaju metode pomoću kojih objekt komunicira s drugim objektima. Specifikacije atributa i metoda određuju klase objekata. Kod funkcionalnih se jezika ne koristi temeljna imperativna naredba pridjele vrijednosti, već se sva međudjelovanja u programu opisuju funkcijama. Teorijska podloga ovih jezika je u tzv. λ- računu. Kod logičkih programskih jezika međudjelovanja se u programu opisuju predikatnim logičkim izrazima i funkcijama. Naglasak je na zapisu onoga “što program treba izvršiti”, za razliku od imperativnih jezika pomoću kojih se zapisuje “kako nešto izvršiti”. Apstrakcija je dakle, temeljna mentalna aktivnost programera. Ona je moguća samo ako se dobro poznaje programski jezik i programske algoritme za efikasno korištenje računarskih resursa. O tome će biti riječi u sljedećim poglavljima. 12
  • 13.
    2 Matematički ielektronički temelji računarstva Naglasci: • Izjavna logika • Logičke funkcije i predikati • Booleova logika • Temeljni digitalni sklopovi • Brojevni sustavi 2.1 Izjavna i digitalna logika Bit će navedeni osnovni pojmovi potrebni za razumijevanje izjavne logike (ili propozicijske logike), koji se intenzivno koristi u programiranju, i digitalne logike koja je temelj izgradnje digitalnog računala. Osnovni objekt kojeg proučava izjavna logika je elementarna izjava. Ona može imati samo jedno svojstvo - njome se izriče "laž" ili "istina". Primjerice, izjava "osam je veće od sedam" je istinita, a izjava "broj sto je djeljiv sa sedam" je laž. Pri označavanju izjava koristit će se slovo T (true) za istinitu izjavu i F (false) za lažnu izjavu. Rečenica "broj x je veći od broja y" ne predstavlja izjavu jer njena istinitost ovisi o veličini brojeva x i y. Ako se umjesto x i y uvrste brojevi dobije se izjava. Ovakve rečenice se nazivaju izjavne funkcije, a za x i y se kaže da su (predmetne) varijable. Odnos među varijablama, kojeg izjavna funkcija izriče, naziva se predikat. Označi li se u prethodnom primjeru predikat " ... je veći od.... " sa P, navedena izjavna funkcija se može zapisati u obliku P(x,y). Izjavne funkcije se prevode u izjave kada se uvrsti vrijednost predmetnih varijabli ili ako se uz izjavne funkcije primijene neodređene zamjenice svaki (oznaka ∀ koja se naziva univerzalni kvantifikator) ili neki (oznaka ∃ koja se naziva egzistencijalni kvantifikator). ∃x se čita i "postoji x". Primjerice, prethodna izjavna funkcija primjenom kvantifikatora u predikatnom izrazu (∀ y)(∃x)P(x,y) postaje izjava koja znači: "za svaki broj y postoji broj x takav da je x veći od y". Rezultat izjavne funkcije je logička vrijednost T ili F. Varijable koje sadrže logičku vrijednost nazivaju se logičke varijable. U programiranju se često koriste izjavne funkcije iskazane tzv. relacijskim izrazima primjerice a ← (x<z) označava da se logičkoj varijabli a pridijeli logička vrijednost određena izjavnom funkcijom (x<z). Kada je x manje od z logička varijabla poprima logičku vrijednost T inače je F. Standardno se koriste relacijski operatori: < (veće), > (manje), ≠ (različito ili nije jednako), ≥ (veće ili jednako), ≤ (manje ili jednako). 13
  • 14.
    Složene logičke izjavenastaju korištenjem sljedećih logičkih operacija: Konjunkcija, a & b, (ili a ∧ b) dviju izjava a i b je je složena izjava, nastala povezivanjem izjava a i b veznikom i za kojeg se upotrebljava simbol ∧ ili &. Složena izjava je istinita samo ako su obje izjave istinite. Izjava a & b čita se "a i b". Disjunkcija, a ∨ b, je složena izjava, koja je lažna onda i samo onda kada su obje izjave lažne; a ∨ b čita se "a ili b". Implikacija, a ⇒ b, je složena izjava koja je lažna onda i samo onda ako je a istinito i b lažno; čita se " a povlači b" ili " a implicira b". Za izjavu b ⇒ a kaže se da je obrat izjave a ⇒ b. Vrijedi i sljedeće tumačenje implikacije: ako je izjava a ⇒ b istinita onda je a dovoljan uvjet za b, ili b je nuždan uvjet za a. Ekvivalencija, a ⇔ b, je složena izjava koja je istinita onda i samo onda kada su obje izjave istinite, ili kada su obje lažne: čita se " a je ekvivalentno sa b". Negacija, ¬a, je izjava koja je istinita onda i samo onda kada je izjava a lažna. Simboli: ¬, &, ∨, ⇔ i ⇒ su logički operatori. Njihovo djelovanje na logičke varijable a i b je prikazano tzv. tablicom istinitosti (tablica 2.1). A b ¬a a & b a ∨ b a ⇒ b a ⇔ b T T F T T T T T F F F T F F F T T F T T F F F T F F T T Tablica 2.1. Tablica istinitosti logičkih operacija Upotrebom logičkih operatora i uvođenjem zagrada mogu se, kao i u algebri, graditi razni logički izrazi, primjerice ¬a ∨ (b & d) ⇒ c. Redoslijed izvršavanja operacija je sljedeći: (1) izraz u zagradi, (2) negacija, (3) disjunkcija, (4) konjunkcija, (5) implikacija i ekvivalencija. Logički izrazi koji sadrže samo operacije negacije, konjunkcije i disjunkcije, te zagrade, određuju Booleovu algebru. Svi se logički izrazi mogu iskazati Booleovom algebrom jer se djelovanje operatora implikacije i ekvivalencije može izraziti pomoću Booleovih izraza. Vrijedi: x ⇒ y = ¬x ∨ y x ⇔ y = ¬((¬x & y) ∨ (¬y & x)) Zadatak: Provjerite prethodne izraze tablicom istinitosti. U Booleovoj algebri vrijede slijedeće zakonitosti: 1. Zakon komutacije x ∨ y ≡ y ∨ x x & y ≡ y & x 2. Zakon asocijacije 14
  • 15.
    x ∨ (y∨ z) ≡ (x ∨ y) ∨ z x & (y & z) ≡ (x & y) & z 3. Zakon idempotentnosti x ∨ x ≡ x x & x ≡ x 4. Zakon distribucije x ∨ (y & z) ≡ (x ∨ y) & (x ∨ z) x & (y ∨ z) ≡ (x & y) ∨ (x & z) 5. De Morganov teorem ¬(x ∨ y) ≡ ¬x & ¬y ¬(x & y) ≡ ¬x ∨ ¬z 6. Zakon dvostruke negacije ¬¬x ≡ x Booleova logika ima veliku primjenu u programiranju i posebno pri projektiranju sklopova digitalnog računala, jer se gotovo svi potrebni sklopovi digitalnog računala mogu realizirati pomoću tri temeljna elektronička sklopa: invertor, sklop-I (eng. AND gate) i sklop-ILI (eng. OR gate). Slika 2.1. Temeljni digitalni sklopovi Ovi se sklopovi upravljaju naponom (ili strujom) tako da reagiraju na stanje pod naponom i stanje bez napona, dakle oni raspoznaju samo dvije naponske razine: nisku i visoku. Uobičajeno se ta dva stanja označavaju s "1" i "0" umjesto s true i false. To su sklopovi kojima izlaz odgovara operacijama negacije, disjunkcije i konjunkcije ulaznih logičkih stanja "0" i "1". Funkcija ovih sklopova se može prikazati pomoću preklopki. Primjerice, rad sklopa I se može opisati strujnim krugom u kojem su serijski spojene žarulja, sklopka A i sklopka B. Žarulja će zasvijetliti kada proteče struja, a to je moguće samo ako ako su obje sklopke uključene, odnosno izlaz je 1 samo ako su varijable A i B jednake 1. Kod sklopa ILI dovoljno je uključiti jednu sklopku da bi zasvijetlila žarulja. Očito sklop I obavlja logičku funkciju konjunkcije, a sklop ILI obavlja logičku funkciju disjunkcije. U digitalnom se računalu pomoću navedenih sklopova obrađuje i prenosi mnoštvo digitalnih signala. Pošto je uvedeno označavanje stanja digitalnog signala znamenkama 0 i 1, može se reći da se digitalnim signalom prenosi poruka o vrijednosti binarne znamenke koja u jednom trenutku 15
  • 16.
    može imati iznosnula ili jedinica. Iz tog se razloga umjesto pojma Booleova algebra ili matematička logika često koristi pojam digitalna logika. U digitalnoj je tehnici uobičajena primjena logičkih operatora na nizove bitova. Tada se podrazumijeva da se logičke operacije provode nad bitovima jednake značajnosti. Takve logičke operacije se nazivaju bit-značajne operacije. Primjer: bit značajnom konjunkcijom dva binarna niza A i B dobije se niz C: 7 6 5 4 3 2 1 0 bit ----------------------- 1 1 0 0 1 1 1 1 = A 0 0 0 0 0 1 0 0 = B ---------------------- A & B = 0 0 0 0 0 1 0 0 = C U nizu C jedino bit 2 može biti jednak 1 i to samo ako je i u nizu A taj bit jednak 1. Ovo je često korišten postupak da se ispita da li je neki bit u nizu jednak 1 ili 0. Obično se niz B naziva "maska" za ispitivanje bitova u nizu A. Pored prije navedenih Booleovih logičkih operacija u digitalnoj se tehnici često koristi bit- značajna operacija koja se naziva ekskluzivna disjunkcija ili ekskluzivno ILI. Označava se znakom ⊕ ili XOR. Ima značaj zbrajanja po modulu 2, a njeno korištenje u programiranju bit će pojašnjeno kasnije. A XOR B = A ⊕ B = (¬A & B) ∨ (A & ¬B) A B A ⊕ B 0 0 0 0 1 1 A ⊕ B = (¬A & B) ∨ (A & ¬B) 1 0 1 1 1 0 Slika 2.2 Definicijska tablica ekskluzivne disjunkcije i simbol digitalnog XOR-sklopa 2.2 Brojevni sustavi i računska sposobnost računala U programskim jezicima operacije s brojevima se najčešće zapisuju u decimalnom brojevnom sustavu, jer je čovjek naviknut na rad s decimalnim brojevima. U računalu se pak računske operacije vrše u binarnom brojevnom sustavu. 2.2.1 Binarni brojevni sustav Sasvim općenito, numerička vrijednost broja Z, koji je u pozicionoj notaciji zapisan znamenkama: zn-1....z1z0, u brojevnom sustavu baze x, računa se prema izrazu: n −1 Z = ( zn −1 .... z1z0 ) x = ∑ zi ⋅ x i i =0 Decimalni brojevni sustav je definiran bazom x=10 i znamenkama zi ε{0,1,2,3,4,5,6,7,8,9}, primjerice iznos broja 765 je jednak 7⋅102 + 6⋅101 + 5⋅100 . 16
  • 17.
    Binarni brojevni sustav je definiran bazom x=2 i binarnim znamenkama zi ∈ {0,1}.Primjerice, iznos binarnog broja 1011 odgovara iznosu broja 11 u decimalnom sustavu, jer je (1011)2 = 1⋅23 + 0⋅22 + 1⋅21 +1⋅20 = 8 + 0 + 2 + 1 = (11)10. Općenito vrijedi da se s binarnim nizom od n bita može kodirati pozitivni cijeli broj maksimalnog iznosa 2n -1, što odgovara broju različitih kombinacija binarnog niza duljine n umanjenom za jedan (i nula je broj!). Za pozitivne cijele brojeve koristi se i nazivi kardinalni brojevi i nepredznačeni cijeli brojevi. U binarnom brojevnom sustavu se mogu izvoditi osnovne računske operacije kao i u decimalnom brojevnom sustavu. Binarno zbrajanje se obavlja kao i decimalno zbrajanje, osim što se prijenos na slijedeće značajnije mjesto ne obavlja nakon zbroja 10, već nakon 2 (1+1). Primjer: 1 1 ← prijenos 1 0 1 = 510 1 1 1 = 710 + 0 1 0 = 210 + 1 0 1 = 510 ----------------- ---------------------- 1 1 1 = 710 1 1 0 0 = 1210 Ukoliko se zbrajanje izvodi bez prijenosa ta operacija se naziva zbrajanje po modulu 2. U logičkom smislu ta operacija je ekvivalentna ekskluzivnoj disjunkciji (XOR). Operaciju zbrajanja LSB bitova može se prikazati tablicom istinitosti 2.2: A B zbroj = A ⊕ B prijenos = A & B 0 0 0 0 0 1 1 0 1 0 1 0 1 1 0 1 Tablica 2.2. Tablica istinitosti za polu-zbrajalo A B Donos Zbroj prijenos 0 0 0 0 0 0 0 1 1 0 0 1 0 1 0 0 1 1 0 1 1 0 0 1 0 1 0 1 0 1 1 1 0 0 1 1 1 1 1 1 Tablica 2.3. Tablica istinitosti za potpuno zbrajalo Digitalni sklop koji realizira ovu funciju naziva se poluzbrajalo (half-adder) i prikazan je na slici 2.3(a). Pri zbrajanju ostalih bitove treba pribrojiti i bit donosa kao u tablici 2.3. Digitalni sklop koji realizira ovu funkciju naziva se potpuno zbrajalo (full-adder). Prikazan je na slici 2.3(b). Očito je da se upotrebom više ovakvih sklopova može "izračunati" zbroj dva binarna niza, na način da se "prijenos" s zbrajala bitova manjeg značaja prenosi kao "donos" u zbrajalo bitova većeg značaja. 17
  • 18.
    Slika 2.3 Sklopovskaizvedba 1-bitnog zbrajala Operacija ekskluzivne disjunkcije (XOR) se često koristi u bit-značajnim operacijama pri šifriranju i u programima s bit-mapiranim grafičkim algoritmima. Interesantno svojstvo ove operacije je da ako se na neki binarni niz A dva puta uzastopno primjeni bit-značajna ekskluzivna disjunkcija s nizom B rezultatni niz je jednak nizu A. Primjerice, neka je niz A= 1010, a niz B=0110. Tada je: A ⊕ B = 1100 (A ⊕ B) ⊕ B = 1010 = A Dakle, prvo djelovanje je šifriranje, a drugo djelovanje je dešifriranje originalnog niza. Oduzimanje broja se može izvesti kao zbrajanje negativne vrijednosti broja. Kako se kodiraju negativni brojevi bit će pokazano kasnije. Binarno množenja se vrši tako da se djelomičan umnožak pomiče za jedno mjesto ulijevo pri svakom uzimanju idućeg množitelja. Ako je množitelj 0, djelomični umnožak je 0, a ako je množitelj 1, djelomični umnožak jednak je množeniku. Primjer: 5 x 5 = 25 5 x 10 = 50 101 (5) 101 (5) 101 (5) 1010 (10) ------------ --------------- 101 000 000 101 101 000 ------------ 101 11001 (25) ---------------- 110010 (50) Binarno dijeljenje se u računalu izvodi primjenom binarnog množenja i oduzimanja, na isti način kao i kod decimalnih brojeva. Navedene operacije su ugrađene u skup naredbi većine današnjih procesora. 18
  • 19.
    Još dvije operacijesu specifične za rad s nizovima bitova. To su operacije logičkog posmaka bitova u lijevo ili u desno (podrazumijeva se LSB na desnoj strani niza), a označavaju se sa SHL (eng. shift left - posmak u lijevo) i SHR (shift right - posmak u desno). Posmak od jednog mjesta u lijevo odgovara množnju kardinalnih brojeva s 2, a posmak bitova jedno mjesto udesno odgovara dijeljenju kardinalnih brojeva s 2. Na prazna mjesta se postavljaju nule. Primjer: 0011 SHL 1 ≡ 0110 odgovara 3 * 2 = 6 0011 SHL 2 ≡ 1100 odgovara 3 * 4 = 12 1110 SHR 1 ≡ 0111 odgovara 14 / 2 = 7 2.2.2 Oktalni i heksadecimalni brojevni sustavi U višim programskim se jezicima rijetko koristi zapis broja u binarnom obliku jer čovjek teško pamti veće nizove "nula i jedinica". Radije se koristi oktalni ili heksadecimalni brojevni sustav. U oktalnom brojevnom sustavu koristi se 8 znamenki: 01234567, a baza brojevnog sustava je x=23=8. Oktalnim brojem jednostavno se označava niz od 3 bita, jer je s binarnim nizom od 3 bita moguće kodirati 8 znamenki oktalnog brojevnog sustava: bit 0 0 1 0 1 0 1 0 1 bit 1 0 0 1 1 0 0 1 1 bit 2 0 0 0 0 1 1 1 1 ----------------------- 0 1 2 3 4 5 6 7 znamenke oktalnog brojevnog sustava To omogućuje pregledniji zapis većih binarnih nizova, primjerice 1001000101112 = 44278, a koristi se pravilo grupiranja po 3 bita: 100=4, 100=4, 010=2, 111=7. U heksadecimalnom brojevnom sustavu koristi se 16 znamenki: 0123456789ABCDEF, a baza brojevnog sustava iznosi x=16. Za kombinacije od 10 do 15 upotrebljena su prva slova abecede, kojima numerička vrijednost u decimalnom brojevom sustavu iznosi: A=10, B=11, C=12, D=13, E=14 i F=15. Heksadecimalnim se brojem jednostavno označava niz od 4 bita, jer se binarnim nizom od 4 bita može kodirati 16 znamenki heksadecimalnog brojevnog sustava: bit 0 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 bit 1 0 0 1 1 0 0 1 1 0 0 1 1 0 0 1 1 bit 2 0 0 0 0 1 1 1 1 0 0 0 0 1 1 1 1 bit 3 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 --------------------------------------- 0 1 2 3 4 5 6 7 8 9 A B C D E F heksadecimalne znamenke To omogućava pregledniji zapis većih binarnih nizova, primjerice: 10010001011111102 = 917E16, a koristi se pravilo grupiranja po 4 bita: 1001=9, 0001=1, 0111=7, 1110=E. U programskim jezicima se uvode posebna leksička pravila za zapis konstanti u pojedinom brojevnom sustavu. Ta pravila će biti opisana u četvrtom poglavlju. 19
  • 20.
    3 Izrada prvogC programa Naglasci: • Izvorni program i izvršni program • Prvi program u C jeziku – hello.c • Kompiliranje programa koji je napisan u više datoteka • Integrirana programska okolina • Makefile U ovom poglavlju opisani su programi koji se koriste u razvoju programa. Njihova upotreba se demonstrira s nekoliko jednostavnih programa u C jeziku. 3.1 Strojni, asemblerski i viši programski jezici Procesor izvršava radnje u računalu na temelju binarno kodiranih strojnih naredbi, koje dobavlja iz memorije računala. Skup svih strojnih naredbi procesora naziva se strojni jezik, a skup naredbi kojima se neposredno izvršava neka zadana operacija u računalu naziva se strojni program. Strojne naredbe je veoma teško pamtiti. Zbog toga se koristi simboličko zapisivanje naredbi asemblerskim jezikom, u kojem se naredbe zapisuju s kraticama riječi iz engleskog jezika. Primjerice, naredba da se broj, koji se nalazi u registru ax procesora Intel 8086, uveća za jedan, glasi: strojni jezik 01000000 asemblerski jezik inc ax (inc je kratica od increment) Dakle, binarni strojni kôd je zamijenjen simboličkim zapisom koje procesor ne razumije. Za prevođenje asemblerskog zapisa u strojni program koristi se program koji se naziva asembler. Viši programski jezici omogućuju jednostavnije programiranje uvođenjem simboličkih naredbi koje zamjenjuju više naredbi strojnog jezika. Primjerice, iskaz C jezika x = sin(3.14) + 7; znači naredbu da se varijabli x pridijeli vrijednost koja se dobije zbrojem vrijednosti funkcije sin(3.14) i konstante 7. Zapis programa asemblerskim ili višim programskim jezikom naziva se izvorni program (eng. source program). Za pisanje izvornog programa koriste se programi za uređivanje teksta koji se nazivaju editori. Izvorni program u C jeziku se obično pohranjuje kao datoteka s imenom koje završava znakovima “.c” Programi za prevođenje izvornog programa u izvršni program mogu biti realizirani kao interpreteri ili kao kompilatori (eng. compiler), a razlika među njima je u načinu kako se izvorni program prevodi u izvršni program. Kompilator analizira izvorni program i stvara strojni kôd, koji pohranjuje u tzv. objektnu datoteku (eng. object file). Kod MS-DOS računala ime objektne datoteke završava s “.obj”, a kod Unix računala s “.o”. Iako objektna datoteka sadrži strojni kôd, on još nije pogodan za izvođenje u računala, naime za izvođenje programa potrebno je odrediti veze s operativnim 20
  • 21.
    sustavom i memorijskelokacije programskih objekata. To se vrši programom koji se zove povezivač ili linker. Nakon obrade s linkerom dobije izvršni ili izvedivi program (eng. executive program), a ako se pohrani kao datoteka onda se ta datoteka naziva izvršna datoteka (eng. executive file). Kod MS-DOS računala ime izvršne datoteke završava slovima “.exe”. Postoje programi u kojima je integrirana funkcija kompilatora i linkera. Primjerice, Microsoft Visual C sadrži program "cl.exe", a na Unix-u se koriste "cc" ili "gcc" programi. Učitavanje izvršne datoteke u memoriju računala vrši program koji se naziva punjač (eng. loader). On je sastavni dio operativnog sustava računala. Korisnik pokreće izvršenje programa tako da otkuca ime programa u komandnoj liniji ili se služi programima s grafičkim sučeljem (primjerice, program Explorer kod Windows-a). Ostale radnje punjača obavlja operativni sustav. Programi se mogu izvršavati i pomoću specijalnih programa koji služe za testiranje izvršenja procesa. Tu spadaju programi koji se nazivaju dibageri (eng. debugger) i monitori. Pomoću njih je moguće izvršavati program u konačno zadanom broju koraka, naredbu po naredbu, i nakon svakog koraka moguće je pratiti stanje varijabli (memorije) i registara procesora. Kod naprednijih dibagera moguće je pratiti izvršenje programa na razini izvornog koda, dok se kod jednostavnijih dibagera i monitora izvršenje programa može pratiti na razini strojnog koda. Interpreter prevodi izvorni kod u niz izvršnih naredbi koje se obično izvršavaju unutar samog programa interpretera. Skup izvršnih naredbi interpretera obično se naziva "virtuelni stroj" (primjerice Java VM ili Microsoft CLR). Program za "interpretiranje", pomoću virtuelnog stroja, može biti i odvojeni program. U tom slučaju interpreter ima funkciju kompilatora koji generira datoteke s nizom naredbi virtuelnog stroja. U razvoju programa koristi se veliki broj programa, tzv. softverskih alata, koji olakšavaju razvoj programa i pisanja dokumentacije. Primjerice, na Unix-u se koriste: make - program za kontrolu procesa kompiliranja, grep - program za pretraživanje datoteka, profiler - program za analizu izvršenja programa, diff – program za utvrđivanje razlika među izvornim datotekama, patch – program za automatsko unošenje izmjena u izvorne datoteke. Kod Windows operativnog sustava više su u upotrebi programi koji se nazivaju integrirana razvojna okolina (eng. IDE – integrated developement environment) kod kojih se pod jedinstvenim grafičkim sučeljem koordinira rad svih faza razvoja programa – editiranje izvornog koda, kompiliranje, dibagiranje i profiliranje koda. U okviru IDE-a integriran je i pristup kompletnoj dokumentaciji kompilatora i programskih biblioteka. Bez sumnje, najpopularniji program ovog tipa je Microsoft Visual Studio. 3.2 Prvi program u C jeziku Gotovo sve knjige o C jeziku kao prvi primjer C-programa uzimaju program: /* Datoteka: hello.c */ /* Prvi C program. */ #include <stdio.h> int main() { printf("Hello world!n"); return 0; } Ovaj program vrši samo jednu radnju; na standardnom izlaznom uređaju ispisuje poruku: Hello World!. 21
  • 22.
    Na slici 3.1prikazan je Windows komandni prozor. On se još naziva MS-DOS prozor ili još jednostavnije komandna konzola. Karakteristika ispisa u konzoli je da se može jedino vršiti ispis teksta, i to u po principu da se ispisuje redak po redak, od vrha konzole na dolje. U konzoli se mogu zadavati komande operativnom sustavu u retku koji uobičajeno započinje s oznakom tekućeg direktorija. Taj redak se naziva komandna linija operativnog sustava. Najprije će biti pokazano kako se može editirati, kompilirati i izvršiti ovaj program, koristeći komandnu liniju operativnog sustava u MSDOS-prozoru (sl. 3.1). To se vrši u tri koraka: 1. Pomoću editora stvara se tekstualna datoteka, imena "hello1.c", koja sadrži prikazani izvorni kôd programa. 2. Pomoću kompilatora se izvorni kôd programa prevodi u objektni, a zatim i u izvršni kôd. Ako kompilator dojavi da u izvornom kôdu postoje leksičke ili sintaktičke pogreške, ponavlja se korak 1 i ispravljaju pogreške. 3. Izvršenje programa se zadaje u komandnoj liniji. Primjer za OS Windows: c:> edit hello.c ↵ - poziv editora edit (↵ je tipka enter) i unos izvornog programa u datoteku hello.c c:> cl hello.c ↵ - poziv kompilatora (Microsoft-program cl.exe) koji stvara izvršnu datoteku hello.exe c:> hello ↵ - komanda za izvršenje programa hello.exe Hello world! - rezultat izvršenja programa Slika 3.1 Izgled komandnog prozora u Windows operativnom sustavu Primjer za OS Linux: $ vi hello.c ↵ - poziv editora vi i unos datoteke hello.c $ gcc hello.c –o hello ↵ - poziv kompilatora gcc, koji stvara izvršnu datoteku hello $ hello ↵ - komanda za izvršenje programa hello Hello world! - rezultat izvršenja programa 22
  • 23.
    Analiza programa "hello1.c": C programi se sastoje od niza potprograma koji se zovu funkcije C-jezika. U programu "hello1.c" definirana je samo jedna funkcija, nazvana main(). Ona mora biti definirana u svakom C programu, jer predstavlja mjesto početka izvršenja programa. Programer može definirati nove funkcije, svaku s jedinstvenim imenom, a mogu se koristiti i prethodno definirane funkcije iz standardne biblioteke funkcija C jezika. Radnje koje obavlja neka funkcija zapisuju se unutar tijela funkcije. Tijelo funkcije je omeđeno vitičastim zagradama. U ovom je slučaju u tijelu funkcije je iskaz koji predstavlja naredbu da se pomoću standardne C funkcije printf(), na standardnoj izlaznoj jedinici, ispiše poruka "Hello World!". Pojedini dijelovi programa "hello1.c" imaju sljedeći značaj: /* Prvi C program. */ Tekst omeđen znakovima /* i */ predstavlja komentar. Kompilator ne analizira komentare, već ih tretira kao umetnuto "prazno" mjesto. #include <stdio.h> #include predstavlja pretprocesorsku direktivu. Ona označava da u proces kompiliranja treba uključiti sadržaj datoteke imena "stdio.h". Ta datoteka sadrži deklaracije funkcija iz standardne biblioteke C-jezika. int main() Ovo je zaglavlje funkcije imena main. int označava tip vrijednosti (cijeli broj) koji vraća funkcija na kraju svog izvršenja (u ovom programu to nema nikakvi značaj). { { označava početak tijela funkcije main. printf("Hello world!n"); Ovo je naredba za poziv standardne funkcije printf(), kojom se ispisuje niz znakova (string) koji je argument ove funkcije. n predstavlja oznaku za prijelaz u novi red ispisa. Znak točka-zarez označava kraj naredbe. Return 0; main() "vraća" vrijednost 0, što se uobičajeno koristi kao oznaka uspješnog završetka programa. } } označava kraja tijela funkcije main. U objašnjenju programskih iskaza korišteni su neki novi pojmovi (deklaracija, standardna biblioteka, pretprocesorska direktiva). Oni će biti objašnjeni u sljedećim poglavljima. Ako program nije napisan u skladu s pravilima jezika, tada kažemo da je program sintaktički pogrešan. Primjerice, ukoliko u prethodnom programu nije otkucana točka-zarez iza naredbe printf("Hello world!n"), kao u programu "hello2.c", 23
  • 24.
    /* Datoteka: hello2.c*/ /* Hello s greškom */ #include <stdio. h> int main() { printf("Hello world!n") /* greška: nema ; */ return 0; } tada kompilator, u ovom slučaju program cl.exe, ispisuje poruku da postoji sintaktička pogreška u sljedećem obliku: C:>cl hello.c Microsoft (R) 32-bit C/C Optimizing Compiler Ver.12.00.8168 for 80x86 Copyright (C) Microsoft Corp 1984-1998. All rights reserved. hello.c hello.c(5) : error C2143: syntax error : missing ';' before 'return' Poruka o greški ima sljedeće elemente: hello.c(5) – obavijest da je greška u retku 5 datoteke "hello.c", error C2143: syntax error - kôd i tip greške, missing ';' before 'return' – kratki opis mogućeg uzroka greške. Na temelju dojave greške često je lako izvršiti potrebne ispravke u programu. Važno je uočiti da je kompilator pronašao grešku u petom retku, iako je pogrešno napisana naredba u četvrtom retku. Razlog tome je pravilo C jezika po kojem se naredba može pisati u više redaka, a stvarni kraj naredbe predstavlja znak točka-zarez. Pošto kompilator nije pronašao točku-zarez u četvrtom retku, kompiliranje je nastavljeno s petim retkom i tek tada je utvrđeno da postoji pogreška. Zadatak: Provjerite da li je sintaktički ispravno napisan sljedeći program: /* Datoteka: hello3.c * Zapis naredbe u više redaka */ #include <stdio. h> int main() { printf ( "Hello world!n" ); return 0; } 24
  • 25.
    3.3 Struktura ikompiliranje C programa Na sličan način, kao u funkciji main(), može se neka druga grupa naredbi definirati kao posebna funkcija s prikladnim imenom. Primjerice, prethodni program se može napisati pomoću dvije funkcije Hello() i main() na sljedeći način: /* Datoteka: hello4.c * Program s korisnički definiranom funkcijom Hello() */ #include <stdio.h> void Hello() { printf("Hello worldn"); } int main() { Hello(); return 0; } Za razumijevanje ovog programa potrebno je upoznati pravila za definiranje i pozivanje funkcija. Funkcija se definira zaglavljem i tijelom funkcije. Zaglavlje funkcije se zapisuje na sljedeći način: ime funkcije se zapisuje nizom znakova koji sadrži slova, znamenke i znak '_', ali uz uvjet da je prvi znak niza slovo ili '_'. Ispred imena funkcije se navodi vrijednost koju funkcija vraća. Ako funkcija ne vraća nikakovu vrijednost tada se ispred imena piše riječ void, koja znači “ništa ili nevažno je”. Ovakve funkcije se nazivaju procedure. U njima se ne mora koristiti naredba return, već one završavaju kada se izvrši posljednje definirana naredba. Iza imena funkcije se, u zagradama, navode formalni argumenti funkcije, ako ih ima. Kasnije će biti objašnjeno kako se definiraju i koriste argumenti funkcije. Tijelo funkcije se zapisuje unutar vitičastih zagrada, a sadrži niz naredbi i deklaracija. Poziv funkcije je naredba za izvršenje funkcije, (tj. za izvršenje naredbi koje su definirane unutar funkcije). Zapisuje se na način da se prvo napiše ime funkcije, a zatim obvezno zagrade i argumenti funkcije, ako su prethodno definirani. Primjerice, u funkciji main() iskaz Hello(); predstavlja poziv funkcije Hello(). Poziv funkcije pokreće izvršenje naredbi koje su definirane u tijelu funkcije Hello(), tj. poziva se funkcija printf() s argumentom "Hello Worldn". Nakon izvršenja te naredbe program se vraća, u funkciju main() na izvršenje prve naredbe koja je napisana iza poziva funkcije Hello(). Funkcija iz koje se pokreće izvršenje pozvane funkcije naziva se pozivna funkcija. U prethodnom primjeru funkcija Hello() je definirana prije funkcije main(). Taj redoslijed je određen pravilom da se funkcija može pozivati samo ako je prethodno definirana. Iznimka od ovog pravila je ako se koriste tzv. prototipovi funkcija (ili unaprijedne deklaracije funkcija). 25
  • 26.
    Prototip ili deklaracijafunkcije je zapis u koji sadrži zaglavlje funkcije i znak točka-zarez. On služi kao najava da je funkcija definirana negdje drugdje; u standardnoj biblioteci ili u programu iza mjesta njenog poziva ili u drugoj datoteci. U skladu s ovim pravilom dozvoljeno je prethodni program pisati u obliku: /* Datoteka: hello5.c: * C program s korisnički definiranom funkcijom Hello() * i prototipom funkcije Hello() */ #include <stdio.h> void Hello(); /* prototip ili deklaracija funkcije Hello()*/ int main() /* definicija funkcije main() */ { Hello(); return 0; } void Hello() /* definicija funkcije Hello() */ { printf("Hello worldn"); } U C jeziku se programi mogu zapisati u više odvojenih datoteka. Primjerice, prethodni program se može zapisati u dvije datoteke "hellomain.c" i "hellosub.c". U datoteci "hellomain.c" definirana je funkcija main() i deklarirana je funkcija Hello(). Definicija funkcije Hello() zapisana je u datoteci "hellosub.c". /* Datoteka: hellomain.c */ /* Datoteka: hellosub.c */ void Hello(); #include <stdio.h> int main() void Hello() { { Hello(); printf("Hello worldn"); return 0; } } Izvršni program se može dobiti komandom: c:> cl hellomain.c hellosub.c /Fe"hello.exe" U komandnoj liniji su zapisana imena datoteka koje treba kompilirati. Zatim je komandnom preklopkom /Fe zapisano da izvršnu datoteku treba formirati pod imenom "hello.exe". 26
  • 27.
    Slika 3.2 Procesformiranja izvršnog programa Proces formiranja izvršnog programa je prikazan na slici 3.2. Najprije leksički pretprocesor kompilatora unosi sve deklaracije iz datoteke "stdio.h" u datoteku "hellosub.c". Zatim kompilator prevodi izvorne datoteke "hellomain.c" i "hellosub.c" u objektne datoteke "hellomain.obj" i "hellosub.obj". U ovim datotekama se uz prijevod izvornog koda u strojni jezik nalaze i podaci o tome kako izvršiti poziv funkcija koje su definirane u drugoj datoteci. Povezivanje strojnog kôda, iz obje datoteke, u zajednički izvršni program obavlja program link.exe, kojeg skriveno poziva program cl.exe. Dobra strana odvajanja programa u više datoteka je da se ne mora uvijek kompilirati sve datoteke, već samo one koje su mijenjane. To se može ostvariti na sljedeći način: Prvo se pomoću preklopke /c kompilatorskom pogonskom programu cl.exe zadaje da izvrši prijevod u objektne datoteke, tj. c:> cl /c hellomain.c c:> cl /c hellosub.c Time se dobiju dvije objektne datoteke: "hellomain.obj" i "hellosub.obj". Povezivanje ovih datoteka u izvršnu datoteku vrši se komandom: c:> cl hellomain.obj hellosub.obj /Fe"hello.exe" Ako se kasnije promijeni izvorni kôd u datoteci "hellomain.c", proces kompiliranja se može ubrzati komandnom: c:> cl hellomain.c hellosub.obj /Fe"hello.exe" jer se na ovaj način prevodi u strojni kôd samo datoteka "hellomain.c", a u procesu formiranja izvršne datoteke koristi se prethodno formirana objektna datoteka "hellosub.obj". 3.4 Integrirana razvojna okolina (IDE) Integrirana razvojna okolina Visual Studio omogućuje editiranje izvornog koda, kompiliranje, linkanje, izvršenje i dibagiranje programa. Sadrži sustav "on-line" dokumentacije o programskom jeziku, standardnim bibliotekama i programskom sučelju prema operativnom sustavu (Win32 API). Pomoću njega se mogu izrađivati programi s grafičkim korisničkim sučeljem i programi za konzolni rad. Nakon poziva programa dobije se IDE Visual Studio prikazan na slici 3.3. 27
  • 28.
    Slika 3.3 IzgledVisual Studio pri pokretanju programa Najprije će biti opisano kako se formira projekt za konzolni tip programa. Prije pokretanja programa, neka se u direktoriju c:My DocumentsC2002pog2 nalaze izvorne datoteke hellomain.c i hellosub.c. Pokretanjem komande menija: File-New-Project, dobije dijalog za postavljanje novog projekta. U dijalogu prikazanom na slici 3.4 označeno je 1. da će projekt biti klase: Win32 Console Application, 2. upisano je ime direktorija d:src 3. gdje će biti zapisana dokumentacija projekta imena hello. U ovom slučaju Visual Studio zapisuje dokumentaciju projekta u datotekama hello.vsproj i hello.sln (datoteka ekstenzije .sln sadrži opis radne okoline, a datoteka ekstenzije .vcproj sadrži opis projekta). Visual Studio također automatski formira dva poddirektorija: .Release i .Debug, u kojima će se nalaziti objektne i izvršne datoteke. (Debug direktorij je predviđen za rad kompilatora u obliku koji je prikladan za pronalaženje grešaka u programu) Pokretanjem komande: Project - Add to project – File, u dijalogu prikazanom na slici 3.9 odabiremo datoteke "hellomain.c" i "hellosub.c", iz direktorija c:My Documentscpp- 2001pog3. Dobije se izgled radne okoline kao na slici 3.10. Pokretanjem komande: Build – Build hello.exe vrši se proces kompiliranja i linkanja. Ako se izvrši bez greške, nastaje izvršni program imena hello.exe. Program hello.exe se može pokrenuti pozivom komande Build – Execute hello.exe (ili pritiskom tipki Ctrl+F5). 28
  • 29.
    Slika 3.4 Dijalogza postavljanje novog projekta Slika 3.5 Dijalog za postavljanje tipa Win32-Console projekta 29
  • 30.
    Slika 3.6 Dijalogkoji izvještava o postavkama novog projekta Slika 3.7 Izgled radne okoline nakon formiranja novog projekta “hello” 30
  • 31.
    Slika 3.9 Dijalogza umetanja datoteka u projekt Slika 3.10 Izgled IDE s aktivnim editorom 31
  • 32.
    3.5 Usmjeravanje procesakompiliranja programom nmake Kada se program sastoji od velikog broja datoteka, procesom kompiliranja se može upravljati pomoću program imena nmake (make na UNIX-u) i specifikacije koja se zapisuje u datoteci koji se obično naziva makefile. Za prethodni primjer datoteka makefile može biti napisana na sljedeći način: # datoteka: makefile #Simbolička definicija za spisak objektnih datoteka OBJS = hellomain.obj hellosub.obj #progam se formira povezivanjem objektnih datoteka: hello.exe : $(OBJS) cl $(OBJS) /Fe"hello.exe" # $(...) znači umetanje prethodnih makro definicija # ovisnost objektnih datoteka o izvornim datotekama # i komanda za stvaranje objektne datoteke hellomain.obj : hellomain.c cl -c hellomain.c hellosub.obj : hellosub.c cl -c hellosub.c Ako se ovu datoteku spremi pod imenom makefile, dovoljno je u komandnoj liniji otkucati: c:> nmake i biti će izvršen cijeli postupak kompiliranja i linkanja izvršnog programa. Ako se pak ova datoteka zapiše pod nekim drugim imenom, primjerice "hello.mak", u komandnoj liniji, iza preklopke –f treba zadati i ime datoteke, tj. c:>nmake –fhello.mak Kako se formira makefile. Temeljna pravila formiranja makefile datoteke su: • Komentari se u makefile zapisuju tako da redak započne znakom #. • Mogu se navesti različite simboličke definicije oblika: IME = text, što omogućuje da na mjestu gdje se piše $(IME) bude supstituiran text. • U makefile se zatim navodi niz definicija koje se sastoje od dva dijela: prvi dio opisuje ovisnost datoteka, a drugi opisuje kao se ta ovisnost realizira. Primjerice, u zapisu hellosub.obj : hellosub.c cl –c hellosub.c • U prvom retku je označena ovisnost sadržaja "hellosub.obj" o sadržaju "hellosub.c". • U drugom retku je specificirana komanda koja se primjenjuje na datoteku "hellosub.c" • Redak u kojem se specificira komanda mora započeti znakom tabulatora Program nmake uspoređuje vrijeme kada su nastale međuovisne datoteke. Čim se promijeni sadržaj "hellosub.c", dolazi do razlike u vremenu nastanka međuovisnih datoteka, pa program nmake pokreće program za kompiliranje koji je specificiran u drugom retku. Korištenje makefile je vrlo popularno, posebno na Unix sustavima i kod profesionalnih programera, jer se pomoću simboličkih definicija lako može definirati proces kompiliranja na različitim operativnim sustavima. 32
  • 33.
    4 Kodiranje itipovi podataka Naglasci: • kodiranje brojeva • kodiranje znakova • kodiranje logičkih vrijednosti • pojam tipa podataka • tipovi konstanti i varijabli u C jeziku • adrese i pokazivači • ispis i unos podataka U ovom se poglavlju se opisuje kako se u računalu kodiraju brojevi i znakovi, objašnjava se koncept tipa podataka i pokazuje karakteristike tipova u C jeziku. 4.1 Kodiranje i zapis podataka Kodiranje je postupak kojim se znakovima, numeričkim vrijednostima i drugim tipovima podataka pridjeljuje dogovorom utvrđena kombinacija binarnih znamenki. Ovdje će biti opisano kodiranje koje se koristi u C jeziku. S programerskog stajališta, važnije od samog načina kodiranja je veličina zauzeća memorije i interval vrijednosti koji se postiže kodiranjem. Također, važno je upoznati leksička pravila po kojima se zapisuju znakovne i numeričke konstante u "literalnom" obliku. 4.1.1 Kodiranje pozitivnih cijelih brojeva (eng. unsigned integers) Pozitivni cijeli brojevi (eng. unsigned integers), ili kardinalni brojevi, su brojevi iz skupa kojeg čine prirodni brojevi i nula. Način njihovog kodiranja je opisan u poglavlju 2. U C jeziku se literalne konstante, koje predstavljaju pozitivne cijele brojeve, mogu zapisati u decimalnom, heksadecimalnom i oktalnom brojevnom sustavu, prema slijedećem leksičkom pravilu: • niz decimalnih znamenki označava decimalnu konstantu ukoliko prva znamenka nije nula. • niz oktalnih znamenki označava oktalnu konstantu ako je prva znamenka jednaka nuli. • niz heksadecimalnih znamenki, kojem prethodi prefix 0x ili 0X, označava heksadecimalnu konstantu. Primjer: tri ekvivalentna literalna zapisa vrijednosti binarnog niza 011011 u C jeziku su: decimalna konstanta 27 oktalna konstanta 033 heksadecimalna konstanta 0x1B 4.1.2 Kodiranje cijelih brojeva (eng. integers) Cijeli brojevi (eng. integers) su brojevi iz skupa kojeg čine prirodni brojevi, negativni prirodni brojevi i nula, ili drukčije kazano, to su brojevi s predznakom (eng. signed integers). Većina današnjih procesora za kodiranje cijelih brojeva s predznakom koristi tzv. komplementni 33
  • 34.
    brojevni sustav. Punikomplement n-znamenkastog broja Nx, u brojevnom sustavu baze x matematički se definira izrazom: N x = xn − N x ′ primjerice, u decimalnom sustavu komplement troznamenkastog broja 733 je 103-733= 277. Ako se n-znamenkasti broj i njegov komplement zbroje vrijedi da će n znamenki biti jednako nuli. U prijašnjem primjeru 733+277=1000, dakle tri znamenke su jednake nuli. U binarnom se sustavu puni komplement naziva komplement dvojke i vrijedi: N 2 = 2n − N 2 ′ Komplement dvojke se koristi za označavanje negativnih brojeva. Primjerice, komplement dvojke broja +1 za n=4 iznosi: ′ N 2 = 2 4 − 1 = 10000 − 0001 = 1111 . Ako se broj i njegov komplement zbroje, rezultat treba biti nula. To vrijedi, u prethodnom primjeru, jer su prva četiri bita zbroja jednaka nuli. Peti bit je jednak jedinici, ali on se u 4- bitnom sustavu odbacuje. U sustavu komplementa dvojke pozitivni brojevi uvijek imaju MSB=0, a negativni brojevi imaju MSB=1. Što se događa ako se zbroje dva pozitivna broja koji imaju bitove ispod MSB jednake jedinici. Primjerice, ako zbrojimo 4+5 ( u 4-bitnom sustavu) 0100 +0101 ----- 1001 dobije se rezultat koji predstavlja negativan broj u sustavu komplementa dvojke. Do prijelaza u područje komplementa ne bi došlo da je rezultat zbrajanja bio manji od 7, odnosno 24-1-1. Poopći li se prethodno zapažanje na brojeve od n-bita, može se zaključiti da operacija zbrajanja ima smisla samo ako je zbroj operanada manji od 2n-1-1. Zbog toga, najveći pozitivni broj koji se može predstaviti u sustavu komplementa dvojke iznosi: max_int = (0111...111) = 2n-1-1, a najveći iznos negativnog broja iznosi: min_int = (1000...000) = -2n-1. Uočite da postoji za jedan više negativnih brojeva od pozitivnih brojeva. Obični komplement binarnog broja (naziva se i komplement jedinice) dobije se zamjenom svih jedinica s nulom i obratno. Iznos broja. koji se dobije na ovaj način, računa se prema izrazu: N 2 = 2n − N 2 − 1 Obični komplement nije pogodan za izražavanje prirodnih brojeva jer nije jednoznačno određena vrijednost nule, naime obični komplement od 0000 iznosi 1111. On služi za jednostavno izračunavanje punog komplementa, jer vrijedi: N2 = N2 + 1 ′ 34
  • 35.
    Puni komplement seizračunava tako da se običnom komplementu pribroji jedinica, primjerice, komplement dvojke broja 6 u 8-bitnoj notaciji iznosi: 00000110 (+6) -------- 11111001 (obični komplement od +6) 1 (dodaj 1) -------- 11111010 (-6 u komplementu dvojke) Izračunajmo puni komplement od 0: 00000000 (0) -------- 11111111 (komplement od 0) 1 (dodaj 1) -------- 100000000 (-0 u komplementu dvojke) Jedinica predstavlja deveti bit. Ona se u 8-bitnom sustavu odbacuje pa rezultat opet predstavlja nulu. Komplement dvojke omogućuje jednoznačno određivanje nule, pa je podesan za kodiranje cijelih brojeva. 4.1.3. Kodiranje realnih brojeva Realni brojevi se u matematici zapisuju na način da se cijeli i decimalni dio odvoje decimalnim zarezom (pr. 67,098), a koristi se i ekponentni format (eng. scientific format). Primjerice, prethodni se broj može zapisati u obliku 0,67089⋅102. U programskom jeziku C koristi se sličan zapis kao u matematici, s razlikom da se umjesto decimalnog zareza koristi "decimalna točka", a potencija broja 10 se označava velikim ili malim slovom E. matematički zapis ekvivalentni zapis u C-jeziku 1,789 1.789 0,789 0.789 ili .789 -178,9⋅10-2 -178.9e-2 ili -178.9E-2 -0,01789⋅102 -0.01789e2 ili -0.01789E2 ili -0.01789e+2 Tablica 4.1. Matematički i programski zapis realnih brojeva Eksponentni format se sastoji od dva dijela: mantise i eksponenta eksponentni decimalni format = mantisa 10eksponent Mantisa se zapisuje kao obični decimalni broj s predznakom, a eksponent se zapisuje kao cijeli broj. Prednost korištenja eksponentnog formata je u lakšem zapisu vrlo velikih i vrlo malih brojeva. Uočite da se promjenom vrijednosti eksponenta pomiče položaj decimalnog zareza. Kodiranje s fiksnim položajem binarne točke (eng. fixed point numbers) Umjesto pojma decimalnog zareza uvodi se pojam binarne točke. Opći oblik zapisivanja realnog broja s fiksnim položajem binarne točke, u slučaju da se N znamenki koristi za 35
  • 36.
    označavanje cijelih vrijednosti,a n znamenki za označavanje razlomljenih vrijednosti (po bazi 2: 1/2, 1/4, 1/8 itd.) glasi: bN −1bN − 2 ...b0 • b−1b− 2 ...b− n , a iznos mu se računa prema izrazu: N .n2 = bN −1 2 N −1 + ... + b0 2 0 + b−1 2 −1 + b− 2 2 −2 + ... + b− n 2 − n , bi ∈ (0,1) Ako se ovaj broj pomnoži s 2n može ga se u operacijama smatrati cijelim brojem, a nakon izvršenih aritmetičkih operacija rezultat se skalira za iznos 2-n. Ovaj oblik kodiranja ima brojne nedostatke, i koristi se samo u izuzetnim slučajevima. Kodiranje s pomičnim položajem binarne točke (eng. floating point numbers) Ideja eksponentnog formata uzeta je kao temelj za kodiranje realnih brojeva i u binarnom brojevnom sustavu. Kodiranje se vrši prema izrazu F = ( − 1) m 2 e s gdje m predstavlja mantisu, e je eksponent dvojke, a s∈(0,1) određuje predznak broja. Eksponent i mantisa se kodiraju u binarnom brojevnom sustavu. Eksponent se kodira kao cijeli broj, a mantisa kao binarni broj s fiksnim položajem binarne točke. Ideja je jednostavna: promjenom eksponenta pomiče se i položaj binarne točke iako se mantisa zapisuje s fiksnim položajem binarne točke. Primjerice, neka je broj kodiran u obliku: 0.00001xxxxxxxxx 2e gdje x može biti 0 ili 1. Ovaj oblik zapisa realnog broja naziva se nenormalizirani zapis. Pomakne li se položaj binarne točke za 5 mjesta ulijevo dobije se ekvivalentni zapis 1.xxxxxxxxx00000 2e-5, Posmak bitova ulijevo ekvivalentan je dijeljenju s dva, stoga se vrijednost eksponenta smanjuje za 5. Ovakav kodni zapis, u kojem je uvijek jedinica na prvom mjestu, naziva se normalizirani zapis. Značaj normaliziranog zapisa je u činjenici što se njime iskorištavaju svi bitovi mantise za kodiranje vrijednosti, dakle osigurava se veća točnost zapisa. Normaliziranim se oblikom ipak ne može kodirati veoma male vrijednosti, pa je tada pogodniji nenormalizirani zapis broja. Treba pojasniti i kako je kodirana vrijednost nula. Pogodno bi bilo da sva bitna polja pri kodiranju vrijednosti nula budu jednaka nuli (zbog logičkih operacija), ali pošto se za kodiranje eksponenta također koristi binarni zapis, vrijednost eksponenta nula se matematički koristi za označavanje brojeva većih od jedinice. Da bi se zadovoljilo zahtjevu kodiranja nule s nultim zapisom eksponenta uobičajeno je da se umjesto stvarne vrijednosti eksponenta kodira vrijednost: E = e + pomak, gdje je pomak neka konstantna vrijednost, a odabire se na način da je jednak najnižoj vrijednosti eksponenta e, koji je negativna vrijednost Ovako zapisani eksponent naziva se pomaknuti eksponent. Značaj pomaka pokazuje sljedeći primjer. Neka je eksponent opisan s 8 bita. Tada se E kreće u rasponu od 0 do 255. Ako se uzme da je pomak=127, i da je E=0 rezervirano za kodiranje nule, onda se vrijednost binarnog eksponenta kreće u rasponu od -126 do +127. 36
  • 37.
    Postoji više različitihformata zapisa mantise i eksponenta u binarnom kodu. Danas se gotovo isključivo koristi format koji je određen ANSI/IEEE standardom br.745 iz 1985. godine. Prema tom standardu koriste se dva tipa kodiranja: jednostruki format (32 bita) i dvostruki format (64 bita). Slika 4.1 Format kodiranja realnih brojeva prema IEEE/ANSI standardu STANDARDNI IEEE/ANSI FORMAT REALNIH BROJEVA parametar Jednostruki (SINGLE) dvostruki (DOUBLE) ukupan broj bita 32 (+1) 64 (+1) broj bita eksponenta 8 11 broj bita za predznak 1 1 broj bita mantise 23 (+1) 52 (+1) pomak +127 +1023 Emax 255 2047 Emin 0 0 minreal (za nenorm.) (-1)s⋅1.4⋅10-45 (-1)s⋅2.225⋅10-324 minreal (za norm.) (-1)s⋅1.175⋅10-38 (-1)s⋅2.225⋅10-308 maxreal (-1)s⋅3.4028⋅10+38 (-1)s⋅1.797⋅10+308 Tablica 4.2. Standardni IEEE/ANSI format realnih brojeva Bitna karakteristika ovog standarda je da je u format za kodiranje realnog broja moguće upisati i šifru o ispravno obavljenoj matematičkoj operaciji (pr. dijeljenje s nulom dalo bi beskonačnu vrijednost, koju je nemoguće kodirati, pa se ta operacija izvještava kao greška). Binarno kodirani signal greške koristi format binarno kodiranih realnih brojeva, ali kako to nije broj, u standardu se opisuje pod nazivom NaN (Not a Number). Kodiranje s normaliziranim zapisom mantise je izvršeno na način da se ne upisuje prva jedinica, čime se podrazumjeva da je mantisa kodirana s jednim bitom više nego je to predviđeno u binarnom zapisu. Vrijednost pomaka i raspona eksponenta dana je tablicom 3.2. Vrijednost eksponenta Emin je iskorištena za kodiranje nule, a vrijednost Emax za kodiranje NaN-a i beskonačnosti. Zapis formata se interpretira na sljedeći način: 1. Ako je E=Emax i m≠0 kodna riječ predstavlja NaN, bez obzira na vrijedost predznaka s. 2. Ako je E=Emax i m=0 kodna riječ predstavlja (-1)s (∝). 3. Ako je Emin<E<Emax kodna riječ predstavlja broj (-1)s2e-127(1.m), tj. predstavlja normalizirani realni broj. 4. Ako je E=0 i m=0 kodna riječ predstavlja broj (-1)s(0). 5. Ako je E=0 i m≠0 kodna riječ predstavlja broj (-1)s2ee-127(0.m) tj. predstavlja nenormalizirani realni broj (vodeća nula se ne zapisuje). Opis nenormaliziranim brojevima ne osigurava točnost za sve brojeve pa se ovo kodiranje u nekim implementacija ne koristi. Vrijednosti za minimalnu i maksimalnu vrijednost realnog broja u tablici 3.2. dani su za normalizirani i nenormalizirani format realnog broja. 37
  • 38.
    Programer, koji programirau višem programskom jeziku, ne mora znati kako se neki broj kodira u procesoru ili memoriji računala. Njega zanimaju pravila za zapis literalnih konstanti, veličina zauzeća memorije, maksimalna i minimalna vrijednost broja, te broj točnih decimala. Normaliziranim zapisom postiže se točnost na 7 decimala za jednostruki format, odnosno 15 decimala za prošireni format. 4.1.4. Kodiranje znakova Znak (eng. character) je element dogovorno usvojenog skupa različitih simbola koji su namijenjeni obradi podataka (slova abecede, numeričke znamenke, znakovi interpunkcije i sl.). Niz znakova se često tretira kao cjelina i naziva se string. Primjerice, u C jeziku se string literalno zapisuje kao niz znakova omeđen znakom navodnika ("ovo je literalni zapis C stringa"). Za kodiranje slova, znamenki i ostalih tzv. kontrolnih znakova koristi se američki standard za izmjenu informacija ASCII (American Standard Code for Information Interchange). Njemu je ekvivalentan međunarodni standard ISO 7. ASCII kôd se najviše koristi za komuniciranje između računala i priključenih vanjskih jedinica: pisača, crtača, modema, terminala itd. To je sedam bitni kôd (ukupno 128 različitih znakova), od čega se prva 32 znaka koriste kao kontrolni znakovi za različite namjene, a ostali znakovi predstavljaju slova abecede, pravopisne i matematičke simbole. 000: (nul) 016: (dle) 032: (sp) 048: 0 064: @ 080: P 096: ž 112: p 001: (soh) 017: (dc1) 033: ! 049: 1 065: A 081: Q 097: a 113: q 002: (stx) 018: (dc2) 034: " 050: 2 066: B 082: R 098: b 114: r 003: (etx) 019: (dc3) 035: # 051: 3 067: C 083: S 099: c 115: s 004: (eot) 020: (dc4) 036: $ 052: 4 068: D 084: T 100: d 116: t 005: (enq) 021: (nak) 037: % 053: 5 069: E 085: U 101: e 117: u 006: (ack) 022: (syn) 038: & 054: 6 070: F 086: V 102: f 118: v 007: (bel) 023: (etb) 039: ' 055: 7 071: G 087: W 103: g 119: w 008: (bs) 024: (can) 040: ( 056: 8 072: H 088: X 104: h 120: x 009: (tab) 025: (em) 041: ) 057: 9 073: I 089: Y 105: i 121: y 010: (lf) 026: (eof) 042: * 058: : 074; J 090: Z 106: j 122: z 011: (vt) 027: (esc) 043: + 059: ; 075: K 091: [ 107: k 123: { 012: (np) 028: (fs) 044: , 060: < 076: L 092: 108: l 124: | 013: (cr) 029: (gs) 045: - 061: = 077: M 093: ] 109: m 125: } 014: (so) 030: (rs) 046: . 062: > 078: N 094: ^ 110: n 126: ~ 015: (si) 031: (us) 047: / 063: ? 079: O 095: _ 111: o 127: del Tablica 4.3. ACSII standard za kodiranje znakova U Hrvatskoj se za latinična slova Č,Š,Ž,Ć i Đ koristi modifikacija ASCII standarda. (tablica 4.4). HR-ASCII IBM EBCDIC-852 standard dec hex ASCII HR-ASCII Dec hex EBCDIC-852 64 40 @ Ž 166 A6 Ž 91 5B [ Š 230 E6 Š 92 5C Đ 209 D1 Đ 93 5D ] Ć 172 AC Ć 94 5E ^ Č 143 8F Č 96 60 ` ž 167 A7 ž 123 7B { š 231 E7 š 124 7C | đ 208 D0 đ 125 7D } ć 134 86 ć 126 7E ~ č 159 9F č Tablica 4.4. HR_ASCII i EBCDIC-852 standard 38
  • 39.
    Na IBM PCračunalima se koristi 8-bitno kodiranje znakova, čime je omogućeno kodiranje 256 znakova. Ovaj kod se naziva EBCDIC (Extended Binary Coded Decimal Interchange Code). Prvih 128 znakova ovog koda jednaki su ASCII standardu, a ostali znakovi predstavljaju različite grafičke i matematičke simbole te slova koja se koriste u alfabetu većine zapadno- evropskih zemalja, a nisu obuhvaćena ASCII standardom. U sklopu tog standarda predviđeno je da se znakovi Č,Š,Ž,Ć,Đ,č,ć,ž,š,đ kodiraju vrijednostima većim od 127. Taj standard ima oznaku EBCDIC-852 i prikazan je u tablici 4.4. U Windows operativnom sustavu, koji koristi grafičko sučelje, koristi se poseban tip kodiranja, gdje se uz kôd znakova zapisuje i oblik znaka (font). Sve se više koristi prošireni skup znakova koji je određen 16-bitnim Unicode standardom. Njime su obuhvaćeni znakovi iz svih svjetskih jezika. U C jeziku se imena varijabli i funkcija označavaju nizom znakova, stoga se za označavanje znakovne konstante koriste jednostruki navodnici, primjerice u C iskazu: a = 'a'; slovo a se koristi za označavanje varijable s lijeve strane naredbe pridjele vrijednosti, a s desne strane je napisana znakovna konstanta 'a'. Ovom se naredbom se varijabli a pridjeljuje ASCII vrijednost znakovne konstante 'a', odnosno numerička vrijednost 97. Potrebno je razlikovati sljedeće iskaze: 1) a = 8; 2) a = '8'; U prvom iskazu varijabla a ima numeričku vrijednost 8, a u drugom iskazu varijabla a ima numeričku vrijednost 56, jer se tada varijabli pridjeljuje numerička ASCII vrijednost znaka '8'. U samom procesu programiranja uglavnom nije nužno znati numeričku vrijednost znakovne konstante, već se tada apstraktno uzima da je varijabli a pridijeljen znak '8'. Neki znakovi iz ASCII skupa se ne mogu zapisati u izvornom kodu, jer za njih ne postoji oznaka na standardnoj tipkovnici. Za slučaj da se pak želi raditi i s takvim znakovima , za njihov unos u C jeziku se koriste specijalne kontrolne sekvence (tzv. escape sequence), koje se zapisuju na način da se napiše obrnuta kosa crta i jedan od sljedećih znakova. b oznaka za povrat unatrag - backspace BS f oznaka za stranicu unaprijed - form feed FF n oznaka nove linije - new line NL r oznaka za povrat na početak reda CR t oznaka za horizontalni tab HT v oznaka za vertikalni tab ” oznaka za dvostruki navodnik ' oznaka za jednostruki navodnik oznaka za znak - backslash ooo ASCII kôd znaka je zapisan oktalnim znamenkama ooo. 0xhh ASCII kôd znak je zapisan heksadecimalnim znamenkama hh. Primjerice, ekvivalentna su tri zapisa znaka A: 'A', '0x41', '101' Znakovne konstante su u C jeziku zapisuju unutar jednostrukih zagrada znakovima iz ASCII skupa (ili kontrolnom sekvencom). Može ih se tretirati i kao numeričke 39
  • 40.
    vrijednosti cjelobrojnog tipana način da je vrijednost znakovne konstante jednaka ASCII kodu znaka. Kada se u C jeziku koriste literalne konstante, koje predstavljaju string (niz znakova), one se zapisuju unutar dvostrukih navodnika, primjerice "Hello World!n". Unutar stringa se kontrolni znakovi zapisuju bez navodnika. U ovom primjeru, na kraju stringa je zapisan znak za prijelaz u novu liniju. 4.1.5. Kodiranje logičkih vrijednosti Moguće su samo su dvije logičke vrijednosti: istina i laž. U većini programskih jezika (C++, Pascal) ove vrijednosti se zapisuju s “true” i “false”. U C jeziku se ne koristi posebno označavanje logičkih vrijednosti, već vrijedi pravilo da se svaka numerička vrijednost može tretirati kao logička “istina” ako je različita od nule, odnosno “laž” ako je jednaka nuli. Logičke vrijednosti nastaju i kao rezultat relacijskih izraza u kojima se koriste operatori: > (veće), < (manje), >= (veće ili jednako) ,<= (manje ili jednako), == (jednako) i != (nije jednako). U C jeziku, ako je vrijednost relacijskog izraza logička vrijednost “istina” ona se kodira kao numerička vrijednost 1, a logička vrijednost “laž” se kodira kao numerička vrijednost 0. Primjerice u izrazu a = x > y; varijabli a se pridjeljuje vrijednost 1 ako je izraz na desnoj strani istinit (tj. ako je x veće od y), u suprotnome, pridjeljuje se vrijednost 0. Relacijski se odnos dvije vrijednosti pri izvršavanju programa zapravo određuje pomoću operacije oduzimanja, a definiran je u tablici 4.5. Ako je x == y x != y x > y x < y x <= y x >= y x-y > 0 0 1 1 0 0 1 x-y = 0 1 0 0 0 1 1 x-y < 0 0 1 0 1 1 0 Tablica 4.5 Rezultat izvršenja relacijskih operacija u C jeziku 4.2 Memorija Prije nego se pokaže kako se na apstraktnoj razini manipulira s podacima, koji se nalaze u radnoj memoriji računala, potrebno je upoznati neke hardverske karakteristike radne memorije računala. Elektronička memorijska jedinica može pamtiti 1 bit informacije. Više memorijskih jedinica se pakira u integrirani elektronički sklop koji se popularno naziva čip. U računalu se memorijski čipovi spajaju na način da se istovremeno može pristupiti grupi od 8 ili više temeljnih memorijskih jedinica, pa se sa softverskog stajališta može uzeti da jedna memorijska ćelija sadrži 1 bajt digitalne informacije. Informacije se između procesora i memorije ne prenose serijski, bit po bit, već se vrši istovremeni prijenos više digitalnih znamenki (8, 16, 32 ili 64 bita) pomoću višestrukih električnih vodova, koje se obično naziva sabirnica (eng. bus). Postoje tri tipa sabirnice: adresna sabirnica služi za aktiviranje memorijske ćelije, podatkovna sabirnica služi za prijenos podatka koji će biti spremljen (zapisan) ili dobavljen (pročitan) iz te memorijske ćelije i kontrolna sabirnica kojom se određuje da li i kako se vrši čitanje ili pisanje. Na slici 3.2. prikazana je pojednostavljena shema spajanja memorijskih čipova. Stanje upisivanja ili čitanja sadržaja memorijske grupe (8,16,32 ili 64 bita) kontrolira se signalom mem 40
  • 41.
    r/w (memory read/write).Usklađenost rada s ostalim sklopovima računala osigurava se na način da se vrijeme upisivanja ili čitanja iz memorije kontrolira posebnim signalima generatora takta. Aktiviranje jedne memorijske grupe određeno je binarnom kombinacijom naponskih stanje adresne sabirnice. Memorija je, stoga, linearno uređen prostor, jer je položaj u memoriji, tj. adresa, proporcionalan numeričkoj vrijednosti binarne kombinacije adresne sabirnice. Slika 4.2. Pojednostavljeni prikaz sheme spajanja memorijskih čipova Potrebno je uočiti da svakom podatku u memoriji su pridjeljene dvije vrijednosti: adresa i sadržaj memorije na toj adresi. Veličina se adresne sabirnice i sabirnice podataka iskazuje brojem bita koje oni u jednom trenutku prenose i određena je mogućnostima procesora i hardverskom strukturom računala. Maksimalni memorijski kapacitet je određen izrazom: Maksimalni memorijski kapacitet = 2veličina adresne sabirnice (bajta) Tzv. 8-bitni mikroprocesori (Z-80, Motorola 6800, Intel 8080) imaju 16-bitnu adresnu sabirnicu i 8-bitnu sabirnicu podataka, pa im maksimalni memorijski kapacitet iznosi 64 kilobajta. Prva personalna računala klase IBM PC XT imala su adresnu sabirnica od 20 linija i 8-bitnu sabirnica podataka, pa je maksimalni kapacitet memorije tih računala iznosio 1M. Kod današnjih personalnih računala koriste se 32-bitne sabirnice (kod većih računala koriste se 64- bitne sabirnice podataka) pa maksimalni kapacitet memorije može biti 4GB. Realno se u računala ugrađuje znatno manje memorije (64MB-1GB). Na temelju onoga što je do sada kazano o kodiranju sadržaja i hardverskom ustrojstvu memorije, sa programerskog stajališta, važne su sljedeće činjenice i definicije: 1. Podaci, koji se nalaze na nekoj adresi u memoriji, nazivaju se memorijski objekti. 2. Adresa se opisuje kardinalnim brojem koji opisuje položaj temeljne memorijske ćelije, veličine 8 bita, u linearno uređenom prostoru memorije. 3. Podaci su najčešće kodirani tako da pripadaju skupu numeričkih vrijednosti ili skupu znakova koji su kodirani prema ASCII standardu, ili im se pridodaje logička vrijednost {true, false}. 4. Memorijskim objektima procesor može mijenjati sadržaj i s njima obavljati aritmetičko- logičke operacije. 5. Memorijski objekti, s kojima procesor operira kao s cjelinom, nazivaju se varijable. 41
  • 42.
    4.3 Prosti tipovipodataka U uvodu je naglašeno da ime varijable simbolički označava položaj u memoriji (adresu) na kojem je upisana vrijednost varijable. Sada ćemo uz pojam varijable uvesti i pojam tipa varijable. U matematici je uobičajeno uz matematičke izraze navesti i skup vrijednosti varijabli za koje ti izrazi vrijede. Primjerice, neka varijabla x pripada skupu realnih brojeva (x ∈ R), a varijabla z skupu kompleksnih brojeva (z ∈ Z). Tada vrijedi: z Re( z) Im( z) = + j x x x Vrijednost ovog izraza također pripada skupu kompleksnih brojeva. Bitno je uočiti da oznaka pripadnosti skupu vrijednosti određuje način kako se računa matematički izraz i kojem skupu vrijednosti pripada rezultat izraza. U programskim se jezicima pripadnost skupu koji ima zajedničke karakteristike naziva tipom. Tip varijable određuje način kodiranja, veličinu zauzimanja memorije i operacije koje su sa tom varijablom dozvoljene. Tipovi varijabli (ili konstanti) koji se koriste u izrazima određuju tip kojim rezultira taj izraz. U većini programskih jezika se koriste sljedeći tipovi podataka: • numerički (cjelobrojni ili realni), • logički, • znakovni, a nazivaju se i primitivni ili prosti tipovi jer predstavljaju nedjeljive objekte koji imaju izravan prikaz u memoriji računala. Tip Oznaka u Interval zauzeće C jeziku vrijednosti Memorije znakovni [signed] char -127 .. 128 1 bajt tip Unsigned char 0 .. 255 1 bajt cjelobrojni [signed] int -2147483648.. 2147483647 4 bajta tip [signed] short -32768 .. 32767 2 bajta [signed] long -2147483648.. 2147483647 4 bajta kardinalni unsigned [int] 0 .. 4294967295 4 bajta tip unsigned short 0 .. 65535 2 bajta unsigned long 0 .. 4294967295 4 bajta realni tip min ± 1.175494351e-38 (jednostruk float maks ± 3.402823466e+38 4 bajta i format) realni tip double min ± 2.2250738585072014e-308 8 bajta (dvostruki maks ± 1.7976931348623158e+308 format) logički tip - 0 .. različito od nule - Tablica 4.6. Označavanje standardnih tipova podataka u C jeziku (uglate zagrade označavaju opcioni element) Tablica 4.6 prikazuje karakteristike standardnih tipove podataka koji se koriste u C jeziku; ime kojim se označavaju, interval vrijednosti i veličina zauzeća memorije u bajtima. Uglate zagrade označavaju opciona imena tipova, primjerice može se pisati int ili signed int unsigned ili unsigned int 42
  • 43.
    Oznake tipova suuzete iz leksike engleskog jezika: int je kratica leksema integer (cijeli broj), char je kratica leksema character (znak), signed je pridjev koji označava skup cijelih brojeva u kojem se koristi predznak (+ ili -), a unsigned je pridjev koji označava da se iz skupa cijelih brojeva koriste samo brojevi bez predznaka (kardinalni brojevi), float je kratica od floating point (realni broj kodiran prema IEEE standardu jednostrukog formata), a double znači dvostruko u smislu dvostruko preciznog IEEE formata zapisa realnih brojeva. Uz pojam varijable uvijek su vezani pojmovi: tip i ime. Ime predstavlja oznaku adrese varijable u memoriji, pa se zove i referenca varijable, ili referenca memorijskog objekta. Tip označava skup vrijednosti varijable, veličinu zauzeća memorije, način kodiranja i operacije koje se mogu primijeniti. Pridjeljivanje oznake tipa nekoj varijabli naziva se deklaracija varijable, a ako se pod tom deklaracijom podrazumijeva i rezerviranje memorijskog prostora u kojem će biti smještena vrijednost varijable, onda se kaže da je deklaracijom izvršena i definicija varijable. U C jeziku uvijek mora biti deklariran tip varijable prije nego sa ona upotrijebi u programskim iskazima. Primjer: Program, imena povrsina.c, služi za proračun površine kruga. U njemu se koriste se dvije varijable tipa double: r označava radijus, a P površinu kruga. Pretpostavlja se da je radijus poznat i neka iznosi 2.1 m. Rezultat proračuna će biti ispisan pomoću printf() funkcije. Evo kako je napisan taj program. /* Datoteka: povrsina.c */ /* Program koji računa površinu kruga radijusa 2.1m */ #include <stdio.h> int main() { double r, P; /* deklaracija varijabli r i P */ r = 2.1; /* zadana vrijednost radijusa 2.1m */ P = r*r*3.14; /* proračun površine kruga */ printf( "n Povrsina kruga = %f m", P); return 0; } Kada se program kompilira i izvrši dobije se ispis: Povrsina kruga = 13.847400 m Komentarima je opisan značaj pojedinog programskog iskaza. Najprije je izvršena deklaracija varijabli r i P, prema pravilu da se oznaka tipa napiše ispred imena varijabli (ako se istovremeno deklarira više varijabli istoga tipa, njihova se imena odvajaju zarezom). Zapis deklaracije završava znakom točka-zarez. Nadalje, naredbom pridjele vrijednosti postavljena je vrijednost varijable r na zadanu vrijednost 2.1. Proračun površine je izvršen prema izrazu 3.14*r*r, gdje zvjezdica označava operator množenja. U matematici formula za površinu glasi r2π. Ovu formulu se ne može izravno primijeniti jer u C jeziku ne postoji operator potenciranja. 43
  • 44.
    Rezultat proračuna jepridijeljen varijabli P. Za ispis vrijednosti te varijable koristi se standardna funkcija printf(). Potrebno je uočiti razliku od zapisa printf() funkcije koji je korišten u programu Hello.c. U ovom slučaju funkcija printf() koristi dva argumenta. Prvi argument je literalni string, a drugi argument je varijabla P čiju vrijednost treba ispisati. Položaj na kojem će se ispisati ta vrijednost označen je unutar literalnog stringa oznakom %d. Ova oznaka se naziva specifikator ispisa. Argumenti su odvojeni zarezom. Opći oblik korištenja printf funkcije je: printf(format_ispisa, lista_argumenata) gdje lista_argumenata sadrži nula ili više argumenata funkcije odvojenih zarezom, a položaj gdje će biti izvršen ispis vrijednosti tih argumenta označava se u stringu format_ispisa specifikatorom ispisa. Broj specifikatora ispisa mora biti jednak broju argumenata funkcije koji su navedeni u lista_argumenata. Specifikatori ispisa moraju odgovarati tipu argumenta, primjerice za cjelobrojne argumente ( tip: int, short, long) oznaka je %d, za znakovne argumente (tip char) oznaka je %c, za realne argumente (tip: float, double) oznaka je %f ili %g, a za ispis stringa oznaka je %s. Argumenti funkcije mogu biti imena varijabli, konstante i izrazi koji rezultiraju nekom vrijednošću. Sada će sa nekoliko programa biti pokazane karakteristike standardnih tipova C jezika. Prvi program sizeof.c ispisuje koliko pojedini tip zauzima memorije. /* Datoteka: sizeof.c */ /* Program ispisuje zauzeće memorije za sve proste tipove C jezika */ #include <stdio.h> int main() { printf( "nSizeof(char) = %d", sizeof( char )); printf( "nSizeof(int) = %d", sizeof( int )); printf( "nSizeof(short) = %d", sizeof( short )); printf( "nSizeof(long) = %d", sizeof( long )); printf( "nSizeof(float) = %d", sizeof( float )); printf( "nSizeof(double) = %d", sizeof( double )); printf( "nSizeof(unsigned char) = %d", sizeof( unsigned char )); printf( "nSizeof(unsigned int) = %d", sizeof( unsigned int )); printf( "nSizeof(unsigned short) = %d", sizeof( unsigned short )); printf( "nSizeof(unsigned long) = %dn", sizeof( unsigned long )); return 0; } Kada se kompilira i izvrši, program daje ispis: Sizeof(char) = 1 Sizeof(int) = 4 Sizeof(short) = 2 Sizeof(long) = 4 Sizeof(float) = 4 Sizeof(double) = 8 Sizeof(unsigned char) = 1 Sizeof(unsigned int) = 4 Sizeof(unsigned short) = 2 Sizeof(unsigned long) = 4 44
  • 45.
    U ovom seprogramu koristi standardni C operator sizeof koji daje vrijednost u bajtima, koju neki tip ili prethodno deklarirana varijabla zauzima u memoriji. Primjerice, sizeof(short int) daje vrijednost 2. Konstante C jezika indirektno u svom zapisu sadrže oznaku tipa. Primjeri su dani u tablici 4.7. Uočite da sufiks U ili u označava konstante kardinalnog tipa, sufiks f ili F označava realne brojeve jednostrukog formata, sufiks L ili l označava brojeve dvostrukog formata. char 'A' znakovna konstanata A 'a' znakovna konstanata a ili '035' znakovna konstanata 35 u oktalnoj notaciji 'x29' znakovna konstanata 29 u heksadecimalnoj notaciji int 'n' znak za novu liniju Int 156 decimalna notacija 0234 oktalna notacija cjelobrojne konstante 0x9c heksadecimalna notacija unsigned 156U Decimalno 0234U oktalno (prefiks U određuje kardinalni broj) 0x9cU heksadecimalno float 15.6F realni broj – jednostruki format 1.56e1F određen primjenom sufiksa F ili f double 15.6 konstante su tipa "double" ukoliko se ne koristi 1.56E1L prefiks F. Nije nužno pisati sufiks L. Tablica 4.7 Literalni zapis konstanti U tablici 7.7 važno je uočiti da su znakovne konstante kompatibilne i sa znakovnim i s cjelobrojnim tipom. To demonstrira program charsize.c. /* Datoteka: charsize.c */ /* Program kojim se ispituje tip znakovne konstante */ #include <stdio.h> int main() { char c; /* deklaracija varijable c tipa char*/ int x; /* deklaracija varijable x tipa int*/ c = 'A'; /* objema varijablama može se pridijeliti */ x = 'A'; /* vrijednost znakovne konstante 'A' */ printf( "n c = %c", c); printf( "n Sizeof c = %d", sizeof (c) ); printf( "n x = %d", x); printf( "n Sizeof x = %d", sizeof(x)); printf( "n Sizeof 'A' = %d", sizeof('A')); return 0; } koji daje ispis: c = A Sizeof c = 1 x = 65 Sizeof x = 4 Sizeof 'A' = 4 45
  • 46.
    U programu suprvo deklarirane dvije varijable; c je tipa char, a x je tipa int. Objema varijablama je zatim pridijeljena vrijednost znakovne konstante 'A'. Prilikom ispisa varijable c koristi se specifikator ispisa za znakove - %c, pa se ispisuje slovo A. Ispis vrijednosti varijable x daje cjelobrojnu vrijednost, koja je jednaka vrijednosti ASCII koda znaka A. Očito je da kompilator tretira znakovnu konstantu ovisno o kontekstu u kojem se koristi, a za nju rezervira u memoriji mjesto od 4 bajta, što pokazuje posljednji ispis. Ova dvostrukost primjene znakovnih konstanti zapravo je dobra strana C jezika. Time je omogućeno da se znakovne konstante mogu koristiti i kao simboli i kao numeričke vrijednosti. Simbolički značaj konstante važan je pri unosu i ispisu znakova, numerički značaj omogućuje da se znakovne konstante mogu koristiti u aritmetičkim i relacijskim izrazima na isti način kao i cjelobrojne konstante. 4.4 Direktiva #define Leksička se vrijednost numeričkih, znakovnih i literalnih konstanti, pa i samog programskog teksta, može pridijeliti nekom simboličkom imenu pomoću pretprocesorske direktive #define. Primjerice, ako se zapiše: #define PI 3.141592653589793 to ima efekt da kada se u tekstu programa, koji slijedi iza ove direktive, zapiše PI, vrijedi kao da je zapisano 3.141592653589793. Do sada smo upoznali direktivu #include. Sve pretprocesorske direktive počinju znakom #. Njih se ne smatra programskim naredbama pa se iza njih ne zapisuje znak točka-zarez. Pomoću njih se vrši leksička supstitucija teksta iz drugih datoteka ( pomoću #include) ili prema supstitucijskom pravilu koji je opisan direktivu #define . Općenito direktivu #define ima oblik: #define IME supstitucijski_tekst gdje IME predstavlja proizvoljan naziv zapisan neprekinutim nizom znakova (obično se zapisuje velikim slovima i podvlakom), a supstitucijski_tekst može sadržavati i isprekidani niz znakova. Primjerice, #include <stdio. h> #define MESSAGE "Vrijednost broja pi = " #define PI 3.141592653589793 #define PR_NL printf("n") int main( void) { printf(MESSAGE); printf("%f" ,PI); PR_NL; return 0; } Nakon izvršenja dobije se ispis Vrijednost broja pi = 3.141592653589793 Kasnije će biti detaljnije pojašnjen rad s pretprocesorskim direktivama. 46
  • 47.
    Na kraju ovogpoglavlja pogledajmo program Limits.c. Njime se ispisuje minimalna i maksimalna vrijednost za sve proste tipove podataka C jezika. U programu se koriste simboličke konstante koje su pomoću direktive #define zapisane u standardnim datotekama limits.h i float.h. /* Datoteka: Limits.c */ /* Ispisuje interval vrijednost numeričkih tipova */ #include <stdio.h> #include <limits.h> #include <float.h> int main( void ) { printf("%12s%12s%15s%15sn", "Tip", "Sizeof", "Minimum", "Maksimum"); printf("%12s%15d%15dn","char", CHAR_MIN, CHAR_MAX) ; printf("%12s%15d%15dn","short int", SHRT_MIN, SHRT_MAX) ; printf("%12s%15d%15dn","int", INT_MIN, INT_MAX) ; printf("%12s%15ld%15ldn","long int", LONG_MIN, LONG_MAX) ; printf("%12s%15g%15gn", "float", FLT_MIN, FLT_MAX) ; printf("%12s%15g%15gn", "double", DBL_MIN, DBL_MAX) ; printf("%12s%15Lg%15Lgn","long double",LDBL_MIN, LDBL_MAX) ; return 0 ; } Nakon izvršenja dobije se ispis: Tip Sizeof Minimum Maksimum char 1 -128 127 short int 2 -32768 32767 int 4 -2147483648 2147483647 long int 4 -2147483648 2147483647 float 4 1.17549e-038 3.40282e+038 double 8 2.22507e-308 1.79769e+308 long double 8 2.22507e-308 1.79769e+308 Ovaj ispis izgleda uredno. To je postignuto korištenjem specifikatora ispisa printf() funkcije u proširenom obliku, tako da je između znaka % i oznake tipa ispisa upisan broj kojim se određuje točan broj mjesta za ispis neke vrijednosti. 4.5 Specifikatori printf funkcije Kada se ispis vrši po unaprijed određenom obliku i rasporedu kažemo da se vrši formatirani ispis. Sada će biti pokazano kako se zadaje format ispisa printf() funkcije. Općenito format se zadaje pomoću šest polja: %[prefiks][širina_ispisa][. preciznost][veličina_tipa]tip_argumenta Format mora započeti znakom % i završiti s oznakom tipa argumenta. Sva ostala polja su opciona (zbog toga su napisana unutar uglatih zagrada). U polje širina_ispisa zadaje se minimalni broj kolona predviđenih za ispis vrijednosti. Ako ispis sadrži manji broj znakova od zadane širine ispisa, na prazna mjesta se ispisuje razmak. Ako ispis sadrži veći broj znakova od zadane širine, ispis se proširuje. Ako se u ovo polje 47
  • 48.
    upiše znak *to znači da će se broj kolona indirektno očitati iz slijedećeg argumenta funkcije, koji mora biti tipa int. Polje prefiks može sadržavati jedan znak koji ima sljedeće značenje: - Ispis se poravnava prema lijevoj granici ispisa određenog poljem širina_ispisa. (inače se poravnava s desne strane) U prazna mjesta se upisuje razmak + Pozitivnim se vrijednostima ispisuje i '+' predznak. razmak Ako je vrijednost pozitivna, dodaje se razmak prije ispisa (tako se može poravnati kolone s pozitivnim i negativnim brojevima). 0 Mjesta razmaka ispunjaju se znakom 0. # Alternativni stil formatiranja Polje .preciznost određuje broj decimalnih znamenki iza decimalne točke kod ispisa realnog broja ili minimalni broj znamenki ispisa cijelog broja ili maksimalni broj znakova koji se ispisuje iz stringa. Ovo polje mora započeti znakom točke, a iza nje se navodi broj ili znak *, koji znači da će se preciznost očitati iz slijedećeg argumenta tipa int. Ukoliko se ovo polje ne koristi, tada se podrazumijeva da će realni brojevi biti ispisano s maksimalno šest decimalnih znamenki iza decimalne točke. Polje tip_argumenta može sadržavati samo jedan od sljedećih znakova: c Argument se tretira kao int koji se ispisuje kao znak iz ASCII skupa. d, i Argument se tretira kao int, a ispisuje se decimalnim znamenkama. e, E Argument je float ili double, a ispis je u eksponentom formatu. F Argument je float ili double, a ispis je prostom decimalnom formatu. Ako je prefiks # i preciznost .0, tada se ne ispisuje decimalna točka. g, G Argument je float ili double, a ispis je prostom decimalnom formatu ili u eksponencijalnom formatu, ovisno o tome koji daje precizniji ispis u istoj širini ispisa. o Argument je unsigned int, a ispisuje se oktalnim znamenkama. p Argument se tretira kao pokazivač tipa void *, pa se na ovaj način može ispisati adresa bilo koje varijable. Adresa se obično ispisuje kao heksadecimalni broj. s Argument mora biti literalni string odnosno pokazivač tipa char *. u Argument je unsigned int, a ispisuje se decimalnim znamenkama. x, X Argument je unsigned int, a ispisuje se heksadecimalnim znamenkama. Ako se zada prefiks # , ispred heksadecimalnih znamenki se ispisuje 0x ili 0X. Polje veličina_tipa može sadržavati samo jedan znak koji se upisuje neposredno ispred oznake tipa. h Pripadni argument tipa int tretira se kao short int ili unsigned short int. l Pripadni argument je long int ili unsigned long int. L Pripadni argument realnog tipa je long double. Primjeri korištenja printf() funkcije dani su u programu printf.c. /* Datoteka: printf.c */ /* Primjer korištenja printf funkcije */ #include<stdio.h> int main() { 48
  • 49.
    printf("%d, %5d, %-5d,%05d, %5.5dn", 1, 2, 3, 4, 5); printf("%o %x %X %#o %#xn", 171, 171, 171, 171, 171); printf("%f %e %gn", 3.14, 3.14, 3.14); printf("%s, %.5s!n", "Hello", "worldly"); printf("%0*d, %.*f, %*.*sn", 2, 3, 4, 5.6, 7, 3, "abcdef"); return 0; } Ovaj pogram daje ispis: 1, 2, 3 , 00004, 00005 253 ab AB 0253 0xab 3.140000 3.140000e+000 3.14 Hello, world! 03, 5.6000, abc U prethodnim tablicama za oznake tipa argumenata 'p' i 's' naglašeno je da su argumenti funkcije pokazivači. Što je to pokazivač, bit će objašnjeno u slijedećem odjeljku. 4.6 Pristup podacima pomoću pokazivača Jedan od glavnih razloga zašto se C jezik smatra jezikom niske razine je taj što omogućuje korištenje numeričke vrijednosti adresa varijabli i funkcija za indirektno manipuliranje s podacima i izvršenjem programa. U tu svrhu koriste se posebni operatori - adresni operator & i operator indirekcije *, te specijalni tip varijabli koje se nazivaju pokazivačke varijable ili pokazivači (eng. pointer). Adresni operator & Prije je naglašeno da ime varijable ujedno označava i adresu varijable. Koja je to adresa? Brigu o tome vodi kompilator. Adresa varijable se može odrediti pomoću posebnog operatora & koji se naziva adresni operator. Koristi se kao unarni operator koji se zapisuje ispred imena varijable. Pogledajmo primjer programa adresa.c. /* Datoteka: adresa.c */ /* Primjer ispisa adrese varijable */ #include<stdio.h> int main() { int y; y = 777; printf("n Vrijednost varijable je %d", y); printf("n Adresa varijable je %#p", &y); return 0; } Ispis programa može izgledati ovako: Vrijednost varijable y je 777 Adresa varijable y je 0x0063FDF4 49
  • 50.
    Za ispis adresevarijable korištena je naredba printf("n Adresa varijable je %#p", &y); Uočimo da je ispred imena varijable zapisan adresni operator &. Ispis adrese je izvršen u heksadecimalnim obliku s osam znamenki, jer je korišteno 32-bitno računalo na kojem je adresa određena 32-bitnim kardinalnim brojem. Napomena: pri ponovljenom izvršenju programa ispis adrese ne mora biti isti jer operativni sustav ne učitava program uvijek na isto mjesto u memoriji (time se mijenja i adresa na kojoj se nalazi varijabla x). Operator indirekcije * Komplementarno adresnom operatoru koristi se unarni operator indirekcije koji se označava znakom *. Zapisuje se ispred izraza čija vrijednost predstavlja neku adresu. Značaj operatora indirekcije je u tome da se pomoću njega dobije vrijednost koja je upisana na toj adresi. To znači da ako je y jednako 777, onda je *(&y) također jednako 777 jer operator indirekcije daje vrijednost koja se nalazi na adresi varijable y. Ovo pravilo se može provjeriti tako da se u prethodnom programu napiše naredba: printf("n Vrijednost dobivena indirekcijom je %d", *(&y) ); Tada se dobije ispis: Vrijednost varijable je 777 Adresa varijable je 0X0063FDF4 Vrijednost dobivena indirekcijom je 777 Napomena: izraz *(&y) se može pisati i bez zagrada *&y, međutim , obično se pišu zagrade zbog lakšeg uočavanja redoslijeda djelovanja operatora. Pokazivači Varijable kojima je vrijednost adresa neke druge varijable ili funkcije nazivaju se pokazivači ili pointeri. Pokazivači moraju biti deklarirani, jer C kompilator mora znati kakav će tip podatka biti na adresi koju oni sadrže, ili kako se češće kaže, mora biti poznat tip objekta na kojeg oni pokazuju. Deklaracija pokazivača vrši se slično deklaraciji varijable, s razlikom što se između oznake tipa i imena pokazivača obvezno zapisuje znak indirekcije '*'. Primjerice, int *p; /* p je pokazivač na objekt tipa int */ unsigned *q; /* q je pokazivač na objekt tipa unsigned */ Ovim deklaracijama definirane su dvije pokazivačke varijable. Njihova vrijednost je neodređena, jer im nije pridijeljena adrese nekog realnog memorijskog objekta. Važno je znati da pokazivače prije upotrebe treba inicijalizirati, odnosno mora im se pridijeliti vrijednost adrese postojećeg memorijskog objekta. Pri tome, tip pokazivača mora biti jednak tipu memorijskog objekta. To se ostvaruje adresnim operatorom '&'. Primjerice, u programu int suma; /* deklaracija varijable suma */ int *p; /* deklaracija pokazivača na objekt tipa int */ sum = 777; /* inicijalizacija varijable suma */ p = &suma; /* p inicijaliziran na adresu varijable suma */ 50
  • 51.
    najprije je izvršenadeklaracija varijable suma i pokazivača p. Zatim je varijabli suma pridijeljena vrijednost 777, a pokazivač p je inicijaliziran da pokazuje na tu varijablu. Ako bi dalje koristili naredbe printf ("%d", suma); printf ("%d", *p); dobili bi isti ispis, jer se indirekcijom pokazivača dobiva vrijednost na koju on pokazuje, a to je vrijednost varijable suma (*p ⇔*&suma). memorijska sadržaj ime adresa memorije varijable 0x09A8 ...... ...... 0x09AC 777 suma 0x09B0 ...... ...... ...... ...... ...... ...... ...... ...... ...... ...... ...... 0x1000 0x09AC p 0x1004 ...... ...... Slika 4.3. Prikaz memorijskog sadržaja i adresa varijable suma i pokazivača p Stanje u memoriji prikazano je na slici 4.3. Strelicom je označeno da je vrijednost pokazivača p jednaka adresi varijable suma. Vrijednost adresa je napisana proizvoljno, jer se ne može unaprijed znati na kojoj će se adresi nalaziti podaci. Zbog toga se češće za opis operacija s pokazivačima koristi sljedeća simbolika. Varijabla se označava kao pravokutni objekt. Unutar pravokutnika upisuje se vrijednost varijable (ako je vrijednost neodređena upisuje se znak ?), a ime varijable se upisuje uz pravokutnik. Sadržaj pokazivačke varijable se označava strelicom koja pokazuje na neki drugi objekt. U skladu s ovakvom simbolikom prije opisane operacije se mogu predstaviti slikom 4.4. Slika 4.4. Prikaz operacija s pokazivačem na varijablu Važna osobina indirekcije pokazivača je da se može koristiti u svim izrazima u kojima se koriste i obične varijable. Primjerice, naredbom *p = 678; 51
  • 52.
    je iskazano dase vrijednost 678 pridijeli memorijskom objektu na kojeg pokazuje pokazivač p. Pošto je prije određeno da je taj objekt varijabla suma, onda ovaj izraz ima isti efekt kao da je korištena naredba: suma = 678; Ovime je demonstriran glavni razlog korištenja pokazivača, a to je da se pomoću pokazivača može manipulirati sa svim memorijskim objektima. U slučaju kada se pokazivač koristi samo za bilježenje adresa i kad nije predviđeno da se koristi u izrazima on se može deklarirati posebnom oznakom tipa void *p; što znači da je deklariran pokazivač koji pokazuje na “bilo što”, ili da je deklariran “netipski” pokazivač. Riječ void se u C jeziku koristi s značenjem kojeg je teško prevesti na hrvatski, jer sama riječ void ima višestruko značenje – prazan , slobodan i nevažeći. Često se koristi naziv nul pokazivač za pokazivače kojima je vrijednost jednak nuli. Njihova upotreba je vrlo opasna jer pokazuju na početni dio memorije, odnosno na područje memorije gdje nije smješten korisnički program, već programi ili podaci operativnog sustava. Stoga nije čudo da programi u kojima se greškom koristi nul pokazivači mogu potpuno “uništiti” operativno stanje računala. Iz prethodnog izlaganja očito je da su pokazivači varijable, jer im se sadržaj može mijenjati. To je razlog da se u C jeziku češće koristi pojam memorijski objekt nego varijabla za objekte kojima se može mijenjati sadržaj. Memorijskom se objektu može pristupiti pomoću imena ili posredno pomoću pokazivača. Imena označavaju fiksni položaj u memorija i često se kaže da predstavljaju referencu memorijskog objekta ili referencu. U skladu s ovim nazivljem, kada se ispred pokazivača primijeni operator indirekcije, kaže se da je to dereferencirani pokazivač. Rad s pokazivačima predstavlja važan element programiranja C jezikom. Slučajevi kada se koriste, i zašto se koriste, biti će objašnjeni tek kada se upoznaju temeljni elementi programiranja C jezikom. 4.7 Unos podataka u memoriju računala Do sada smo razmatrali kako su podaci smješteni u računalu i kako se njihova vrijednost može predočiti korisniku programa pomoću printf() funkcije. Sada će biti pokazano kako se podaci mogu unijeti u memoriju računala, u slučaju kada se unos podataka vrši pomoću tipkovnice. U tu svrhu se može koristiti funkcija scanf(). Funkciju scanf() ćemo koristiti u obliku: scanf(format_unosa, lista_adresnih_izraza); format_unosa je literalni string u kojem se zadaju specifikatori tipa objekta čija se vrijednost unosi. Oni su gotovo identični specifikatorima ispisa kod printf() funkcije. lista_adresnih_izraza je niz izraza odvojenih zarezom, čija vrijednost predstavlja adresu postojećeg memorijskog objekta. Tip objekta mora odgovarati specifikaciji tipa prethodno zapisanog u formatu_unosa. 52
  • 53.
    Jednostavni programi ukojima ćemo koristiti funkciju scanf() imat će sljedeći oblik: /* Datoteka: scanf1.c */ /* Obrazac unosa podataka pomoću scanf() funkcije*/ #include <stdio.h> int main(void) { /* 1. definiraj varijablu čiju vrijednost će unositi korisnik */ int unos; /* 2. ispiši poruku korisniku da se program očekuje unos : */ printf("Molim otipkajte jedan cijeli broj >"); /* 3. Pozovi funkciju scan s agumentom koji je adresa varijabe*/ scanf("%d", &unos); /* 4. obavi radnje s tom varijablom ......*/ /* 5. ispiši rezultat obrada*/ printf("nOtkucali ste broj %d.n", unos); return 0; } Kada se pokrene, ovaj program ispisuje poruku: c:> Molim otipkajte jedan cijeli broj >_ i čeka da korisnik otkuca jedan broj. Unos završava kada korisnik pritisne tipku <Enter>. Primjerice, ako korisnik otipka 12345<Enter>, program će završiti s porukom: Otkucali ste broj 12345. Važno je zapamtiti da argumenti funkcije scanf() moraju biti izrazi čija je vrijednost adresa. U prethodnom primjeru to je adresa varijable unos. Adresa se dobije primjenom adresnog operatora & na varijablu unos. Obično se u format_unosa ne upisuje nikakvi dodatni tekst, kao što je bio slučaj kod printf() funkcije, iako je to dozvoljeno. Razlog tome je činjenica da se tada od korisnika očekuje da otipka i taj dodatni tekst. Primjerice, ako bi se koristila naredba scanf("Broj=%d", &unos); i ako se želi unijeti vrijednost 25, onda korisnik mora otipkati "Broj=25<Enter>". Ako bi otkucao samo broj 25, funkcija scanf() ne bi dala ispravan rezultat. Pomoću scanf() funkcije može se odjednom unijeti više vrijednosti, primjerice unos jednog cijelog broja, jednog realnog broja i jednog znaka, može se ostvariti samo s jednim pozivom funkcije scanf(); int i; double x; char c; .... scanf("%d%f%c", &i, &x, &c); Pri unosu cijelih i realnih brojeva, funkcijom scanf(), podrazumijeva se da unos broja završava tzv. “bijelim” znakovima (razmak, tab, nova linija). Svi bijeli znakovi uneseni ispred 53
  • 54.
    broja se odbacuju.To nije slučaj kada se unosi znak, jer i bijeli znakovi predstavljaju znak. Stoga, pri unosu znaka potrebno je, u formatu zapisa, eksplicitno zadati razmak od prethodnog unosa. Razmotrimo prijašnji primjer i pretpostavimo da korisnik želi unijet cijeli broj 67, realni broj 3.14 i znak 'Z'. Prema zadanom formatu on bi morao otkucati: 86 3.14Z dakle, znak bi trebalo otipkati bez razmaka od prethodnog broja. Problem se može riješiti tako da se u format upisa unese razmak: scanf("%d%f% %c", &i, &x, &c); iako i ovo može stvarati probleme u komuniciranju s korisnikom, jer se smije koristiti samo jedan razmak. Primjerice ako bi korisnik otipkao: 86 3.14 Z ne bi bio unesen znak 'Z' već znak razmaka, jer su ispred znaka 'Z' dva mjesta razmaka. Navedeni problemi su razlog da programeri rijetko koriste scanf() funkciju za komuniciranje s korisnikom. Kasnije će biti pokazano da je za unos znakova pogodnije koristiti neke druge funkcije. Također, bit će pokazano kako dijagnosticirati da li je izvršen unos koji odgovara zadanom tipu varijable. 4.8 Inicijalizacija varijabli Temeljni uvjet korištenja neke varijable je da ona prethodno mora biti deklarirana. Samom deklaracijom nije određeno i početna (inicijalna) vrijednost varijable, pa prije korištenja varijable u izrazima, treba joj pridijeliti neku početnu vrijednost. Primjerice, u dijelu programa int main() { int y, x; /* deklaracija varijabli x i y */ x = 77; /* početna vrijednost varijable x */ y = x + 7; /* početna vrijednost varijable y */ ... koriste se dvije varijable: x i y. Početno je varijabli x pridijeljena vrijednost 77, i pomoću nje je određena početna vrijednost varijable y. Kada ne bi bila određena vrijednost od x, program bi se kompilirao bez dojave pogreške, ali tada bi pri izvršenju programa bila neodređena vrijednost varijable y. Određivanje početnih vrijednosti varijabli važan je element programiranja. Prilikom izrade većih programa, ukoliko se koriste neinicijalizirane varijable, mogu nastati greške koje je teško otkriti. U C jeziku se početna vrijednost varijable može odrediti i u samoj deklaraciji. Primjerice, prethodni program se može napisati u obliku: int main() { int y, x = 77; /* deklaracija varijabli x i y */ /* i inicijalizacija x na vrijednost 77 */ y = x + 7; /* početna vrijednost varijable y */ Inicijalizacija varijable je deklaracija u kojoj se određuje početna vrijednost varijable. 54
  • 55.
    5 Uvod uprogramiranje C jezikom Naglasci: • postupak izrade programa • algoritamska struktura programa u C jeziku • složena naredba, if-else selekcija i while-petlja • standardne i korisničke funkcije • definiranje prototipa funkcije • zaglavlje i tijelo funkcije • formalni i stvarni argumenti funkcije • razvoj jednostavnih algoritama U prethodnom je poglavlju opisano nekoliko jednostavnih C programa. Cilj je bio upoznati standardne tipove podataka i jednostavne postupke komuniciranja s korisnikom u dobavi i ispisu podataka. Sada će biti opisana algoritamska i funkcionalna struktura C programa te postupci izrade jednostavnih programa. 5.1 Postupak izrade programa Izrada se programa može opisati kao aktivnost koja se odvija u četiri temeljna koraka: 1. Definiranje zadatka i analiza problema. 2. Izrada detaljne specifikacije i uputa za rješenje problema. 3. Pisanje programa, dokumentacije i formiranje izvršnog programa. 4. Testiranje programa. Programer treba znati: • mogućnosti programskog jezika, • kako obraditi problem: o definiranje objekata obrade (podaci), o definiranje postupaka obrade (apstraktni i programski algoritmi), o definiranje korisničkog sučelja za unos podataka i prezentiranje rezultata obrade. • kako metodološki pristupiti razradi programa (strukturalno programiranje, modularno programiranje, objektno orijentirano programiranje), • kako optimalno iskoristiti računarske resurse i mogućnosti operativnog sustava računala, • koje softverske alate koristiti za razvoj programa. Većina od ovih pitanja bit će obrađena u narednim poglavljima. Postupak izrade manjih programa se može prikazati i dijagramom toka na slici 5.1. (korišteni su standardnim elementi za opis dijagrama toka, a opisani su u Dodatku 1). 55
  • 56.
    Slika 5.1. Postupakizrade manjih programa Bitno je uočiti: o Izradi programa prethodi analiza problema i izrada algoritama za rješenja problema o Tijekom pisanja programa često je potrebno ispravljati sintaktičke pogreške. o Ukoliko se program ne izvršava u potpunosti, moguće je postojanje pogreške u korištenju računarskih resursa (pr. u korištenju memorije). Postojanje takovih pogrešaka se ispituje posebnim programima – dibagerima (eng. debugger). o Postupak programiranja ne može biti završen ako program pri izvršavanju iskazuje nelogične rezultate. Tada ne preostaje ništa drugo nego da se krene od početka i da se ponovo kritički sagleda zadatak programiranja. Postupci izrade velikih programa, koji obrađuju kompleksne sustave, ovdje neći biti razmatrani. 56
  • 57.
    5.2 Algoritamska strukturaC programa? U uvodnom su poglavlju opisani temeljni oblici zapisa programskih algoritma. Oni se sastoje od naredbi koje se izvršavaju jedna za drugom (sekvence), od naredbi selekcije i iterativnih naredbi (petlji). Kroz niz primjera bit će pokazano kako se ove naredbe zapisuju u C jeziku. Posebnu pažnju posvetit će se problemu koji se obrađuje. Prvo će se definirati zadatak, a zatim će se vršiti analiza problema. Moguće rješenje iskazat će se podesnim algoritmom. Zatim će biti pokazano kako se izvršenje tog algoritam može ostvariti programom napisanim u C jeziku. Na kraju će se analizirati napisani program i rezultati koje on iskazuje tijekom svog izvršenja. Zadatak: Napisati program kojim se računa vrijednost od 5! (čitaj: pet faktorijela). Analiza problema: Vrijednost n! u matematici naziva n-faktorijela, a definirana je formulom: ⎧1 za n = 0 ⎪ n n! = ⎨ ⎪∏ k za n > 0 ⎩ k =1 Ovu se formulu može opisati i sljedećim zapisom: n! je jednako 1 ako je n=0, a za vrijednosti n>0, n! je jednako 1*2*3*..*n Rješenje: Trivijalno rješenje problema dano je u programu fact0.c. Najprije je deklarirana cjelobrojna varijabla nfac. Zatim je toj varijabli pridijeljena vrijednost umnoška konstanti 1*2*3*4*5, što odgovara vrijednosti 5!. Za ispis te vrijednosti korištena je standardna funkcija printf(). /* Datoteka fact0.c - Proračun 5! */ #include <stdio.h> int main() { int nfact; nfact = 1 * 2 * 3 * 4 * 5; printf("Vrijednost 5! iznosi: %dn", nfact); return 0; } Nakon izvršenja programa dobije se ispis: Vrijednost 5! iznosi: 120 Pošto argument funkcije printf() može biti bilo koji izraz koji rezultira nekom vrijednošću, prethodni se program može napisati i u obliku: /* Datoteka fact01.c */ /* Proračun 5! unutar argumenta funkcije printf() */ #include <stdio.h> int main() { printf("Vrijednost 5! iznosi: %dn", 2 * 3 * 4 * 5); return 0; 57
  • 58.
    } Oba, prethodno napisana programa nisu od neke koristi, jer se njima računa nešto što čovjek može napamet puno brže riješiti. 5.2.1 Naredba iteracije – while petlja Cilj pisanja programa je poopćenje procesa obrade nekog problema na način da se dobije rezultat za različite vrijednosti ulaznih podatka. U tu svrhu definiran je sljedeći zadatak: Zadatak: Napisati program kojim se računa vrijednost od n!. Vrijednost n zadaje korisnik. Program mora obaviti sljedeće operacije: 1. Dobaviti vrijednost od n. 2. Izračunati vrijednost n!. 3. Ispisati vrijednost od n i n!. Postavlja se pitanje kako realizirati korak 2 ovog algoritma. Problem je u tome što se unaprijed ne zna vrijednost od n, jer tu vrijednost unosi korisnik programa. Analiza problema: Polazi se od definicije n-faktorijela n! = 1 za n = 0 n! = 1*2 * ..(n-2)*(n-1)*n za n > 0 Lako je uočiti da vrijedi i sljedeće pravilo: n! = 1 za n=0 n! = n * (n-1)! za n>0 koje kazuje da se vrijednost od n! može izračunati iz prethodno poznate vrijednosti od (n-1)!. Koristeći ovu formulu, prijašnji problem proračuna 5! bi se mogao programski riješiti uvođenjem pomoćne cjelobrojne varijable k i sljedećim nizom naredbi: /* stanje nakon izvršenja naredbi */ k = 0; nfact = 1; /* k jednak nuli, nfact jednak 1 */ k = k+1; nfact = k * nfact; /* k jednak 2, nfact jednak 2 */ k = k+1; nfact = k * nfact; /* k jednak 3, nfact jednak 6*/ k = k+1; nfact = k * nfact; /* k jednak 4, nfact jednak 24*/ k = k+1; nfact = k * nfact; /* k jednak 5, nfact jednak 120*/ Ovaj primjer pokazuje vrlo neefikasan način proračuna 5!, međutim, značajan je jer ukazuje da se do rezultata dolazi ponavljanjem istih naredbi. U ovom se slučaju naredba k=k+1; nfact=k*nfact; ponavlja sve dok je vrijednost varijable k manja od 5, pa se može napisati algoritamsko rješenje u obliku iterativne petlje: 1. k = 0; nfact = 1; 2. dok je k<5 ponavljaj k = k+1; nfact = k * nfact; U C jeziku se ovaj tip petlje zapisuje iskazom koji se zove while-petlja: 58
  • 59.
    k = 0;nfact = 1; while (k < 5) /* zaglavlje petlje */ { k = k+1; /* tijelo petlje */ nfact = k * nfact; } Općenito while-petlja ima oblik: while (izraz) { niz_naredbi ili while (izraz) naredba } a ima značenje: dok je (eng. while) izraz u zaglavlju petlje različit od nule izvršava se niz_naredbi tijela petlje koje su napisane unutar vitičastih zagrada. Ako je izraz jednak nuli izvršenje programa se nastavlja operacijom koja je definirana naredbom koja slijedi iza tijela petlje. U slučaju kada se u tijelu petlje navodi samo jedna naredba, tada nije nužno pisati vitičaste zagrade. Izraz može biti bilo koji numerički ili relacijski izraz, a tretira se kao logički uvjet za izvršenje naredbi koje su obuhvaćene tijelom petlje. U prethodnom primjeru izraz ima oblik relacijskog izraza k < 5. Taj izraz u C jeziku može imati samo dvije vrijednosti: 1 ili 0, što je ekvivalentno logičkim vrijednostima istina ili laž. Naredbe tijela petlje će se ponavljati za vrijednosti k=1,2,3,4, jer je za te vrijednosti relacijski izraz k<5 istinit, odnosno njegova numerička vrijednost je različita od nule. Pravu korist korištenja while-petlje spoznaje se tek kada ona primijeni za računanje vrijednosti n!, gdje je n vrijednost koju zadaje korisnik programa. Rješenje je jednostavno: u zaglavlju while-petlje, umjeto izraza k<5, dovoljno je uvrstiti izraz k<n, pa za realizaciju koraka 2 vrijedi algoritam: 2. Izračunati vrijednost n!. 2.1. Postavi nfact = 1; 2.2. Postavi k=0; 2.3. Dok je k < n ponavljaj Uvećaj vrijednost varijable k za jedan nfact = k * nfact; Dorada koraka 2: Analiziram prethodnog algoritama može se uočiti da je vrijednost nfact jednaka jedinici ne samo kada je n=0, već i u slučaju kada je n=1. Zbog toga se kao početna vrijednost varijable k može uzeti jedinica. Vrijedi algoritam: 2. Izračunati vrijednost n!. 2.1 Postavi nfact = 1; 2.3 Postavi k=1; 2.3 Dok je k < n ponavljaj Uvećaj vrijednost varijable k za jedan nfact = k * nfact; Sada se može napisati program "fact1.c", kojim se implementira prethodno opisani algoritam. 59
  • 60.
    /* Datoteka: fact1.c*/ /* Proračun n!. Vrijednost od n unosi korisnik. */ #include <stdio.h> int main() { int n, k, nfact; /* deklaracija potrebnih varijabli*/ /* korak 1*/ scanf("%d", &n); /* korak 2 */ nfact = 1; /* korak 2.1 */ k = 1; /* korak 2.2 */ while ( k < n) { /* korak 2.3 */ k = k + 1; nfact = k * nfact; } /* korak 3 */ printf("Vrijednost %d! iznosi: %dn", n, nfact); return 0; } Unutar programa komentarima je označen pojedini korak algoritma. Testiranje programa fact1: Nakon izvršenja ovog programa, na ekranu se dobije prikaz c:>_ Program čeka da korisnik unese neku vrijednost za n. Ako unese vrijednost 5, dobije se ispis Vrijednost 5! iznosi: 120 Ako korisnik unese vrijednost 13 dobije se rezultat: Vrijednost 13! iznosi: 1932053504 Ako korisnik unese vrijednost 18 dobije se rezultat: Vrijednost 18! iznosi: -898433024 Ovaj posljednji rezultat je pogrešan, jer vrijednost od 18! nadmašuje maksimalnu vrijednost koja se može kodirati kao cijeli broj u memoriji veličine 4 bajta (tj. 2147483647). Može se zaključiti da je maksimalna vrijednost koja se može izračunati jednaka 13!. Ako korisnik otkuca negativni broj, primjerice broj -3, program će ispisati: Vrijednost -3! iznosi: 1 Ovaj rezultat nema nikakvog smisla jer funkcija n-faktorijela nije definirana za negativne brojeve. 60
  • 61.
    5.2.2 Uvjetna naredba– if naredba Nakon provedenog testiranja, pokazala se potreba za doradom prvog koraka algoritma sljedećim operacijama: Dorada koraka 1: 1. Dobaviti vrijednost od n. 1.1. Upozoriti korisnika da se očekuje unos broja unutar intervala [0,13] 1.2. Dobaviti otipkanu vrijednost u varijablu n 1.3 Ako je n < 0 ili n > 13 tada izvršiti sljedeće: izvijestiti korisnika da je otkucao nedozvoljeni broj prekinuti izvršenje programa Kako implementirati ove korake u C jeziku? Korake 1.1 i 1.2 može se zapisati naredbama printf("Unesite broj unutar intervala [0,13]n"); scanf("%d", &n); Za implementaciju koraka 1.3 potrebno je upoznati kako se u C jeziku zapisuje uvjetna naredba tzv. if-naredba. Njen opći oblik glasi: if (izraz) { niz_naredbi ili if (izraz) naredba; } a značenje ove naredbe je: ako je (eng. if) izraz različit od nule izvršava se niz_naredbi koji je omeđen vitičastim zagradama, u protivnom izvršit će se naredba koja slijedi iza if-naredbe. Predikatni izraz, na temelju kojeg se u algoritamskom zapisu vrši selekcija, glasi: n < 0 ili n > 13. U C jeziku se logički operator “ili” zapisuje s dvije okomite crte ||, pa prethodni izraz u C jeziku ima oblik n < 0 || n > 13 (Napomena: logički operator “i” se zapisuje s &&, a logička negacija znakom ! ispred logičkog izraza). Sada se korak 1.3 može napisati u obliku: if((n < 0) || (n > 13)) { printf("Otipkali ste nedozvoljenu vrijednost"); return 1; /* forsirani izlaz iz funkcije main */ } pa kompletni program izgleda ovako: /* Datoteka fact2.c */ /* Proračun n!. Vrijednost od n unosi korisnik. */ /* Vrijednost od n mora biti unutar intervala [0,13]*/ #include <stdio.h> int main() 61
  • 62.
    { int n, k, nfact; printf("Unesite broj unutar intervala [0,13]n"); scanf("%d", &n); if((n < 0) || (n > 13)) { printf("Otipkali ste nedozvoljenu vrijednost"); return 1; /* forsirani izlaz iz funkcije main */ } nfact = 1; k = 1; while ( k < n) { k = k + 1; nfact = k * nfact; } printf("Vrijednost %d! iznosi: %dn", n, nfact); return 0; } Konačno je ostvaren kvalitetan i robustan program. On za bilo koju ulaznu vrijednost daje rezultat nakon konačnog broja operacija. Ovo svojstvo se smatra temeljnim uvjetom koji mora zadovoljiti svaki programski algoritam. 5.2.3 Naredba selekcije: if-else naredba Radi vježbe i upoznavanja još jednog programskog iskaza – if-else naredbe, prethodni algoritam se može zapisati u ekvivalentnom obliku: Dobavi vrijednost od n. Ako je n >= 0 i n<=13 tada Izračunaj vrijednost n!. Ispiši vrijednost od n i n!. inače Izvijesti o pogrešnom unosu Kraj! Tijek programa se sada kontrolira naredbom selekcije, koja ima značenje: ako je logički uvjet istinit tada izvrši prvi niz naredbi inače izvrši alternativni niz naredbi U C jeziku se ovaj tip naredbe zove if-else naredba ili if-else iskaz, a zapisuje se prema obrascu: if(izraz) { niz_naredbi1 ili if(izraz) } naredba1; else else { naredba2; niz_naredbi2 } 62
  • 63.
    Značenje ove naredbeu je: ako je (eng. if) izraz različit od nule izvršava se niz_naredbi1, inače (eng. else) izvršava se niz_naredbi2. Ako niz_naredbi sadrži samo jednu naredbu ne moraju se pisati vitičaste zagrade. Izraz se tretira kao logička vrijednost. U ovom primjeru proračun n! će se izvršiti samo ako su istovremeno zadovoljena dva uvjeta: n>=0 i n<=13. Ovaj se uvjet u C jeziku zapisuje s dva relacijska izraza povezana logičkim operatorom “i”, koji se označava s &&. Program sada izgleda ovako: /* Datoteka fact3.c */ /* Proračun n!. Vrijednost od n unosi korisnik. */ /* Vrijednost od n mora biti unutar intervala [0,13]*/ #include <stdio.h> int main() { int n, k, nfact; printf("Unesite broj unutar intervala [0,13]n"); scanf("%d", &n); if((n >= 0) && (n <= 13)) { nfact = 1; k = 1; while ( k < n) { k = k + 1; nfact = k * nfact; } printf("Vrijednost %d! iznosi: %dn", n, nfact); } else printf("Otipkali ste nedozvoljenu vrijednost"); return 0; } 5.3 Funkcije C jezika U prethodnoj su sekciji opisani temeljni iskazi kontrole izvršenja C programa, te kako se oni koriste u implementaciji programskih algoritama. Čitav se program izvršavao unutar jedne funkcije – main(). Unutar te funkcije korištene su standardne funkcije print() i scanf(), iako nije poznato kako su te funkcije implementirane. Korištene su zbog toga jer su poznata pravila njihove upotrebe i efekti koje one uzrokuju. 5.3.1 Korištenje funkcija iz standardne biblioteke C jezika Funkcije se u programiranju koriste slično načinu kako se koriste funkcije u matematici. Kada se u matematici napiše y=sin(x), x predstavlja argument funkcije, a ime funkcije sin označava pravilo po kojem se skup vrijednosti, kojem pripada argument x, pretvara u skup vrijednosti koje može poprimiti y. Funkcija sin() se može koristiti i u izrazima C-jezika jer je implementirana u standardnoj biblioteci funkcija. Primjerice, dio programa, u kojem se ona koristi, može biti sljedećeg oblika: #include <math.h> int main() 63
  • 64.
    { double x,y; ......... x = sin(5.6); y = sin(x)+4.6; ..... } Prvo je napisana leksička direktiva da se u proces kompiliranja uključi datoteka "math.h" u kojoj je specificiran prototip (ili deklaracija) funkcije sin(). Pregledom te datoteke može se pronaći specifikacija prototipa funkcije sin() oblika: double sin(double); Prototip iskazuje da argument funkcije mora biti vrijednost tipa double i da funkcija u izraze vraća vrijednost tipa double. Općenito, funkcija može imati više argumenata. Oni se navode u zagradama iza imena funkcije i odvajaju zarezom. Tip vrijednosti kojim rezultira izvršenje funkcije uvijek se navodi ispred imena funkcije. Deklaracija prototipa završava znakom točka-zarez. Ime argumenta funkcije nije navedeno već samo tip argumenta. Ime argumenta može biti i napisano (primjerice, double sin(double x) ), međutim, u prototipu ono nema nikakvi značaj jer deklaracija prototipa služi kompilatoru jedino kao pokazatelj s kojim tipovima vrijednosti će se koristiti funkcija. Važno je zapamtiti da C funkcije “uzimaju” vrijednost svojih argumenata za proračun novih vrijednosti ili za ostvarenje nekog drugog procesa. Argument funkcije može biti bilo koji izraz koji rezultira tipom vrijednosti koji je deklariran u prototipu funkcije. Primjerice, vrijednost argumenta u naredbi x=sin(5.6) je vrijednost konstante 5.6, a u naredbi y=sin(x)+4.6 stvarni argument funkcije je vrijednost varijable x. Slika 5.2. Redoslijed poziva funkcije Uobičajeno se kaže da je u prethodnim naredbama izvršen poziv funkcije sin(), čime se želi naglasiti da se za izvršenje te funkcije aktivira dio izvršnog koda u kojem se nalaze naredbe koje realiziraju tu funkciju. Funkcija iz koje se poziva funkcija, naziva se pozivna funkcija, (u ovom slučaju to je funkcija main()), a sama funkcija sin() se naziva pozvana funkcija. Simbolički, pozvanu funkciju možemo shvatiti kao “crnu kutiju” koja prima i vraća vrijednost u pozivnu funkciju. Ta je simbolika ilustrirana na slici 5.2. Prethodni segment programa se može napisati u ekvivalentnom obliku: #include <math.h> ....... y = sin(sin(5.6))+ 4.6; ....... 64
  • 65.
    prema pravilu daargumenti funkcije mogu biti i izrazi. Postavlja se pitanje: kojim redoslijedom se izvršavaju operacije u navedenoj naredbi pridjele vrijednosti. U C jeziku vrijedi pravilo da se pri proračunu izraza najprije računa vrijednost izraza koji se nalaze unutar zagrada. Stoga, najprije će biti izračunata vrijednost funkcije sin(5.6), zatim će ta vrijednost biti upotrebljena kao argument za ponovni poziv funkcije sin(). Konačno, dobivenoj će vrijednosti biti pribrojena vrijednost konstante 4.6. Korisnik ne mora znati kako je napisan dio programa koji računa vrijednost funkcije sin() jer se taj dio programa uključuje u izvršni kod direktno iz biblioteke kompiliranih funkcija. Kako se to može napraviti i s funkcijama koje kreira korisnik bit će pokazano u sljedećoj sekciji. 5.3.2 Korisnički definirane funkcije Sada će biti pokazano kako korisnik može definiranja neku funkciju i kako se ona uključuje u korisnički program. Pravilo je: Definicija funkcije se sastoji od "zaglavlja" i "tijela" funkcije. Zaglavlje funkcije je deklaracija u kojoj se redom navodi 1. oznaka tipa koji funkcija vraća u izraze, 2. ime funkcije, 3. deklaracija liste parametara (formalnih argumenata) funkcije napisanih unutar zagrada. Tijelo funkcije je složeni iskaz naredbi i deklaracija varijabli, koji definiraju implementaciju. Piše se unutar vitičastih zagrada. Unutar tijela funkcije se pomoću ključne riječi return označava izraz, čiju vrijednost funkcija vraća u pozivnu funkciju. Primjerice, definicija funkcije kojom se računa kvadrat cjelobrojnog argumenta glasi int kvadrat(int y) { return y * y; } Ključna riječ return označava mjesto na kojem se prekida izvršenje funkcije, na način da se prethodno izračuna vrijednost izraza koji je napisan iza riječi return. Vrijednost tog izraza je vrijednost koju funkcija vraća u izraz iz kojeg je pozvana. U definiciji funkcije mora se navesti ime argumenta s kojim će se izvršiti operacije unutar funkcije. To ime se naziva formalni argument ili parametar funkcije jer on ima značaj samo pri definiranju funkcije (pri pozivu funkcije kao stvarni argument koristi se vrijednost nekog izraza). U sljedećem programu ilustrirana je definicija i upotreba funkcije kvadrat(). 65
  • 66.
    Slika 5.3 Definiranjefunkcije Složeni iskaz C jezika, koji je napisan unutar vitičastih zagrada, naziva se blok. Tijelo funkcije je blok C jezika. Unutar bloka se mogu koristiti svi tipovi iskaza C jezika uključujući deklaraciju varijabli i prototipova funkcija, jedino se ne smije vršiti definiranje neke druge funkcije. Deklaracije se moraju pisati neposredno na početku bloka (iza vitičastih zagrada). Alternativno se funkcija kvadrat() može napisati u obliku: int kvadrat(int y) { int tmp; tmp = y*y; return tmp; } U ovom je slučaju najprije deklarirana varijabla tmp koja služi za privremeni smještaju rezultata izraza y*y. Funkcija vraća vrijednost te varijable. Iako ova verzija funkcije kvadrat() izgleda bitno drugačije od prve verzije, ne mora biti nikakve razlike u načinu kako se stvarno izvršavaju ove funkcije Razlog tome je činjenica da kompilator sam generira tzv. privremene varijable za smještaj rezultata aritmetičkih operacija. Optimizirajući kompilatori često za smještaj privremenih varijabli koriste registre procesora, jer se njima najbrže pristupa. Iz ovog razloga u C jeziku je omogućeno da se pomoću ključne riječi register, napisane ispred deklaracije cjelobrojne varijable, sugerira kompilatoru da za smještaj varijable koristi registre procesora. Primjerice, sljedeći oblik funkcije int kvadrat(int y) { register int tmp; /* sugeriraj korištenje registra*/ tmp = y*y; return tmp; } 66
  • 67.
    je najbliži načinukako optimizirajući kompilatori prevode prvi oblik funkcije kvadrat(). Danas mnogi programeri smatraju da uopće ne treba koristiti ključnu riječ register, jer moderni optimizirajući kompilatori mnogo efikasnije koriste procesorske registre, nego što to može učiniti programer tijekom procesa programiranja. 5.3.2 “void” funkcije U programskim se jezicima često koristi dva tipa potprograma: funkcije i procedure. Procedura je potprogram koji vrši neki proces, ali ne vraća nikakvu vrijednost. Pošto se u C- jeziku svi potprogrami nazivaju funkcije, onda se kaže da je procedura funkcija koja vraća ništa (eng. void). Primjerice, u trećem poglavlju korištena je funkcija void hello() za ispis poruke "Hello World!". Pomoću ključne riječi void označava se da je tip vrijednosti koji funkcija vraća "ništa", odnosno da je nevažan. Poziv procedure se vrši njezinim imenom. Pošto procedure ne vraćaju nikakvu vrijednost, ne mogu se koristiti u izrazima. U proceduri se ne navodi ključna riječ return, iako se može koristiti (bez argumenta) ako se želi prekinuti izvršenje procedure prije izvršenja svih naredbi koje se pozivaju u proceduri. 5.3.3 Primjer: funkcija za proračun n! Za proračuna n-faktorijela zgodno je definirati funkciju koja obavlja taj proračun. Prototip te funkcije može biti oblika: int factorial(int n); Funkcija factorial() će kao argument koristiti vrijednost tipa int. Primjena ove funkcije u izrazima rezultirat će vrijednošću tipa int koji predstavlja vrijednost n-faktorijela. Definicija i primjena funkcije zapisani su u programu fact4.c /* Datoteka fact4.c */ /* Proračun n! pomoću funkcije factorial(n) */ /* Vrijednost od n mora biti unutar intervala [0,13]*/ #include <stdio.h> /* definicija funkcije za proračun n faktorijela */ int factorial(int n) { int k = 1, nfact = 1; while (k < n) { k = k + 1; nfact = k * nfact; } return nfact; } int main() { int n; printf("Unesite broj unutar intervala [0,13]n"); 67
  • 68.
    scanf("%d", &n); if((n < 0) || (n > 13)) printf("Otipkali ste nedozvoljenu vrijednost"); else printf("Vrijednost %d! iznosi: %dn", n, factorial(n)); return 0; } Bitno je uočiti da se u glavnom programu više ne koriste varijable k i nfact. Te varijable su deklarirane unutar funkcije factorial(), jer su one potrebne samo za vrijeme dok se izvršava ta funkcija. U C jeziku vrijedi opće pravilo da sve varijable, koje se definiraju unutar bloka ili tijela funkcije, zauzimaju memoriju samo dok se izvršava taj blok ili funkcija. Kada započne izvršenje funkcije, skriveno od korisnika rezervira se dio memorije za te varijable, i to u dijelu memorije koja se uobičajeno naziva stog (eng. stack). Nakon izvršenja funkcije, a prije nego se nastavi izvršenje programa iz pozivne funkcije, ta se memorija ponovo smatra slobodnom za korištenje. Ovo ujedno znači da se varijable, koje se deklariraju u nekoj funkciji, mogu koristiti samo u toj funkciji. One se stoga po dosegu imena ili vidljivosti (eng. scope) nazivaju lokalne varijable, a pošto im je vrijeme postojanja ograničeno na vrijeme u kojem se izvršavaju naredbe funkcije, nazivaju se i automatske varijable. 5.3.4 Funkcija za proračun ex Zadatak je napisati funkciju kojom se približno određuje vrijednost funkcije ex (e = 2. 718282) i rezultat usporediti s vrijednošću koja se dobije pomoću standardne funkcije exp(), kojoj je prototip - double exp(double x) - deklariran u datoteci "math.h". Metod: Koristeći razvoj u red: ex = 1 + x/1! + x2 /2! + x3 /3! + .. zbrajati članovi reda, za dati x, sve dok razlika od prethodnog rezultata ne bude manja od zadane preciznosti eps. Primjerice, za x = 1.0, i eps = 0.0001 trebat će zbrojiti 10 članova reda. Specifikacija funkcije: double my_exp(double x, double eps); Parametri: x - vrijednost za koju se računa ex , tipa double eps - zadana preciznost proračuna, tipa double Rezultat: vrijednost tipa double, jednaka vrijednosti ex Algoritam: Razvoj u red funkcije ex ima karakteristiku da se i-ti pribrojnik reda dobije tako da se prethodni pribrojnik pomnoži s x/i. Koristeći tu činjenicu, može se primijeniti sljedeći iterativni algoritam: unesi x i eps i =1, pribrojnik = 1; ex = pribrojnik, preth_ex = 0; dok je apsolutna vrijednost od (ex – preth_ex) manja od eps ponavljaj preth_ex = ex; pribrojnik = pribrojnik * x / i; ex = ex + pribrojnik; uvećaj i; 68
  • 69.
    Napomena: apsolutna sevrijednost realnog broja x u C jeziku dobije primjenom funkcije double fabs(double x) koja je deklarirana u <math.h>. Realizacija programa: /*Datoteka: ex.c*/ #include <stdio.h> #include <math.h> double my_exp(double x, double epsilon) { int i = 1; double pribroj = 1.0; double ex = 1.0, preth_ex = 0.0; while (fabs( ex - preth_ex) > epsilon) { preth_ex = ex; pribroj = pribroj * x / i; ex = ex + pribroj; i = i + 1; } return ex; } int main( void) { double eps, x, ex; printf(" Unesi x i preciznost eps:n"); scanf("%lf%lf", &x, &eps); ex = my_exp(x, eps); printf(" e^%f = %f; (tocno: %f)n", x, ex, exp(x)); return 0; } Izvršenjem programa dobiju su rezultati: c:>ex Unesi x i preciznost eps: 1 .00001 e^1.000000 = 2.718282; (tocno: 2.718282) c:>ex.exe Enter x and the eps: 2 .0001 e^2.000000 = 7.389047; (tocno: 7.389056) U prethodnom programu istim imenom (ex) su deklarirane varijable u funkciji main() i u funkciji my_exp(). Postavlja se pitanje: da li je to ista varijabla ili se radi o dvije različite varijable? Na to pitanje daju odgovor pravila dosega ili postojanosti identifikatora. Pravilo je da se u različitim blokovima mogu deklarirati varijable s istim imenom. To su onda različite varijable koje postoje samo u bloku u kojem su definirane. O tome će biti više govora u poglavlju 8. Na sličan način su definirane mnoge matematičke funkcije iz standardne biblioteke (vidi Dodatak C). 69
  • 70.
    5.4 Zaključak Do sadasu korišteni sljedeći elementi C jezika: 1. Varijable i funkcije su zapisivane simboličkim imenima. U iskazima deklaracije svim varijablama je uvijek označen tip. To omogućuje kompilatoru da rezervira memoriju potrebnu za smještaj vrijednosti varijable. 2. Numeričke konstante i stringovi su zapisivani u literalnom obliku – upravo onako kako se zapisuju i u govornom jeziku.. 3. Korišteni su različiti operatori pomoću kojih se formiraju aritmetički, relacijski i logički izrazi. 4. Korištene su naredbe kojima se određuje izvršenje procesa u računalu. Najprije su korištene tzv. proste naredbe: pridjela vrijednosti i poziv izvršenja standardnih funkcija printf() i scanf(). Zatim su korištene tzv. strukturalne naredbe: sekvenca naredbi koja se omeđuje vitičastim zagradama, while-petlja kojom se kontrolira tijek iterativnih procesa, if- naredba, pomoću koje se uvjetno određuje izvršenje neke naredbe, te if-else naredba, pomoću koje se vrši selekcija naredbi. Kasnije će biti opisane još neke naredbe za kontrolu toka programa. 5. Opisan je jednostavni način interakcije s korisnikom programa. 6. Pokazano je kako se koriste funkcije iz standardne biblioteke i kako korisnik može definirati nove funkcije. 7. Pokazano je da se funkcija može pozivati višestruko. 8. Pokazano je da se proračuni u računalu mogu izvršiti s ograničenom točnošću. Na primjeru eksponencijalne funkcije pokazano je kako je implementirana većina trigonometrijskih funkcija. 9. Razvijen je algoritam za proračun n-faktorijela i izvršena implementaciju tog algoritma u C jeziku. Sam tijek razvoja algoritma može programerima - početnicima biti zbunjujući, jer su stalno vršene dodatne analize i dorada algoritma. Iskusniji programeri znaju da je to jedini ispravni način razvoja programa, jer se samo postupnom analizom i doradom programa može napraviti kvalitetan program. Razvoj programa postupnom analizom i doradom (ili razvoj u koracima preciziranja) je metoda koju su popularizirali E. Dijkstra, u knjizi "Structured Programming", Academic Press, 1972, i N. Wirth u članku "Program Development by Stepwise Refinement",CACM, April 1971. Sam postupak se može opisati na sljedeći način: 1. Formuliraj problem na način da bude potpuno jasno što program treba obaviti. 2. Formuliraj temeljni tijek algoritamskog rješenja običnim govornim jezikom. 3. Izdvoji pogodnu manju cjelinu i razloži je detaljnijim algoritmom. 4. Ponavljaj korak (3) dok se ne dobiju algoritmi koji se mogu zapisati programskim jezikom (ili pseudo-jezikom). 5. Odaberi dio algoritamskog zapisa i zapiši ga programskim jezikom. Pri tome odredi potrebne struktura podataka. 6. Sustavno ponavljaj korak (5) i pri tome povećaj razinu dorade programskih rješenja. Na kraju, mora se nažalost reći, da ni danas u programiranju nema gotovih recepata, pa i dalje vrijedi izneseni metodološki pristup razvoju programskih algoritama. 70
  • 71.
    6 Izrazi isintaksa C jezika Naglasci: • aritmetički, logički i relacijski izrazi • pravila prioriteta i asocijativnosti • bitznačajni operatori • složeni operatori • ternarni izrazi • automatska i eksplicitna pretvorba tipova • typedef • sintaksa i leksika programskih jezika • BNF notacija za zapis sintakse 6.1 Izrazi Izrazi su zapisi koji sadrže operande i operatore. Svaki izraz daje neku vrijednost. Operandi mogu biti varijable, funkcije i konstante. U izrazima može biti više operatora i više različitih tipova operanada. S obzirom na složenost izraza razlikuju se: • Unarni izrazi – imaju samo jedan operator i jedan operand, • Binarni izrazi – imaju dva operanda i jedan operator, • Ternarni izrazi – imaju tri operanda i dva operatora, • Složeni izrazi – sastoje se od više operanada, operatora i zagrada koje služe za grupiranje izraza. Pravilo je da se najprije računa vrijednost izraza koji je napisan u zagradama, a zatim se ta vrijednost tretira kao prosti operand. Ukoliko nema zagrada, tada za redoslijed izvršenja složenog izraza vrijede posebna pravila prioriteta i asocijativnosti djelovanja operatora. S obzirom na upotrebu različitih operatora, izrazi mogu biti aritmetički, relacijski i logički. Bit će pokazno: • Kako se izvršavaju izrazi? • Koji su pravila prioriteta i asocijativnosti djelovanja operatora? • Kako se vrši pretvorba tipova ako u nekom izrazu postoji više različitih tipova? 6.1.1 Aritmetički izrazi Binarni aritmetički izrazi koriste dva operanda i jedan operator: + za zbrajanje, - za oduzimanje, * za mnnoženje, / za djeljenje i % za ostatak dijeljenja cjelobrojnih tipova (modulo operacija). Operandi mogu biti varijable, konstante i funkcije koja vraćaju numeričku vrijednost. Operator % se može primijeniti samo na cjelobrojne operande jer se njime dobija ostatak cjelobrojnog dijeljenja, primjerice izraz 71
  • 72.
    x % 2 dajevrijednost ostatka dijeljenja s 2. Taj ostatak može biti 0 ili 1 (ako je 0, broj x je paran, a ako je 1, broj x je neparan). Unarni aritmetički izrazi imaju jedan operand i jedan operator: - za negaciju (daje negativnu vrijednost) i + za 'afirmaciju' (ne mijenja vrijednost operanda). Operatori se zapisuju ispred imena varijable, konstante ili funkcije koja vraća vrijednost. Prefiks i postfiks unarni operatori Prefiks i postfiks operatori: ++ i --, uvećavaju, odnosno umanjuju, vrijednost numeričkih varijabli za 1. Mogu se primijeniti ispred ili iza imena varijable, ++n; /* uvećava n za 1 */ --n; /* umanjuje n za 1 */ Prefiks operator djeluje na operand prije nego se koristi njegova nova vrijednost. n = 5; x = ++n; /* x je jednak 6, n je jednak 6 */ Postfiks operator djeluje na operand nakon korištenja njegove trenutne vrijednosti. n = 5; x = n++; /* x je jednak 5, n je jednak 6 */ Operandi na koje djeluju operatori ++ i -- moraju biti varijable. Asocijativnost i prioritet djelovanja operatora Kada u izrazima ima više operanada i operatora, redoslijed kojim se računa izraz određen je pravilima prioriteta i asocijativnosti. Prioritet djelovanja operatora određuje koji se podizraz prvi izvodi. Aritmetički operatori imaju sljedeći prioritet izvršenja: viši prioritet unarni operatori - + prefiks op (++ --) ..... binarni operatori * / % niži prioritet binarni operatori + - Primjerice, -2* a + b se izvodi kao da je napisano (((- 2)* a) + b). Asocijativnost određuje redoslijed izvođenja izraza koji imaju više operanada istog prioriteta. Svi aritmetički operatori imaju asocijativnost s lijeva na desno. a + b + c <=> (( a + b) + c) Redoslijed izvođenja se uvijek može predodrediti upotrebom zagrada. Tada se najprije izvršava izraz u zagradama. Kako se vrši potenciranje? U C jeziku ne postoji operator potenciranja. Kada je potrebno potencirati neki broj ili numeričku varijablu, može se koristiti dva postupka: 1. ako se potencira s cijelim brojem tada se potenciranje može realizirati pomoću višestrukog množenja, primjerice a3 se realizira izrazom a*a*a a-3 se realizira izrazom 1/(a*a*a) 72
  • 73.
    2. ako sepotencira s realnim brojem tada se može koristiti standardna funkcija double pow(double x, double y); koja vraća realnu vrijednost koja je jednka xy. Ova funkcija je deklarirana u <math.h>. 6.1.2 Relacijski i logički izrazi Relacijski ili uvjetni izrazi se sastoje se od dva operanda numeričkog tipa i sljedećih operatora: < manje <= manje ili jednako == jednako != nije jednako > veće >= veće ili jednako Rezultat relacijskog izraza je vrijednost 0 ili 1. Primjerice, x = (a == b); /* x je 1, ako je a jednako b, inače x je 0 */ x = (a != b); /* x je 0, ako je a jednako b, inače x je 1 */ x = (a > b); /* x je 1, ako je a veće od b, inače x je 0 */ Pošto u C-u ne postoji logički tip varijabli, nula predstavlja logičku vrijednost false, a nenulta vrijednost predstavlja logičku vrijednost true. Logički operatori su: && logička konjunkcija (i) || logička disjunkcija (ili) ! negacija Djelovanje logičkih operatora se određuje prema pravilu: izraz1 && izraz2 -> 1 ako su oba izraza različita od nule, inače 0 izraz1 || izraz2 -> 0 ako su oba izraza jednaka nuli, inače 1 !izraz -> 0 ako je izraz različit od nule, inače 1 Asocijativnost relacijskih i logičkih operatora je s lijeva na desno, a prioritet je manji od aritmetičkih operatora viši Aritmetički prioritet operatori a + b < max || max == 0 && a == b <, <=, >, >= se izvršava kao: ==, != niži && (( a + b) < max) || (max == 0 && (a == b)) prioritet || Primjer: Godina je prestupna ako je djeljiva sa 4, a ne i s 100, ali godine koje su djeljive s 400 su uvijek prestupne godine. Ta se činjenicu može programski iskazati ne sljedeći način: if ((godina % 4 == 0 && godina % 100 != 0) || godina % 400 == 0) 73
  • 74.
    printf("%d je prestupnagodinan", godina); else printf("%d nije prestupna godina n", godina); Primjer: Definirana je funkcija isupper() kojom se određuje da li neka cjelobrojna vrijednost predstavlja ASCII kod kojim su kodirana velika slova int isupper(int c) /* ukoliko je argument c iz intervala ASCII vrijednosti u kojem su */ /* velika slova, funkcija vraća vrijednost 1, inače vraća 0 */ { return (c >= 'A' && c <= 'Z'); } Primjer: Definirana je funkcija tolower() koja vraća veliko slovo, ako je argument malo slovo. int tolower(int c) /* argument c je vrijednoost iz ASCII skupa * Ako c predstavlja ASCII kod nekog velikog slova, * funkcija vraća vrijednost koja predstavlja * ekvivalentno malo slovo */ { if (isupper(c)) return c + 'a' - 'A'; else return c; } U C jeziku se znakovne konstante tretiraju kao cijeli brojevi Zadatak: Napišite funcije Funkcija vraća vrijednost različitu od nule (true), ako je znak c int isupper(int c); veliko slovo int islower(int c); malo slovo int isalpha(int c); veliko ili malo slovo int iscntrl(int c); kontrolni znak int isalnum(int c); slovo ili znamenka int isdigit(int c); decimalna znamenka int isxdigit(int c); heksadecimalna znamanka int isgraph(int c); tiskani znak osim razmaka int isprint(int c); tiskani znak uključujući razmak int ispunct(int c); tiskani znak osim razmaka, slova ili znamanke int isspace(int c); razmak, tab, vert. tab, nova linija, povrat, nova stranica Ove funkcije su implementirane u standardnoj biblioteci, a njihova deklaracija je dana u datoteci "ctype.h". 6.1.3 Bitznačajni operatori U C jeziku se koristi 6 bitznačajnih operatora, koji se mogu primijeniti na integralne tipove (char, short, int i long) . 74
  • 75.
    & bitznačajni "i" ( AND) | bitznačajni "ili" (OR) ^ bitznačajno "ekskluzivno ili" (XOR) << posmak bitova u lijevo >> posmak bitova u desno ~ bitznačajna negacija (unarni op.) (komplement jedinice) Bitznačajne operacije se provode na bitovima istog značaja. Bitznačajni "i" operator & se najčešće koristi za maskiranje bitova, primjerice nakon naredbe n = n & 0x000F; u varijabli n će svi bitovi biti postavljeni na nula osim 4 bita najmanjeg značaja, bez obzira na vrijednost od n; 1010111000011011 n & 0000000000001111 0x000F ---------------- 0000000000001011 rezultat Bitznačajni "ili" operator | se najčešće koristi za postavljanje bitova, primjerice n = n | 0x000F; ima učinak da se u varijabli n četiri bita najmanjeg značaja postavljaju na vrijednost 1, a ostali bitovi su nepromijenjeni; 1010111000011011 n | 0000000000001111 0x000F ---------------- 1010111000011111 rezultat Bitznačajni "ekskluzivno ili" operator ^ postavlja bitove na vrijednost 1 na mjestima gdje su bitovi oba operanda različiti, odnosno na nulu na mjestima gdje su bitovi oba operanda isti. Posmačni operatori djeluju tako da pomiču bitove udesno (>>) ili ulijevo (<<), primjerice x << 2 daje vrijednost od x s bitovima pomaknutim za dva mjesta udesno ( u 2 prazna mjesta se upisuje 0). Dokažite: Kada posmačni operatori djeluju na varijable unsigned tipa onda pomak bitova za jedno mjesto u lijevo je ekvivalentno množenju s 2, a pomak bitova za jedno mjesto u desno je ekvivalentno dijeljenju s cijelim brojem 2. Primjer: Definirana je funkcija getbit(x,n) kojom se ispituje da li u cijelom broju x n-ti bit ima vrijednost 1. int getbit (unsigned x, int n) { if (n>=0 && n<32) /* unsigned ima 32 bita */ { 75
  • 76.
    return (x &01 << n) != 0; } return 0; } Objasnite primjenu << operatora u ovom primjeru. Primjer: U programu binary.c korisnik unosi cijeli broj, a program ispisuje njegov binarni oblik. /* datoteka: binary.c */ /* program ispisuje binarni kod cijelog broja*/ #include <stdio.h> int main() { int x, i, n; printf("Otkucaj cijeli broj:n"); scanf("%d", &x); n = 8*sizeof(x); printf("Binarni kod je: "); i =n-1; while(i >=0) printf("%d",getbit(x,i--)); printf("n"); return 0; } Ispis je sljedeći: Otkucaj cijeli broj: -2 Binarni kod je: 11111111111111111111111111111110 ili Otkucaj cijeli broj: 67 Binarni kod je: 00000000000000000000000001000011 6.1.4 Složeni operatori pridjele vrijednosti Izraz oblika i = i + 2 u kojem se ista varijabla pojavljuje s obje strane znaka pridjele vrijednosti, može se zapisati u obliku: i += 2 Operator += se naziva složeni operator pridjele vrijednosti. Ovaj oblik se može primijeniti na većinu binarnih operatora: +=, -=, *=, /=, %=, <<=, >>=, &=, ^= i |=, koristeći opće pravilo: Ako su izraz1 i izraz2 neki izrazi, tada izraz1 op= izraz2 76
  • 77.
    je ekvivalentno izraz1 = (izraz1) op (izraz2) pri tome izraz1 mora biti izraz koji označava položaj u memoriji (ime varijable ili dereferencirani pokazivač). Ovi operatori, kao i operator pridjele vrijednosti, imaju niži prioritet od aritmetičkih, relacijskih i logičkih operatora, stoga iskaz x *= y + 1; znači x = x * (y + 1); a ne x = x * y + 1; Primjer: Definirana je funkcija brojbita(x) koja vraća broj bita koji u argumentu x imaju vrijednost 1. int brojbita(unsigned x) /* daje broj bita koji u argumentu x imaju vrijednost 1*/ { int broj=0; while( x != 0) { if (x & 01) broj++; x >>= 1; } return b; } Zadatak: napišite program u kojem korisnik unosi cijeli broj, a program ispisuje broj bita koji su u tom broju različiti od nule. 6.1.5 Ternarni uvjetni izraz Ternarni izraz se sastoji od tri izraza međusobno odvojena upitnikom i dvotočkom: izraz1 ? izraz2 : izraz3 a značenje mu je slijedeće: ako je izraz1 različit od nule, vrijednost ternarnog izraza je jednaka izrazu2, a ako je izraz1 jednak nuli vrijednost ternarnog izraza je jednaka izrazu3. Primjerice u naredbi: max = (x>y) ? x : y; vrijednost varijable max će biti jednaka x ako je x>y, u suprotnom vrijednost od max će biti jednaka vrijednosti varijable y. Ternarni izraz je zapravo skraćeni oblik naredbe selekcije: 77
  • 78.
    if(x>y) max = x; else max = y; međutim, često je prikladnija njegova upotreba od naredbe selekcije jer ga se može koristiti u izrazima. 6.2 Automatska i explicitna pretvorba tipova Automatska pretvorba tipova Svaki izraz daje neku vrijednost čiji tip ovisi o tipu članova izraza. Kada su u nekom izrazu svi članovi i faktori istog tipa tada je i vrijednost izraza tog tipa. Primjerice, za float y = 5, x=2; izraz y/x daje realnu vrijednost 2.5. Ako su x i y cjelobrojne varijable, int y = 5, x=2; tada izraz y/x daje cjelobrojnu vrijednost 2 (ostatak dijeljenja se odbacuje). U C jeziku se svi standardni tipovi tretiraju kao numerički tipovi i može ih se koristiti u svim izrazima. Kada u nekom izrazu ima više različitih tipova tada kompilator u izvršnom kodu vrši automatsku pretvorbu tipova. Princip je da se uvijek izvršava jedna operacija s maksimalno dva operanda. Ako su ta dva operanda različitog tipa onda se prije označene operacije vrši pretvorba tipa niže opsežnosti u tip više opsežnosti. Opsežnost tipa, u redoslijedu od manje prema većoj opsežnosti je: char → int → unsigned → long → float → double. Primjerice, ako se koriste varijable int j=5, k=7; float x=2.1; u izrazu: j+7.1*(x+k) on se izvršava sljedećim redoslijedom: 1. najprije se izvršava proračun izraza u zagradama. U tom izrazu se najprije vrijednost varijable k pretvara (kodira) u tip float, jer je drugi operand tipa float. Zatim se toj vrijednosti dodaje vrijednost varijable x. 2. Vrijednost dobivenog izraza se zatim množi s realnom konstantom 7.1, jer množenje ima viši prioritet od zbrajanja. 3. Konačno preostaje da se zbroji vrijednost varijable j s vrijednošću prethodno izračunatog izraza (7.1*(x +k)), koji je realnog tipa. Pošto je to izraz s dva različita tipa, najprije se vrši pretvorba vrijednosti varijable i u tip float, i tek tada se izvršava operacija zbrajanja. 78
  • 79.
    Pretvorba tipova unaredbi pridjele vrijednosti Pretvorba tipova u naredbi pridjele vrijednosti se uvijek vrši tako da se vrijednost koja se dobije iz izraza koji je na desnoj strani pretvara u tip koji ima varijabla na lijevoj strani. U slučaju da je s lijeve strane tip veće opsežnosti pretvorba se uglavnom može izvršiti bez gubitka točnosti. Primjerice, nakon izvršenja naredbi float x; int i = 3; x = i; printf("x=%f", x); bit će ispisano: x=3.00000. U slijedećem slučaju pretvorba tipa int u tip unsigned neće imati smisla. Nakon izvršenja naredbi: unsigned u; int i = -3; u = i; printf("u=%u", u); bit će ispisano: u= 4294967293. Kada se u izrazima miješaju tipovi int i unsigned, logični rezultat možemo očekivati samo za pozitivne brojeve. Kada se s lijeve strane nalazi tip manje opsežnosti, pretvorba se vrši sa smanjenjem točnošću. Često se vrijednost tipa float ili double pridjeljuje cjelobrojnoj varijabli, primjerice za double d = 7.99; int i ; i = d; printf("i=%d", i); bit će ispisano i = 7. Pravilo je da se pri pretvorbi realnog u cijeli broj odbacuje decimalni dio. To vrijedi bez obzira koliki je decimalni dio. U mnogim programskim zadacima pojavit će se potreba da se pretvorba realnog broja u cijeli broj obavi na način da se vrijednost cijelog broja što manje razlikuje od vrijednosti realnog broja. To znači da ako je d=7.99, tada je poželjno da se ova vrijednost pretvori u cjelobrojnu vrijednost 8. To se može postići tako da se prije pretvorbe u cijeli broj decimalnom broju doda vrijednost 0.5, ako je pozitivan, odnosno da se od decimalnog broja odbije vrijednost 0.5 ako je negativan. U tu svrhu može se definirati funkciju Double2Int(), koja vraća cjelobrojnu vrijednost realnog argumenta; int Double2Int(double x) { /* funkcija vraća cijeli broj koji je * najbliži relnoj vrijednosi x */ if(x>0) 79
  • 80.
    return x+0.5; else return x-0.5; } Ekplicitna pretvorba tipova Ukoliko se ispred nekog izraza ili varijable u zagradama zapiše oznaka tipa, primjerice (float) x time se eksplicitno naređuje kompilatoru da se na tom mjestu izvrši pretvorba vrijednosti varijable x u tip float. Kada se oznaka tipa zapiše u zagradama to predstavlja operator pretvorbe tipa (eng. cast operator). Primjenu ovog operatora ilustrira program u kojem se vrijednost dijeljenja cijelog broja s cijelim brojem pridijeljuje realnoj varijabli. int main() { int i1 = 100, i2 = 40; float f1; f1 = i1/i2; printf("%lfn", f1); return(0); } Dobije se ispis: 2.000000 Pri dijeljenju je izgubljen decimalni dio iako je rezultat izraza i1/i2 pridijeljen realnoj varijabli. Zašto? Zato jer se pretvorba tipa vrši samo ako se u izrazu nalaze različiti tipovi. Pošto su u izrazu i1/i2 oba operanda tipa int izvršava se dijeljenje s cijelim brojevima. Ako želimo da se sačuva i decimalni dio može se primijeniti operator pretvorbe u jednom od tri oblika: f = (float)i1/i2; ili f = i / (float)j; ili f = (float)i / (float)j; Dovoljno je da se pretvorba tipa označi na samo jednom operandu, jer se izrazi računaju tako da se uvijek vrši pretvorba u tip veće opsežnosti. Pokažimo još jedan primjer u kojem je potrebno primijeniti operator pretvorbe tipova short int i = 32000, j = 32000; long li; li = (long)i + j; Operator (long) je primjenjen zbog toga jer maksimalna vrijednosti za tip short int iznosi 32767. Stoga, ako bi se zbrojile dvije short int kodirane vrijednosti iznosa 32000 rezultat bi bio veći od 32767. Operator (long) ispred jednog operanda osigurava da će se zbrajanje izvršiti na način kao da su operandi tipa long. 80
  • 81.
    6.3 Definiranje sinonimatipa pomoću typedef Kada se ispred deklaracije napiše typedef, primjerice typedef int cijelibroj; time se označava da identifikator, u ovom slučaju cijelibroj, neće biti deklariran kao varijabla ili funkcija, već da taj identifikator postaje sinonim za tip koji je opisan deklaracijom. U ovom primjeru, identifikator cijelibroj postaje sinonim za tip int, pa ga se u kasnije može koristiti u drugim deklaracijama, na isti način kako se koristi i originalni tip, primjerice cijelibroj i; /* deklaracija sa typedef tipom */ Važno je napomenuti da se pomoću typedef deklaracije stvaraju sinonimi tipova; a ne neki novi tipovi. Njihova je upotreba korisna za povećanje apstraktnosti programskog zapisa. Prema ANSI standardu, u C jeziku je definirano nekoliko typedef tipova kako bi se jasnije označilo područje njihove primjene. Primjerice, size_t predstavlja tip unsigned int, kojim se često označava veličina, u bajtima, objekata smještenih u datotakama ili u memoriji. Implementacija je provedena deklaracijom typedef unsigned int size_t; u datoteci "stddef.h". Drugi primjeri su FILE, time_t, ptrdiff_t i wchar_t (pogledajte njihovo značenje u opisu standardne C-biblioteke). 6.4 Formalni zapis sintakse C-jezika Pisanje programa podliježe jezičnim pravilima: 1. leksička pravila određuju kako se tvore leksemi na zadanom alfabetu (ASCII skup), 2. sintaktička (gramatička) pravila određuju kojim se redom leksemi slažu u programske iskaze, 3. semantička pravila određuju značenje programskih iskaza. Leksička struktura C jezika se temelji na pravilima koja određuju kako se formiraju leksemi jezika (niz znakova koji čini prepoznatljivu nedjeljivu cjelinu), na zadanom alfabetu (ASCII skup znakova). Temeljne leksičke kategorije su: 1. Ključne riječi jezika (if, while, else, do, int, char, float,..) služe za definiranje programskih iskaza. Pišu se malim slovima. 2. Identifikatori služe za zapis imena varijabli, funkcija i korisničkih tipova. Pišu se pomoću niza velikih i malih slova, znamenki i znaka podvlake ('_'), uz uvjet da prvi znak u nizu mora biti slovo ili podvlaka. 3. Literalne konstante služe za zapis numeričkih i tekstualnih (znakovnih) konstanti (pr. 135, 3.14, 'A', "Hello World"). 81
  • 82.
    4. Operatori (+,-*/,..=,[]..(), &, .+=,*=.) služe označavanju aritmetičko-logičkih i drugih operacija koje se provode sa memorijskim objektima (funkcije i varijable) i konstantama. 5. Leksički separatori su znakovi koji odvajaju lekseme. Jedan ili više znakova razmaka, tabulatora i kraja retka tretiraju se kao prazno mjesto, kojim se razdvajaju leksemi. Operatori, također, imaju značaj leksičkih separatora. Znak točka-zarez (';') predstavlja specijalni separator koji se naziva terminator naredbi. 6. Komentar se piše kao proizvoljni tekst. Početak komentara se označava znakovima /*, a kraj komentara s */. Komentar se može pisati u bilo kojem dijelu programa, i u više linija teksta. Mnogi kompilatori kao komentar tretiraju i tekst koji se unosi iza dvostruke kose crte //, sve do kraja retka. 7. Specijalne leksičke direktive su označene znakom # na početku retka. Izvršavaju se prije procesa kompiliranja, pa se nazivaju i pretprocesorske direktive. Primjerice, #include <stdio.h> je pretprocesorska direktiva kojom se određuje da se u proces kompiliranja uvrsti sadržaj datoteke imena stdio.h. Kao što se zapis u prirodnom jeziku sastoji od različitih elemenata (subjekt, predikat, pridjev, rečenica, poglavlje itd.), tako se i zapis u programskom jeziku sastoji od temeljnih elemenata, koje prikazuje tablica 6.2. Elementi programa Značenje Primjer Tipovi oznake za skup vrijednosti int , float , s definiranim operacijama char Konstante literalni zapis vrijednosti 0 , 123.6 , osnovnih tipova "Hello" Varijable imenovane memorijskih lokacije koje i , sum sadrže vrijednosti nekog tipa Izrazi zapis proračuna vrijednosti kombiniranjem sum + i varijabli, funkcija, konstanti i operatora Naredbe ili iskazi zapisi pridjele vrijednosti, poziva funkcije i sum = sum + i; kontrole toka programa while (--i) if(!x).. else ..; Funkcije imenovano grupiranje naredbi main() (potprogrami) printf(...) Kompilacijska skup međuovisnih varijabli i funkcija koji datoteka.c jedinica se kompilira kao jedinstvena cjelina Tablica 6.2 Temeljni elementi zapisa programa u C jeziku Navedeni elementi jezika se iskazuju kombinacijom leksema prema strogim gramatičkim, odnosno sintaktičkim pravilima, koji imaju nedvosmisleno značenje. U prirodnim jezicima iskazi mogu imati više značenja, ovisno o razmještaju riječi, o morfologiji (tvorba riječi) i fonetskom naglasku. U programskim jezicima se ne koristi morfološka i fonetska komponenta jezika, pa se gramatika svodi na sintaksu, također, dozvoljen je samo onaj raspored riječi koji daje nedvosmisleno značenje. Uobičajeno se kaže da gramatika programskih jezika spada u klasu bezkontesktne gramatike. 82
  • 83.
    Slika 2.1. Osnovnefaze u procesu kompiliranja Za opis sintakse nekog jezika koristi se posebni jezik koji se naziva metajezik. Jezik koji se opisuje metajezikom naziva se ciljni jezik. Za opis semantike nekog jezika ne postoje prikladni metajezici već se semantika izražava opisno, primjenom prirodnih jezika. Prije nego se izvrši opis metajezika, koji će biti upotrijebljen za opis sintakse C jezika, bit će opisani neki pojmovi iz teorije programskih jezika. Na slici 2.1 ilustriran je proces kompiliranja. On se odvija na sljedeći način. Izvorni kod može biti spremljenu u jednoj datoteci ili u više datoteka koje se u toku jezičkog pretprocesiranja formiraju kao jedna datoteka, koja se naziva kompilacijska jedinica. Zatim se vrši leksička analiza izvornog koda, na način da se izdvoje leksemi (nizovi znakova koji predstavljaju nedjeljivu cjelinu). Ukoliko je leksem zapisan u skladu s leksičkom strukturom jezika on predstavlja terminalni simbol jezika (token) kojem se u radu kompilatora pridjeljuje jedinstveno značenje. U jezičke simbole spadaju: ključne riječi (if, else, while,...), specijalni simboli (oznake operatora i separatora), identifikatori (imena varijabli, konstanti, funkcija, procedura i labele), literalne numeričke i tekstualne konstante. Pojedinom simbolu pridjeljuju se različiti atributi koji se koriste u procesu generiranja koda. Primjerice, za varijable se unosi atribut koji opisuje tip varijable, ili uz literalno zapisanu numeričku konstantu se unosi i binarno kodirana numerička vrijednost konstante. Sintaktički analizator (parser) dobavlja jezičke simbole i određuje da li su oni grupirani u skladu s definiranom sintaksom. Ukoliko je to zadovoljeno, vrši se prevođenje u objektni kod usklađeno sa semantikom jezika. Pogreške u procesu kompiliranja se dojavljuju kao: • leksičke pogreške (pr. nije ispravno zapisano ime varijable) • sintaktičke pogreške (pr. u aritmetičkom izrazu nisu zatvorene zagrade) • semantičke pogreške (pr. primijenjen operator na dva nekompatibilna operanda) U programu mogu biti prisutne i logičke pogreške (pr. petlja se ponavlja beskonačno). Njih može otkriti korisnik tek prilikom izvršenja programa. Za pojašnjenje navedenih pojmova razmotrimo iskaz: if (a > 3) max = 5.4; else max = a; 83
  • 84.
    Ovaj iskaz predstavljaispravno zapisani sintaktički entitet - IskazIf. U njemu se pojavljuju sljedeći simboli: ključne riječi (if, then, else), operatori (>, =), identifikatori varijable (a i max), numeričke konstante (3 i 5.4) i terminator iskaza (;). Napomenimo da "razmak" predstavlja leksički separator. On se ne smatra simbolom jezika i može se umetnuti između leksema proizvoljan broj puta. Odnos leksema, tokena i atributa prikazuje donja tablica. Leksem kategorija tokena atribut "if", "else" ključna riječ - "max", "a" Identifikator varijabla "=", ">" operatori - ";" terminator naredbe ( ...) separator izraza "5.4", "3" konstanta numerička vrijednost: 5.4 i 3 Za IskazIf u C jeziku vrijedi sintaktičko pravilo: IskazIf "je definiran kao" if (Izraz) Iskaz else Iskaz "ili kao" if (Izraz) Iskaz Gornji iskaz zadovoljava ovo sintaktičko pravilo jer (a>3) predstavlja relacijski izraz, dakle predstavlja sintaktički entitet Izraz, a iskazi x=5.4; i x=a; predstavljaju iskaze dodjele vrijednosti, dakle pripadaju sintaktičkom entitetu Iskaz. Ako se izneseno sintaktičko pravilo shvati kao zapis u nekom sintaksnom metajeziku onda IskazIf, Izraz i Iskaz predstavljaju metajezičke varijable koje u odnosu na ciljni jezik predstavljaju neterminalne simbole, "je definiran kao" i "ili kao" su metajezički operatori, a leksemi: if, then i else i znakovi zagrada su metajezičke konstante koje odgovaraju simbolima ciljnog jezika, pa se nazivaju terminalni simboli ili tokeni. Uočimo da "ili kao" operator ima značaj logičkog operatora ekskluzivne disjunkcije. Sintaktička pravila, kojima se jedan neterminalni simbol definira pomoću niza terminalnih i/ili neterminalnih simbola, nazivaju se produkcije jezika. Prema ANSI/ISO standardu produkcije C-jezika se zapisuju na sljedeći način: 1. Operator "je definiran kao" je zamijenjen znakom dvotočke, a produkcije imaju oblik: neterminalni_simbol : niz terminalnih i/ili neterminalnih simbola 2. Alternativna pravila ("ili kao") se pišu u odvojenim redovima. 3. Neterminalni simboli se pišu kurzivom. 4. Terminalni simboli se pišu na isti način kao u ciljnom jeziku 5. Opcioni simboli se označavaju indeksom opt (Simbolopt ili Simbolopt). Primjerice, zapis produkcije if-else iskaza glasi IskazIf : if (Izraz) Iskaz else Iskaz if (Izraz) Iskaz Ovo se pravilo može se napisati i na sljedeći način: IskazIf : if (Izraz) Iskaz ElseIskazopt ElseIskaz : else Iskaz 84
  • 85.
    U prvom jepravilu uveden je ElseIskaz kao opcioni neterminalni simbol. Ako postoji, onda je njegova sintaksa opisana drugim pravilom, a ako ne postoji onda prvo pravilo predstavlja pravilo proste uvjetne naredbe. Gornja pravila ćemo proširiti na način da se operator "ili kao" može eksplicitno označiti okomitom crtom (|), zbog dva razloga: 1. Na taj način gornja pravila (1-4) su ekvivalentna popularnoj BNF notaciji (BNF notacija je metajezik razvijen 1960. godine prilikom definicije programskog jezika ALGOL, pri čemu su bitne doprinose dali J.W.Bakus i P.Naur, pa BNF predstavlja kraticu za "Backus-ova normalna forma" ili "Backus-Naur-ova forma"). 2. Na taj način se alternativne produkcije mogu pisati u istom redu Pomoću prethodno definiranih pravila lako se može definirati i leksička struktura jezika. Primjerice, temeljni se leksički objekti znamenka i slovo mogu definirati pravilima: slovo : A⎪B⎪C⎪D⎪E⎪F⎪G⎪H⎪I⎪J⎪K⎪L⎪M⎪N⎪O⎪P⎪Q⎪R⎪S⎪T⎪U⎪V⎪W⎪X⎪Y⎪Z ⎪a⎪b⎪c⎪d⎪e⎪f⎪g⎪h⎪i⎪j⎪k⎪l⎪m⎪n⎪o⎪p⎪q⎪r⎪s⎪t⎪u⎪v⎪w⎪x⎪y⎪z. znamenka : 0⎪1⎪2⎪3⎪4⎪5⎪6⎪7⎪8⎪9. heksa_znamenka : 0⎪1⎪2⎪3⎪4⎪5⎪6⎪7⎪8⎪9⎪A⎪B⎪C⎪D⎪E⎪F⎪a⎪b⎪c⎪d⎪e⎪f. oktalna_znamenka : 0⎪1⎪2⎪3⎪4⎪5⎪6⎪7⎪. Koristeći objekte znamenka i slovo može se definirati objekt znak (koji može biti slovo ili znamenka): znak : znamenka ⎪ slovo. Sintaksa znaka može po potrebi biti i drugačije definirana, naročito ukoliko se pod pojmom znak mogu koristiti i specijalni znakovi, ili još i šire, cijela ASCII kolekcija simbola. Vrlo često, potreban element jezika je niz znakova. Njega se može definirati korištenjem rekurzivnog pravila: niz_znakova : znak ⎪ niz_znakova znak, što se tumači na sljedeći način: niz znakova je ispravno zapisan ako sadrži samo jedan znak ili ako sadrži niz znakova i s desne strane još jedan znak. Dakle, alternativno pravilo prepoznaje sve nizove koji imaju dva ili više znakova. Može se napisati i slijedeće: niz_znakova : znak ⎪ znak niz_znakova, što se tumači ovako: niz znakova je ispravno zapisan ako sadrži samo jedan znak ili ako iza znaka sadrži niz znakova. Uočimo da alternativno pravilo, također, prepoznaje nizove koji sadrže dva ili više znakova. Identifikatori u C-jeziku (nazivi varijabli, labela, funkcija i tipova) moraju početi sa slovom ili znakom podvlake '_', pa vrijedi : identifikator : slovo | _ | identifikator slovo | identifikator znamenka | identifikator _ 85
  • 86.
    Na osnovu ovogpravila, kao ispravno zapisani identifikatori, ocjenjuju se: BETA7 , A1B1 , x , xx , xxx , dok sljedeći zapisi ne predstavljaju indetifikatore: 7, A+B , 700BJ , -beta , x*5 , a=b , x(3). Pod pojmom liste identifikatora podrazumijeva niz identifikatora međusobno razdvojenih zarezom. lista_identifikatora : identifikator ⎪ identifikator , lista_identifikatora U Dodatku B dana je potpuna specifikacija sintakse C jezika. 86
  • 87.
    7 Proste istrukturalne naredbe C jezika Naglasci: • proste i strukturalne naredbe • naredbe bezuvjetnog skoka i označene naredbe • naredbe s logičkom i cjelobrojnom selekcijom • tipovi petlji i beskonačne petlje Naredbe su programski iskazi pomoću kojih se kontrolira izvršenje programa. Prema razini apstrakcije računarskog procesa, kojeg predstavljaju, dijele se na proste naredbe i strukturalne naredbe. U prethodnim poglavljima se korištene strukturalne naredbe tipa sekvence, selekcije (if-else-naredba) i iteracije (while-petlja), te proste naredbe pridjele vrijednosti i poziva potprograma. Interesantno je napomenuti da se pomoću tih naredbi može napisati bilo koji algoritam koji je moguće izvršiti računalom. U ovom poglavlju će biti opisane sve naredbe C jezika koje se koriste za kontrolu toka programa. 7.1 Proste naredbe Proste ili primitivne naredbe su one naredbe koje odgovaraju naredbama strojnog jezika. U C jeziku "najprostija" naredba je izraz iza kojeg se napiše znak točka-zarez. Takova naredba se naziva naredbeni izraz. Sintaksa naredbe je: naredbeni izraz : izrazopt ; Primjerice, 1+3*7; je naredba izračun vrijednosti izraza 1+3*7. Kada računalo izvrši operacije opisane ovim izrazom, rezultat će ostati negdje u memoriji ili u procesoru računala, stoga ova naredba nema nikakvog smisla. Ako se pak napiše naredba x = 1+3*7; tada će rezultat biti spremljen u memoriji na adresi koju označava varijabla imena x. Do sada je ovakva naredba nazivana naredba pridjele vrijednosti, jer se ona tako naziva u većini programskih jezika. U C jeziku se ova naredba zove naredbeni izraz pridjele vrijednosti, jer znak = predstavlja operator pridjele vrijednosti koji se može koristiti u izrazima. Primjerice, u naredbi x = 3 + (a=7); znak = se koristi dva puta. Nakon izvršenja ove naredbe vrijednost varijable a je 7, a vrijednost varijable x je 10. Prema iznesenom sintaktičkom pravilu naredbom se smatra i znak točka-zarez: ; /* ovo je naredba C jezika */ 87
  • 88.
    Ova se naredbanaziva nulta ili prazna naredba. Njom se ne izvršava nikakvi proces. Makar to izgledalo paradoksalno, ovu se naredbu često koristi, i često je uzrok logičkih pogreški u programu. Zašto se koristi i kako nastaju pogreške zbog korištenja ove naredbe bit će pokazano kasnije. Upoznavanje s naredbenim izrazima završit će sa sljedećim primjerima prostih naredbi: x++; /* povećaj vrijednost x za 1 */ --x; /* umanji vrijednost x za 1 */ printf("Hi"); /* poziv potprograma */ x=a+3.14+sin(x); /* kompleksni izraz s pozivom funkcije */ Posljednju naredbu, u kojoj se računa kompleksni izraz, s pozivom funkcije sin(x), moglo se zapisati pomoću više naredbenih izraza: x = a; /* vrijednost od a pridijeli varijabli x */ x += 3.14; /* uvećaj x za 3.14 */ tmp=sin(x); /* pomoćnoj varijabli tmp pridijeli vrijednost */ /* koju vraća funkcija sin(x) */ x += tmp; /* uvećaj x za vrijednost varijable tmp */ Operacijska semantika, odnosno način kako se naredbe izvršavaju u računalu, u oba zapisa je potpuno ista, jer C prevodilac složene izraze razlaže u više prostih izraza koji se mogu direktno prevesti u strojni kôd procesora. U proste naredbe spadaju još naredbe bezuvjetnog i uvjetnog skoka. Pomoću ovih naredbi se može eksplicitno zadati da se izvršenje programa nastavi naredbom koja je označena nekim imenom. Sintaksa označene naredbe je: označena_nareba : identifikator : naredba Identifikator kojim se označava neka naredba često se naziva programska labela. Sintaksa naredbe bezuvjetnog skoka je: naredba_skoka : goto identifikator ; Semantika naredbe je da se izvrši skok, odnosno da se izvršenje programa nastavi naredbom koja je označena identifikatorom i znakom dvotočke. Primjerice, u nizu naredbi: goto next; naredba2 next: naredba3 nikad se neće izvršiti naredba2, jer se u prethodnoj naredbi vrši bezuvjetni skok na naredbu koja je označena identifikatorom next. Skok se može vršiti i unatrag, na naredbe koje su već jednom izvršene. Na taj način se mogu realizirati iterativni procesi – petlje. Naredba skoka i naredba na koju se vrši skok, moraju biti definirani unutar iste funkcije. Zapis naredbe uvjetnog skoka, koji se izvodi na temelju ispitivanja logičke vrijednosti nekog izraza, je: if ( izraz ) goto identifikator ; 88
  • 89.
    što znači: akoje izraz logički istinit (različit od nule) vrši se skok na označenu naredbu, a ako nije izvršava se slijedeća naredba. Naredbe uvjetnog i bezuvjetnog skoka vjerno opisuju procese u računalu, međutim njihova se upotreba ne preporučuje. Sljedeći primjer pokazuje zašto programeri "ne vole goto naredbu". Razmotrimo zapis: if (izraz) goto L1; goto L2; L1: naredba L2: ..... U prvoj se naredbi ispituje vrijednost izraza. Ako je on različit od nule, izvršava se naredba označena s L1, u suprotnom izvršava se naredba goto L2. Primjenom logičke negacije na izraz u prvoj naredbi dobije se ekvivalentni algoritam: if (!izraz) goto L2: naredba L2: ...... Mnogo jednostavnije se ovaj programski tijek zapisuje tzv. uvjetnom naredbom: if (izraz) naredba Ova naredba spada u strukturalne naredbe selekcije. Ona, već na "prvi pogled", jasno iskazuje koji proces treba izvršiti. Ako se pak pogleda prethodna dva zapisa, u kojima je korištena goto- naredba, trebat će znatno više mentalnog napora za razumijevane opisanog procesa. Ovaj problem posebno dolazi do izražaja kod većih programa, gdje primjena goto naredbe dovodi do stvaranja nerazumljivih i "zamršenih" programa. Jedino kada se može opravdati upotreba goto naredbe jest kada se želi napisati algoritam koji treba biti "ručno" preveden na asemblerski jezik. U svim ostalim slučajevima, u programiranju i u razvoju algoritama, treba koristiti naredbe selekcije i petlje kojima se dobija jasna i pregledna struktura programa. 7.2 Strukturalne naredbe 7.2.1 Složena naredba ili blok Pod pojmom složene naredbe podrazumijeva se niz naredbi i deklaracija napisan unutar vitičastih zagrada. Naziva se i blok jer se u okviru neke druge strukturalne naredbe može tretirati kao cjelina. Lijeva zagrada '{' označava početak, a desna zagrada '}' označava kraj bloka. Sintaksa složene naredbe je: složena-naredba : { niz-deklaracijaopt niz-naredbiopt } Unutar bloka dozvoljeno je deklarirati varijable, ali samo na mjestu neposredno iza vitičastih zagrada. Pogledajmo programski odsječak u kojem se vrši zamjena vrijednosti dvije varijable x i y. int x, y; ........ 89
  • 90.
    x=7; y=5; { int tmp; /* tmp je lokalna varijabla bloka*/ tmp = x; /* tmp == 7 */ x = y; /* x == 5 */ y = tmp; /* y == 7 */ } printf("x=%d, y=%d", x, y) Zamjena vrijednosti se vrši pomoću varijable tmp, koja je deklarirana unutar bloka, i koja ima karakter lokalne varijable, što znači da joj se ime može koristiti samo unutar bloka. Uočite da se najprije vrijednost od x upisuje u tmp. Zatim se varijabli x pridjeljuje vrijednost varijable y, i konačno se varijabli y pridjeljuje vrijednost od x, koja je bila sačuvana u varijabli tmp. Nakon izlaska iz bloka nije potrebna varijabla tmp. U C-jeziku se automatski obavlja odstranjenje iz memorije lokalnih varijabli po izlasku iz bloka u kojem su definirane. Kasnije će o ovom problemu biti više govora. Sa semantičkog stajališta blok analizirano kao niz deklaracija i naredbi, dok u analizi sintakse i strukture programa, blok predstavlja jedinstvenu naredbu. To ujedno znači da u zapisu sintakse, na svakom mjestu gdje pišemo naredba, podrazumijeva se da može biti napisana i prosta i složena naredba i ostale strukturalne naredbe. 7.2.2 Naredbe selekcije Općenito se pod selekcijom nazivaju programske strukture u kojima dolazi do grananja programa, a nakon prethodnog ispitivanja vrijednosti nekog izraza. U C jeziku se koriste se tri tipa naredbi selekcije: 1. Uvjetna naredba (if- naredba) 2. Uvjetno grananje (if-else naredba) 3. Višestruko grananje (switch-case naredba) U prva dva tipa naredbi grananje se vrši na temelju ispitivanja logičke vrijednosti nekog izraza, a u switch-case naredbi grananje može biti višestruko, ovisno o cjelobrojnoj vrijednosti nekog selektorskog izraza. Uvjetna naredba (if-naredba) Sintaksa if-naredbe je : if_naredba: if ( izraz ) naredba gdje naredba može biti bilo koja prosta, složena ili strukturalna naredba. Značenje naredbe je: ako je izraz različit od nule se izvršava naredba, a ako je izraz jednak nuli, program se nastavlja naredbom koja slijedi iza if-naredbe. 90
  • 91.
    Slika 7.1 Dijagramtoka if- naredbe Uzmimo primjer da analiziramo dvije varijable : x i y. Cilj je odrediti koja je od te dvije vrijednosti manja, a zatim tu manju vrijednost upisati u varijablu imena min. To se može ostvariti naredbama: min = y; /* pretpostavimo da je y manje od x */ if (x < y) /* ako je x manje od y */ min = x; /* minimum je jednak x-u */ Uvjetno grananje (if-else naredba) Sintaksa if-else naredbe je if-else-naredba: if ( izraz ) naredba1 else naredba2 gdje naredba1 i naredba2 predstavljaju bilo koji prostu, složenu ili strukturalnu naredbu. Značenje if-else-naredbe je: ako je izraz različit od nule, izvršava se naredba1, inače izvršava se naredba2. Primjerice, iskaz: if (x < y) min = x; else min = y; omogućuje određivanje minimalne vrijednosti. Slika 7.2 Dijagram toka if-else naredbe 91
  • 92.
    Uzmimo sada daje potrebno odrediti da li je vrijednost varijable x unutar intervala {3,9}. Problem se može riješiti tako da se unutar if-else naredbe, u kojem se ispituje donja granica intervala, umetne if-else naredba kojom se ispituje gornja granica intervala. Tri sintaktički i semantički ekvivalentna if-else iskaza (nakon izvršenja, varijabla unutar ima vrijednost 1 ako je x unutar intervala {3,9}, inače ima vrijednost 0.) if (x >= 3) if (x >= 3) { if (x <= 9) if (x <= 9) unutar = 1; unutar = 1; else unutar = 0; else } unutar = 0; else else unutar = 0; unutar = 0; if (x >= 3) if (x <= 9) unutar = 1; else unutar = 9; else unutar = 0; U prvom se zapisu može, bez dodatnih pojašnjenja, znati pripadnost odgovarajućih if i else naredbi, jer vitičaste zagrade označavaju da umetnuta if-else naredba predstavlja prosti umetnuti iskaz. Ta pripadnost nije očita u drugom, i posebno ne u trećem zapisu, iako su to sintaktički potpuno ispravni C iskazi, jer se u C-jeziku koristi pravilo da "else naredba" pripada najbližem prethodno napisanom "if uvjetu". Dobar je programerski stil da se umetnuti iskazi pišu u odvojenim redovima s uvučenim početkom reda, kako bi se naglasilo da se radi o umetnutom iskazu. “Uvlačenje redova” je stil pisanja programskih algoritama kojim se dobiva bolja preglednost strukture programskih iskaza. Prije izneseni problem se može riješiti korištenjem samo jedne if-else naredbe: if (x >= 3 && x <= 9 ) unutar = 1; else unutar = 0; Pregledom ovog iskaza već se na prvi pogled može utvrditi koju radnju obavlja, jer se u početku if naredbe ispituje puni interval pripadnosti x varijable. Često je moguće smanjiti broj umetnutih if-else naredbi uvođenjem prikladnih logičkih izraza. U programiranju se često pojavljuje potreba za višestrukim selekcijama. Primjerice, dijagram toka na slici 7.3 prikazuje slučaj u kojem se ispituje više logičkih izraza (L1,L2,..Ln). Ukoliko je ispunjen uvjet Li izvršava se naredba Ni, a ukoliko nije ispunjen ni jedan uvjet izvršava se naredba Ne. 92
  • 93.
    Slika 7.3 Dijagramtoka za višestruku logičku selekciju Programski se ovakva selekcija može realizirati pomoću umetnutih if-else naredbi u obliku: if (L1) N1 else if (L2) N2 ...... else if (Ln) Nn else Ne Primjer: U programu ifelseif.c od korisnika traži da odgovori na upit: Predjednik SAD je: (a) Bill Clinton (b) Bill Gates (c) Bill Third Otipkaj slovo. Ako korisnik pritisne malo ili veliko slovo 'a', program ispisuje poruku "Točno". U slučaju (b) i (c) poruka treba bit "Netočno". Ako pritisne slovo različito od 'a','b' ili 'c', tada se ispisuje poruka "Otipkali ste pogrešno slovo". Za dobavu znaka s tipkovnice koristi se standardna funkcija getchar(), koja vraća ASCII kôd pritisnte tipke. /* Datoteka: ifelseif.c ---------------*/ /* Primjer višestruke logičke selekcije */ #include <stdio.h> int main(void) { char ch; printf(" Predjednik SAD je:n"; printf(" (a) Bill Clintonn (b) Bill Gatesn (c) Bill Thirdn"); printf("nOtipkaj slovo.n"); ch=getchar(); if (ch =='A' || ch =='a') printf ("Tocnon"); else if (ch =='B' || ch =='b') printf ("Nije tocnon"); else if (ch =='C' || ch =='c') printf ("Nije tocnon"); else printf("Otipkali ste pogresno slovo"; return 0; } 93
  • 94.
    Višestruko grananje (switch-casenaredba) Prethodne selekcije su vršene na temelju ispitivanja logičke vrijednosti nekog izraza. Sada će biti predstavljena switch-case naredba pomoću koje se selekcija grananja vrši na temelju ispitivanja cjelobrojne vrijednosti nekog izraza kojeg se naziva selektorski izraz. Logika switch- case naredbe je prikazana na slici 7.4. Slika 7.4 Prikaz selekcije u switch-case naredbi U dijagramu toka "selektorski" izraz je označena sa sel. Skup {a,b,..z} podrazumjeva skup različitih konstanti cjelobrojnog tipa.U slučaju da izraz sel poprimi vrijednost konstante “a” izvršava se naredba Na. Ako izraz sel ima vrijednost konstante “b” izvršava se naredba Nb. Ako izraz sel ima vrijednost iz skupa {z1,z2,..z3} izvršava se naredba Nz, a ako vrijednost selektorskog izraza nije iz skupa {a,b,..,z1,z2,..z3} izvršava se naredba Nx. To se u C jeziku zapisuje iskazom: switch (sel) { case a: Na; break; case b: Nb; break; .... case z1: case z2: case z3: Nz; break; default: Nx; } 94
  • 95.
    Naredba break predstavljanaredbu skoka na prvu naredbu izvan aktivnog bloka. Ime break (prekini) simbolički označava da naredba break "prekida" izvršenje naredbi u aktivnom bloku. Ukoliko iza označene case-naredbe nije navedena break-naredba, nastavlja se ispitivanje slijedeće case-naredbe. Ukoliko u ni jednoj case-naredbi nije pronađena vrijednost konstante koja je jednaka vrijednosti selektorskog izraza, izvršava se naredba koja je označena s default. Semantiku prethodnog iskaza pokazuje ekvivalentna if-else konstrukcija: if (sel == a) Na; else if(sel == b) Nb; else if (sel == z1 || sel == z2 || sel == z3 ) Nz; else Nx; U sljedećem primjeru dan je programski fragment kojim se ispituje vrijednost broja kojeg unosi korisnik. unsigned x; scanf("%d", &x); switch (x) { case 1: printf("otkucali ste broj 1") break; case 2: case 3: case 4: case 5: printf("otkucali ste jedan od brojeva: 2,3,4,5"); break; default: printf("otkucali ste 0 ili broj veći od 5"); } Sintaksa switch-naredbe je prema ANSI standardu dosta slobodno definirana: switch-naredba: switch ( izraz ) naredba pa bi prema tom pravilu za naredbu mogla biti zapisana bilo koja naredba. Međutim, semantički ima smisla koristiti samo tzv. označene-naredbe: case konstanti-izraz : naredba i default : naredba te break naredbu, kojom se izlazi iz switch-bloka . Prema prethodnom pravilu sintaktički je potpuno ispravna naredba switch(n) case 1: printf(" n je jednak 1n"); Njome je iskazano da će biti ispisano "n je jednak 1" u slučaju kada je n jednako 1. Ova naredba nema praktičnog smisla, jer switch-naredba nije efikasno rješenje za ispitivanje samo jednog slučaja. Tada je bolje koristiti if-naredbu. U slučaju kada se koristi više označenih naredbi (što je redovit slučaj) sintaksa switch-case naredbe se može zapisati u znatno razumljivijem obliku: 95
  • 96.
    niz-case-naredbi: switch-case-naredba: case-naredba switch ( izraz ) { | niz-case-naredbi case-naredba niz-deklaracijaopt case-naredba: niz-case-naredbi case konstanti-izraz : niz-naredbiopt prekidopt default-naredbaopt default-naredba: } default: niz-naredbiopt prekid: break ; 7.2.3 Naredbe iteracije - petlje Iterativni procesi ili petlje su procesi u kojima se ciklički ponavlja programski kod koji je definiran unutar petlje, sve dok za to postoje potrebni uvjeti. Uvjete ponavljanja ili izlaska iz petlje postavlja programer. S obzirom na način kako su postavljeni uvjeti izlaska iz petlje, definirani su slijedeći tipovi petlji: 1. Petlje s uvjetnim izlazom na početku petlje 2. Petlje s uvjetnim izlazom na kraju petlje 3. Petlje s višestrukim uvjetnim izlazom 4. Beskonačne petlje Pokazat ćemo kako se ovi tipovi petlji realiziraju u C jeziku. Slika 7.5 Petlje Za iskaze petlje, kao i za sve strukturalne iskaze, vrijedi da unutar njih mogu biti definirani svi tipovi strukturalnih iskaza. Prema tome, unutar petlje može biti definiran proizvoljan broj umetnutih petlji. Preklapanje strukturalnih iskaza nije dozvoljeno. Petlja s uvjetnim izlazom na početku petlje (while i for petlje) Sintaksa while naredbe glasi: while-naredba: while ( izraz ) naredba Značenje je: dok je (eng. while) izraz različit od nule, izvršava se naredba. Izraz predstavlja uvjet ponavljanja petlje. 96
  • 97.
    Slika 7.6 Dijagramtoka while petlje Uvjet ponavljanja petlje se ispituju na samom ulazu u petlju. Postoji mogućnost da se naredba uopće ne izvrši ako je početno izraz jednak 0 . Primjerice, za izračunavanje vrijednosti f (n) = n! (n≥0), mogu se koristiti iskazi: i = 1; i = n; f = 1; f = 1; while (i < n) while (i > 1) { { i++; f *= i; f *= i; i–-; } } Ako je n<2 naredbe unutar while petlje se ne izvršavaju, stoga je prije početka petlje definirano da f ima vrijednost 1. Kontrola izvršenja petlje obavlja se pomoću cjelobrojne kontrolne varijable i, kojoj se vrijednost iterativno uvećava za 1 (u drugom iskazu se smanjuje za 1). U principu, može se koristiti više kontrolnih varijabli. Prije početka while-petlje gotovo uvijek treba inicirati vrijednost neke kontrolne varijable, koja se koristi u uvjetu ponavljanja petlje. Stoga se može napisati obrazac korištenja while- petlje u obliku: iniciraj_kontrolne_varijable while ( izraz_s_kontrolnim_varijablama ) { niz_naredbiopt naredba_promjene_vrijednosti_kontrolnih_varijabliopt niz_naredbiopt } U nekim slučajevima je potrebno da naredba_promjene_vrijednosti_kontrolnih_varijabli bude prva naredba u petlji, u nekim slučajevima ona će biti posljednja naredba ili pak umetnuta naredba. Za zapis procesa u kojima je naredba_promjene_vrijednosti_kontrolnih_varijabli posljednja naredba petlje često je prikladnije koristiti for – petlju. For-petlja se zapisuje tako da se iza ključne riječi for u zagradama zapišu tri izraza međusobno odvojena točka-zarezom, a iza njih naredba koja čini tijelo petlje: for ( izrazopt ; izrazopt ; izrazopt ) naredba 97
  • 98.
    Primjerice, segment programau kojem se računa n –faktorijela, se može zapisati pomoću for- petlje u obliku: f=1 for (i=n; i > 1; i--) f *= i; U prvom naredbenom izrazu se inicira kontrolna varijabla i na vrijednost n, u drugom izrazu se zapisuje uvjet ponavljanja pelje (i>1), a u trećem izrazu se zapisuje naredbeni izraz kojim se definira promjena kontrolne varijable pri svakom ponavljanju petlje(i--). Semantiku for-petlje može se objasniti pomoću ekvivalentne while-petlje izrazopt ; while ( izrazopt ) { naredba izrazopt; } U svakom od ovih izraza može se navesti više izraza odvojenih zarezom. Primjerice, za proračun n-faktorijela (f=n!) vrijede ekvivalentni iskazi: (1) f=1; for(i=2; i<=n; i=i+1) f=f*i; (2) for(f=1, i=2; i<=n; i=i+1) f=f*i; (3) for(f=1, i=2; i<=n; f=f*i, i=i+1) ; Drugi iskaz koristi listu izraza za početne uvjete, a u trećem je iskazu čak i naredba iz bloka petlje f=f*i uvrštena u listu izraza iteracije. Samim time, naredba petlje je transformirana u nultu naredbu (tj. naredbu koja stvarno ne postoji, ali je sintaktički prisutna postavljanjem znaka točka-zarez kao znaka za završetak naredbe). Ovaj primjer ujedno ukazuje na jednu od najčešćih pogrešaka pri pisanju programa u C jeziku, a to je u slučaju kada se točka-zarez napiše odmah iza zagrada for naredbe. Time prestaje djelovanje for petlje na naredbu koja je definirana iza zagrada jer točka-zarez označava kraj naredbe makar to bila i nulta naredba. Ista pogreška se često javlja kod zapisa if i while naredbi. Postavlja se pitanje: kada koristiti for-naredbu, a kada koristiti while-naredbu. U C jeziku je to pitanje jezičkog stila, jer su to dvije ekvivalentne naredbe. Petlje s uvjetnim izlazom na kraju petlje ( do-while naredba) Sintakse do-while naredbe je: do-while-naredba: do naredba while ( izraz ) ; 98
  • 99.
    Izraz predstavlja uvjetza ponavljanje petlje. Značenje je: izvrši naredbu, a zatim ponavljaj tu naredbu dok je izraz logički istinit. Temeljna karakteristika ove naredbe je da se naredbe u tijelu petlje izvršavaju barem jedan put. Slika 7.7 Dijagram toka do-while petlje Primjer: Izrada jednostavnog izbornika. /* Datoteka: do-while.c * Primjer do while petlje *****************************/ #include <stdio.h> int main(void) { char ch; do { printf("Predjednik SAD je:n"); printf("(1) Bill Clintonn(2) Bill Gatesn(3) Bill Thirdn"); printf("nOtipkaj 1, 2 ili 3 <enter>!n"); ch = getchar(); }while( ch != '1' && ch != '2'&& ch != '3'); if(ch == '1') printf("Tocnon"); else printf("Nije tocnon"); return 0; } Naredbe za prekid i djelomično izvršenja petlje (break i continue) U C jeziku iskaz break; predstavlja i naredbu za prekid petlje, a iskaz continue; predstavlja naredbu za povrat na početak petlje. Logika break iskaza je: Pseudo-asembler C jezik L1: početak petlje početak petlje { ....... ....... 99
  • 100.
    if (L) gotoL2; if (L) break; ....... ....... kraj petlje } kraj petlje L2: ........ ...... Prekid petlje, sam za sebe nema nikakovog smisla već se uvijek iskazuje u sklopu neke uvjetne naredbe. Ukoliko postoji više umetnutih petlji, prekida se izvršenje samo one unutarnje petlje u kojoj je definiran iskaz prekida. Logika continue iskaza je: Pseudo-asembler C jezik L1: početak petlje početak petlje { ....... ....... if (L) goto L1; if (L) continue; ....... ....... kraj petlje } kraj petlje Bekonačne petlje Ukoliko je u stukturi petlje uvjet za ponavljanje petlje uvijek istinit, kao u iskazu while (1) { N } dobije se tzv. beskonačna pelja Ona očito ne predstavlja suvislu algoritamsku strukturu jer je trajanje njenog izvršenja beskonačno, to je struktura koja ima ulaz ali nema izlaza. S programskog pak stajališta beskonačne petlje imaju smisla kod onih programa kod kojih se ciklički ponavlja jedan ili više procesa za vrijeme dok je računalo uključeno. Primjerice, jedan takovi program je i operativni sustav računala. Od beskonačne petlje se uvijek može dobiti petlja koja ima izlaz, ako se u zapis bekonačne petlje doda uvjetna naredba za prekid petlje (pomoću break ili goto naredbe). Primjerice, ako sekvencu petlje N čine dva iskaza B1 i B2, tada while (1) { B1 if (L) break; B2 } predstavlja petlju s uvjetnim izlazom unutar same petlje. Beskonačna petlja se može realizirati i pomoću for petlje: for(;;) { /* beskonačna petlja */ } Primjer: U programu cont.c korisnik unosi niz znakova, završno sa <enter>. Program koristi break i continue naredbe u beskonačnoj pelji, koja se izvršava sve dok se ne otkuca 5 malih slova. Nakon toga program ispisuje tih 5 malih slova. Ako se otkuca <enter> prije nego je uneseno 5 malih slova program se prekida i ispisuje poruku: "PREKINUT UNOS". 100
  • 101.
    /* Datoteka: cont.c * filtrira unos znakova s tipkovnice * tako da se propušta prvih 5 malih slova */ #include<stdio.h> #include<ctype.h> int main() { char slovo; int i; /* i registrira broj malih slova */ printf ("Upisite niz znakova i <enter>: "); i= 0; while(1) /* ili for(;;) */ { slovo= getchar(); if(slovo == 'n') { printf("nPREKINUT UNOS!n"); break; } if (islower(slovo)) { i++; printf("%c", slovo); } if(i<=5) continue; else { printf("UNOS OK!"); break; } } } 101
  • 102.
    8 Nizovi Naglasci: • jednodimenzionalni nizovi • inicijalizacija nizova • višedimenzionalni nizovi • prijenos nizova u funkcije U ovom je poglavlju opisano kako se formiraju i koriste nizovi. Rad s nizovima je "prirodni" način korištenja računala, jer memorija računala nije ništa drugo nego niz bajta. U programiranju, kao i u matematici, zanimaju nas nizovi kao kolekcija istovrsnih elemenata koji su poredani jedan za drugim. Elementi niza su varijable koje se označavaju indeksom: ai označava i-ti element niza u matematici a[i] označava i-ti element niza u C jeziku i=0,1,2.... 8.1 Jednodimenzionalni nizovi 8.1.1 Definiranje nizova Niz je imenovana i numerirana kolekcija istovrsnih objekata koji se nazivaju elementi niza. Elementi niza mogu biti prosti skalarni tipovi i korisnički definirani tipovi podataka. Označavaju se imenom niza i cjelobrojnom izrazom – indeksom – koji označava poziciju elementa u nizu. Indeks niza se zapisuje u uglatim zagradama iza imena niza. Primjerice, x[3] označava element niza x indeksa 3. Sintaksa zapisa elementa jednodimenzionalnog niza je element_niza: ime_niza [ indeks ] indeks: izraz_cjelobrojnog_tipa Prvi element niza ima indeks 0, a n-ti element ima indeks n-1. Prema tome, x[3] označava četvrti element niza. S elementima niza se manipulira kao s običnim skalarnim varijablama, uz uvjet da je prethodno deklariran tip elemenata niza. Sintaksa deklaracije jednodimenzionalnog niza je: deklaracija_niza: oznaka_tipa ime_niza [ konstantni_izraz ] ; Primjerice, deklaracijom int A[9]; definira se A kao niz od 9 elementa tipa int. 102
  • 103.
    Deklaracijom niza rezervirase potrebna memorija, na način da Memorijski raspored niza A elementi niza zauzimaju sukcesivne lokacije u memoriji. Vrijedi pravilo: adresa sadržaj adresa(A)=adresa(A[0]) 1000 data[0] 1004 data[1] adresa(A[n])=adresa(A[0]) + n*sizeof(A[0])) 1008 data[2] 1012 data[3] Elementima niza se pristupa pomoću cjelobrojnog indeksa, 1016 data[4] primjerice: 1020 data[6] 1024 data[5] A[0] = 7; 1028 data[7] int i=5; 1032 data[8] A[2]= A[i]; for(i=0; i<9; i++) Napomena: int zauzima printf("%d ", A[i]; 4 bajta U C jeziku se ne vrši provjera da li je vrijednost indeksnog izraza unutar deklariranog intervala. Primjerice, iskaz: A[12] = 5; je sintaktički ispravan i kompilator neće dojaviti grešku. Međutim, nakon izvršenja ove naredbe može doći do greške u izvršenju programa, ili čak do pada operativnog sustava. Radi se o tome da se ovom naredbom zapisuje vrijednost 5 na memorijsku lokaciju za koju nije rezervirano mjesto u deklaraciji niza. Primjer: U programu niz.c pokazano je kako se niz koristi za prihvat veće količine podataka - realnih brojeva. Zatim, pokazano je kako se određuje suma elemenata niza te vrijednost i indeks elementa koji ima najveću vrijednost. /* Datoteka: niz1.c */ #include <stdio.h> #define N 5 int main() { int i, imax; double suma, max; double A[N]; /* niz od N elemenata */ /* 1. izvjesti korisnika da otkuca 5 realnih brojeva */ printf("Otkucaj %d realnih brojeva:n", N); for (i=0; i<N; i++) scanf("%lg", &A[i]); /* 2. izračunaj sumu elemenata niza */ suma = 0; for (i=0; i<N; i++) 103
  • 104.
    suma += A[i]; printf("Suma unesenih brojeva je %fn", suma); /*3.odredi indeks(imax) i vrijednost(max) najvećeg elementa */ imax = 0; max = A[0]; for(i=1; i<N; i++) { if(A[i] > max ) { max = A[i]; imax=i; } } printf ("%d. element je najveci (vrijednost mu je %f)n", imax+1, max); return 0; } Izvršenje programa može izgledati ovako: Otkucaj 5 realnih brojeva: 5 6.78 7.1 8 0.17 Suma unesenih brojeva je 27.050000 4. element je najveci (vrijednost mu je 8.000000) 8.1.2 Inicijalizacija nizova Za globalno i statičko deklarirane nizove automatski se svi elementi postavljaju na vrijednost nula. Kod lokalo deklariranih nizova ne vrši se inicijalizacija početnih vrijednosti elemenata niza. To mora obaviti programer. Za inicijalizaciju elemenata niza na neku vrijednost često se koristi for petlja, primjerice naredba for (i = 0; i < 10; i++) A[i] = 1; sve elemente niza A postavlja na vrijednost 1. Niz se može inicijalizirati i s deklaracijom sljedećeg tipa: int A[9]= {1,2,23,4,32,5,7,9,6}; Lista konstanti, napisana unutar vitičastih zagrada, redom određuje početnu vrijednost elemenata niza. Ako se inicijaliziraju svi potrebni elementi niza, tada nije nužno u deklaraciji navesti dimenziju niza. Primjerice, int A[]= {1,2,23,4,32,5,7,9,6}; je potpuno ekvivalentno prethodnoj deklaraciji. Broj elemenata ovakvog niza uvijek se može odrediti pomoću iskaza: int brojelemenata = sizeof(A)/sizeof(int); Niz se može i parcijalno inicijalizirati. U deklaraciji int A[10]= {1,2,23}; 104
  • 105.
    prva tri elementaimaju vrijednost 1, 2 i 23, a ostale elemente prevodilac postavlja na vrijednost nula. Kada se inicijalizira znakovni niz, tada se u listi inicijalizacije mogu navesti znakovne konstante: char znakovi[2]= {'O','K'}; Primjer: U programu hex.c korisnik unosi cijeli broj bez predznaka, zatim se vrši ispis broja u heksadecimalnoj notaciji. /* Datoteka: hex.c * ispisuje broj, kojeg unosi korisnik, u heksadecimalnoj notaciji */ #include <stdio.h> int main() { int num, k; unsigned broj; char hexslova []= {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'} ; char reverse[8] = {0}; /* zapis do 8 heksa znamenki u reverznom redu */ printf("Otkucaj cijeli broj bez predznaka: "); scanf("%u", &broj); if(broj <0) broj = -broj; printf("Heksadecimalni zapis je: "); num=0; do { k = broj % 16; /* iznos heksa znamenke */ reverse[num++] = hexslova[k]; /* oznaka heksa znamenke */ broj /= 16; /* odstrani ovu znamenku */ } while(broj != 0); /* num sadrži broj heksa znamenki */ /* ispis od krajnjeg (n-1) do nultog znaka */ for(k=num-1; k>=0; k--) printf("%c", reverse[k]); printf("n"); return 0; } Pri izvršenju programa ispis može biti: Otkucaj cijeli broj bez predznaka: 256001 Heksadecimalni zapis je: 3E801 Algoritam pretvorbe u heksadecimalnu notaciju je jednostavan. Temelji se na činjenici da ostatak dijeljenja s 16 daje numeričku vrijednost heksadecimalne znamenke na mjestu najmanjeg značaja. Ta se vrijednost (k=0..15) korisi kao indeks znakovnog niza 105
  • 106.
    hexslova[k]. Vrijednost hexslova[k]je znak odgovarajuće heksadecimalne znamenke, kojeg se pridjeljuje nizu reverse[] ( u njemu će na kraju biti zapisana heksadecimalna notacija broja, ali obrnutm redoslijedom). Zatim se broj dijeli s 16 i dobavlja sljedeća heksadecimalna znamenka. Taj se proces ponavlja sve dok je rezultat dijeljenja različit od nule. Ispis broja u heksadecimalnoj notaciji se vrši tako da se ispiše sadržaj znakovnog niza reverzno, počevši od znamenke najvećeg značaja, završno s znamenkom najmanjeg značaja (koja se u tom nizu nalazi na indeksu 0). Primjer: Histogram U datoteci "ocjene.dat", u tekstualnom obliku, zapisane su ocjene u rasponu od 1 do 10. Sadržaj datoteke "ocjene.dat” neka čini sljedeći niz ocjena: 2 3 4 5 6 7 2 4 7 8 9 1 3 4 6 9 8 7 2 5 6 7 8 9 3 4 5 1 7 3 4 10 10 9 10 5 7 6 3 8 9 4 5 7 3 4 6 1 2 9 6 7 8 5 4 6 3 2 4 5 6 1 3 4 6 9 10 7 2 10 7 8 1 Zadatak je izraditi program imena hist.c pomoću kojeg se prikazuje histogram ocjena u 10 grupa (1, 2,.. 10), i srednja vrijednost svih ocjena. Podaci iz datoteke "ocjene.dat" se predaju programu "hist.exe" preusmjeravanjem standardnog ulaza (tipkovnice) na datoteku "ocjene dat". To se vrši iz komandne linije komandom: c:mydir> hist < ocjena.dat. Na ovaj način se pristupa sadržaju datoteke kao da je otkucan s tipkovnice. /* Datoteka: hist.c */ #include <stdio.h> int main(void) { int i, ocjena, suma; int izbroj[11] = {0}; /* niz brojača ocjena */ /* izbroj[0] sadrži ukupan broj ocjena * izbroj[i] sadrži podatak koliko ima ocjena veličine i. * Početno, elementi niza imaju vrijednost 0 */ while (scanf("%d", &ocjena) != EOF) { izbroj[ocjena]++; /* inkrementiraj brojač ocjena */ izbroj[0]++; /* inkrementiraj brojač broja ocjena */ } printf("Ukupan broj ocjena je %dn", izbroj[0]); /* ispiši histogram - od veće prema manjoj ocjeni*/ for (i = 10; i > 0; i--) { int n = izbroj[i]; /* n je ukupan broj ocjena iznosa i */ printf("%3d ", i); /* ispiši ocjenu, a zatim */ while (n-- > 0) /* ispiši n zvjezdica */ printf("*"); printf("n"); 106
  • 107.
    } /* izračunaj sumu svih ocjena */ suma =0; for (i = 1; i < 11; i++) suma += izbroj[i]*i; /*ispiši srednju ocjenu */ printf ("Srednja ocjena je %4.2fn", (float)suma / izbroj[0]); return 0; } Program se poziva komandom: c:>hist < ocjene.dat Dobije se ispis: Ukupan broj ocjena je 73 10 ***** 9 ******* 8 ****** 7 ********** 6 ********* 5 ******* 4 ********** 3 ******** 2 ****** 1 ***** Srednja ocjena je 5.79 Analiza programa hist.c: Prvo je deklariran niz izbroj od 11 elemenata tipa int, a inicijaliziran je na vrijednost 0. Program će u tome nizu bilježiti koliki je broj ocjena neke vrijednosti (izbroj[1] će sadržavati broj ocjena veličine 1, izbroj[2] će sadržavati broj ocjena veličine 2, itd., a izbroj[0] sadrži ukupan broj ocjena). Bilježenje pojavnosti neke ocjene se dobije inkrementiranjem brojača izbroj[ocjena]. Ocjene se dobavljaju preusmjerenjem datoteke na standardni ulaz. Za unos pojedine ocjene koristi se scanf() funkcija sve dok se na ulazu ne pojavi znak EOF (end-of-file), koji znači kraj datoteke. Nakon toga se ispisuje histogram na način da se uz oznaku ocjene ispiše onoliko zvjezdica koliko je puta ta ocjena zabilježena u nizu brojača ocjena. Na kraju se računa srednja vrijednost ocjena na način da se suma svih ocjena podijeli s ukupnim brojem ocjena. Uočite da je u naredbi printf ("Srednja ocjena je %4.2fn", (float)suma / izbroj[0]); izvršena eksplicitna pretvorba tipa operatorom (float), jer srednja vrijednost može biti realni broj. 8.2 Prijenos nizova u funkciju Nizovi mogu biti argumeti funkcije. Pri deklaraciji ili definiranju funkcije formalni argument, koji je tipa niza, označava se na način da se deklarira niz bez oznake veličine niza, tj. u obliku 107
  • 108.
    tip ime_niza[] Pri pozivufunkcije, kao stvarni argument, navodi se samo ime niza bez uglatih zagrada. Primjer: u programu prod.c korisnik unosi niz od N realnih brojeva. Nakon toga, program računa produkt svih elemenata niza, pomoću funkcije produkt(), i ispisuje rezultat. /* Datoteka: prod.c */ /* Računa produkt elemenata niza od 5 elemenata */ #include <stdio.h> #define N 5 /* radi s nizom od N elemenata */ double produkt(double A[], int brojelemenata) { int i; double prod = 1; for (i=0; i<brojelemenata; i++) prod *= A[i]; return prod; } int main() { int i; double A [N]; /* 1. izvjesti korisnika da otkuca 5 realnih brojeva */ printf("Otkucaj %d realnih brojeva:n", N); for (i=0; i<N; i++) scanf("%lg", &A[i]); /* 2. izračunaj sumu elemenata niza */ printf("Suma unesenih brojeva je %gn", produkt(A, N)); return 0; } Uočite de se vrijednost elemenata niza može mijenjati unutar funkcije. Očito je da se niz ne prenosi po vrijednosti (by value), jer tada to ne bi bilo moguće. Pravilo je: U C jeziku se nizovi – i to samo nizovi – u funkciju prenose kao memorijske reference (by reference), odnosno prenosi se adresa početnog elementa niza. Brigu o tome vodi prevodilac. Memorijska referenca (adresa) varijable se pamti "u njenom imenu" stoga se pri pozivu funkcije navodi samo ime, bez uglatih zagrada. Nizovi, koji su argumenti funkcije, ne smiju se unutar funkcije tretirati kao lokalne varijable. Promjenom vrijednosti elementa niza unutar funkcije ujedno se mijenja vrijednost elementa niza koji je u pozivnom programu označen kao stvarni argument funkcije. 108
  • 109.
    Primjer: Prijašnji primjerprograma za histogram ocjena bit će modificiran, na način da se definira tri funkcije: pojavnost ocjena u nizu izbroj bilježit će se funkcijom registriraj(), crtanje histograma vršit će funkcija histogram(), a proračun srednje vrijednosti vršit će funkcija srednja_ocjena(). /* Datoteka: histf.c */ #include <stdio.h> void registriraj( int ocjena, int izbroj[]) { izbroj[ocjena]++; izbroj[0]++; } void histogram(int izbroj[]) { int n,i; for (i = 10; i > 0; i--) { printf("%3d ", i); n=izbroj[i]; while (n-- > 0) printf("*"); printf("n"); } } float srednja_ocjena(int izbroj[]) { /* izračunaj sumu svih ocjena */ int i, suma =0; for (i = 1; i <= 10; i++) suma += izbroj[i]*i; return (float)suma / izbroj[0]; } int main(void) { int i, ocjena, suma; int izbroj[11]={0}; while (scanf("%d", &ocjena) != EOF) registriraj(ocjena, izbroj); histogram(izbroj); printf ("Srednja ocjena je %4.2fn",srednjaocjena(izbroj)); return 0; } Primjer: Često je potrebno odrediti da li u nekom nizu od N elemenata postoji element vrijednosti x. U tu svrhu zgodno je definirati funkciju int search(int A[], int N, int x); koja vraća indeks elementa niza A koji ima vrijednost x. Ako ni jedan element nema vrijednost x, funkcija vraća negativnu vrijednost -1. Implementacija funkcije je: int search (int A[], int N, int x) { 109
  • 110.
    int indx; for(indx = 0; indx < N; indx++) { if( A[indx] == x) /* element pronađen – prekini */ break; } if(indx == N) /* tada ni jedan element nema vrijednost x*/ return -1; else return indx; } Uočite, ako u nizu postoji više elemenata koji imaju vrijednost x, vraća se indeks prvog pronađenog elementa. 8.3 Višedimenzionalni nizovi Višedimenzionalnim nizovima se pristupa preko dva ili više indeksa. Primjerice, deklaracijom: int x[3][4]; definira se dvodimenzionalni niz koji ima 3 x 4 = 12 elemenata. Deklaraciju se može čitati i ovako: definiran je niz kojem su elementi 3 niza s 4 elementa tipa int. Dvodimenzionalni nizovi se često koriste za rad s Memorijski raspored niza matricama. U tom slučaju nije potrebno razmišljati o adresa sadržaj tome kako je niz složen u memoriji, jer se elementima 1000 x[ 0][ 0] pristupa preko dva indeksa: prvi je oznaka retka, a 1004 x[ 0][ 1] drugi je oznaka stupca matrice. 1008 x[ 0][ 2] 1012 x[ 0][ 3] Matrični prikaz niza je: 1016 x[ 1][ 0] x[0][0] x[0][1] x[0][2] x[0][3] 1020 x[ 1][ 1] x[1][0] x[1][1] x[1][2] x[1][3] 1024 x[ 1][ 2] x[2][0] x[2][1] x[2][2] x[2][3] 1028 x[ 1][ 3] 1032 x[ 2][ 0] 1036 x[ 2][ 1] 1040 x[ 2][ 2] Memorijski raspored elemenata dvodimenzionalnog 1044 x[ 2][ 3] niza, koji opisuju neku matricu, je takovi da su elementi složeni po redovima matrice; najprije prvi redak, zatim drugi, itd.. Višedimenzionalni niz se može inicijalizirani već u samoj deklaraciji, primjerice int x[3][4] = { { 1, 21, 14, 8}, {12, 7, 41, 2}, { 1, 2, 4, 3} }; Navođenje unutarnjih vitičastih zagrada je opciono, pa se može pisati i sljedeća deklaracija: int x[3][4] = {1, 21, 14, 8, 12, 7, 41, 2, 1, 2, 4, 3}; Ovaj drugi način inicijalizacije se ne preporučuje, jer je teže uočiti raspored elemenata po redovima i stupcima. 110
  • 111.
    Elementima se pristupapreko indeksa niza. U sljedećem primjeru računa se suma svih elemenata matrice x; int i, j, brojredaka=3, brojstupaca=4; int sum=0; for (i = 0; i < brojredaka; i++) for (j = 0; i < brojstupaca; j++) sum += x[i][j]; printf("suma elemenata matrice = %d", sum); Prijenos višedimenzionalnih nizova u funkciju Kod deklariranja parametara funkcije, koji su višedimenzionalni nizovi pravilo je da se ne navodi prva dimenzija (kao i kod jednodimenzionalnih nizova), ali ostale dimenzije treba deklarirati, kako bi program prevodilac "znao" kojim su redom elementi složeni u memoriji. Primjer: Definirana je funkcija sum_mat_el() kojom se računa suma elemenata dvodimenzionalne matrice, koja ima 4 stupca: int sum_mat_el(int x[][4], int brojredaka) { int i, j, sum=0; for (i = 0; i < brojredaka; i++) for (j = 0; i < 4; j++) sum += x[i][j]; return sum; } Uočite da je u definiciji funkcije naveden i argument koji opisuju broj redaka matrice. Broj stupaca je fiksiran već u deklaraciji niza na vrijednost 4. Očito da ova funkcija ima ograničenu upotrebu jer se može primijeniti samo na matrice koje imaju 4 stupca. Kasnije, pri proučavanju upotrebe pokazivačkih varijabli, bit će pokazano kako se ova funkcija može modificirati tako da vrijedi za matrice proizvoljnih dimenzija. 111
  • 112.
    9 Blokovi, modulii dekompozicija programa Naglasci: • blok struktura programa • lokalne i globalne varijable • automatske i statičke varijable • programski moduli i biblioteke • "skrivanja podataka" • dekompozicija programa "od vrha prema dolje" • igra "točkice i kružići" 9.1 Blokovska struktura programa Ukoliko se u programiranju ne koristi goto naredba, programi tada imaju prepoznatljivu blokovsku strukturu. Praksa je pokazala da se time dobiju “čitljivi” programi, koje je lako održavati i dograđivati. Blokovska struktura C programa ima četiri razine: • razina datoteke (temeljna kompilacijska jedinica) • razina definicije (tijela) funkcije • razina bloka kontrolnih struktura (sekvenca, iteracija, selekcija) • razina bloka koji je omeđen vitičastim zagradama Blok niže razine može biti umetnut unutar bloka više ili iste razine proizvoljan broj puta, jedino se ne smije vršiti definicija funkcije unutar tijela neke druge funkcije. Cilj je programirati tako da svaki blok predstavlja cjelinu koja je što manje ovisna o ostatku programa. Da bi se to postiglo potrebno je dobro razumjeti pravila dosega identifikatora i postojanosti varijabli. 9.1.1 Doseg Doseg nekog identifikatora (eng. scope) je dio programa u kojem se taj identifikator može koristiti. Deklaracijom argumenata funkcije i lokalnih varijabli stvaraju se novi identifikatori. Za njih vrijede sljedeća pravila dosega: • Doseg argumenata funkcije je tijelo funkcije. • Doseg lokalnih varijabli se proteže od mjesta deklariranja do kraja složenog iskaza koji je omeđen vitičastim zagradama. • Identifikatori s različitim područjima dosega, iako mogu imati isto ime, međusobno su neovisni. • Nisu dozvoljene deklaracije s istim imenom u istom dosegu. Primjerice, 112
  • 113.
    float epowx( floatx, float epsilon) { int x; /* greška, ime x je već pridjeljeno parametru funkcije*/ ... } U sljedećem primjeru varijable x i ex u funkciji main(), te x i ex u funkciji my_exp() su neovisne varijable iako imaju isto ime. int main( void) { double eps, x, ex; ... doseg ex, x return 0; } float my_exp( double x, double epsilon) { doseg x int i; double ex = 1.0, preth_ex = 0.0, … ; ... doseg ex return ex; } Lokalne deklaracije imaju prednost nad vanjskim deklaracijama. Kažemo da lokalna deklaracija prekriva vanjsku deklaraciju. Primjer: f( int x, int a) { int y, b; y = x + a* b; if (...) { int a, b; /* a prekriva parameter a /* ... /* b prekriva lokalnu var. b iz vanjskog dosega */ y = x + a* b; } } Uobičajeno se smatra da nije dobar stil programiranja kada se koriste ista imena u preklopljenim dosezima, iako je to sintaktički dozvoljeno. 9.1.2 Automatske i statičke varijable Lokalne varijable imaju ograničeno vrijeme postojanja, pa se nazivaju i automatske varijable. One nastaju (u memoriji) pozivom funkcije u kojoj su deklarirane, a nestaju (iz memorije) nakon povrata u pozivnu funkciju. Kada se argumenti prenose u funkcije može se uzeti da se tada vrijednost stvarnih argumenata kopira u formalne argumente, koji pak imaju lokalni doseg. Argumenti funkcije se inicijaliziraju kao lokalne varijable, pa za njihovu upotrebu vrijede pravila kao za lokalne varijable. To je ilustrirano u programu doseg.c. /* Datoteka doseg.c */ #include <stdio.h> 113
  • 114.
    void f( inta, int x) { printf(" a = %d, x = %dn",a, x); a = 3; { int x = 4; printf(" a = %d, x = %dn", a, x); } printf(" a = %d, x = %dn", a, x); x = 5; /*nema nikakovi efekt*/ } int main( void) { int a = 1, b = 2; f( a, b); printf(" a = %d, b = %dn", a, b); return 0; } c:> cl args.c c:>args a = 1, x = 2 a = 3, x = 4 a = 3, x = 2 a = 1, b = 2 Ako se lokalna varijabla deklarira s prefiksom static, tada se za tu varijablu trajno rezervira mjesto u memoriji (postoji i nakon izvršenja funkcije), iako je njen doseg ograničen unutar tijela funkcije. U sljedećem primjeru opisana je funkcija incrCounter(), kojom se realizira brojač po modulu mod. U funkciji je definirana statička varijabla count, čija se vrijednost inkrementira pri svakom pozivu funkcije. Ako vrijednost postane jednaka argumentu mod, count se postavlja na nulu. /* Datoteka: countmod3.c */ #include <stdio.h> int incrCounter(int mod) { static int count=0; count++; if(count == mod) count = 0; return count; } int main(void) { int i,modul=3; for(i=0; i<=10; i++) printf("%d, ", incrCounter(modul)); printf("...n"); return 0; } 114
  • 115.
    Dobije se ispis: 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2,... Važno je uočiti: kada se statička lokalna varijabla u deklaraciji i inicijalizira (pr. static int count=0;), ta inicijalna vrijednost vrijedi samo pri prvom pozivu funkcije. Inicijalizacija nije ista kod lokalnih statičkih i lokalnih automatskih varijabli. Vrijednost lokalnih automatskih varijabli se inicijalizira na vrijednost opisanu inicijalizacijom pri svakom pozivu funkcije, a vrijednost lokalnih statičkih varijabli se inicijalizira na vrijednost opisanu inicijalizacijom samo pri prvom pozivu funkcije. 9.1.3 Globalne varijable Globalne varijable su varijable koje se deklariraju izvan tijela funkcije. To su "permanentne" varijable koje trajno zauzimaju memoriju za vrijeme trajanja programa. One se uvijek inicijaliziraju na vrijednost 0. Doseg globalnih varijabli je od točke definiranja do kraja datoteke u kojoj su definirane. Kasnije će biti pokazano kako se njihov doseg može proširiti i na druge datoteke int main( void) { … /* ovdje ne postoji varijabla max */ } int max = 0; /* mjesto definicije varijable max */ void fun( … ) { … max = /* ovdje postoji globalna var. max */ } Argumenti funkcije i lokalne varijable prekrivaju globalne varijable, ako imaju isto ime. void fun( … ) { int max; /* lokalna var. max prekriva globalnu var max */ max = ... } Primjer: Prethodni je program countmod3.c izmijenjen je na način da obje funkcije, incrCounter() i main(), koriste globalnu varijablu count. /* Datoteka: count.c */ #include <stdio.h> int count; /* globalna varijabla, /* inicijalizirana na vrijednost 0 */ int incrCounter(int mod) { count++; if(count == mod) count = 0; 115
  • 116.
    return count; } int main(void) { int i,modul=3; for(i=0; i<=10; i++) { incrCounter(modul); printf("%d, ", count); } printf("...n"); return 0; } Eksterne globalne varijable Ako se nekoj datoteci deklarira globalna varijabla, ona se može dosegnuti i iz drugih kompilacijskih jedinica, ako se u tim datotekama deklarira kao eksterna (ili vanjska) varijabla. Deklaracija eksterne varijable označava se prefiksom extern, primjerice: extern int max; void dump( … ) { max = ... } Primjer: Prethodni program brojača po modulu mod, napisan je u dvjema datotekama. U prvoj datoteci, imena "counter.c", definirana je globalna varijabla count i funkcija incrCounter(). U drugoj datoteci, imena "countmain.c", definirana je funkcija main() koji koristi vrijednost od count i funkciju incrCounter(). /* Datoteka1: counter.c */ /* stanje brojača prije poziva funkcije counter */ int count=0; incrCounter(int mod) { count++; if(count >= mod) count = 0; } /* Datoteka2: countmain.c */ extern int count; void incrCounter(int mod); int main(void) { int i,mod=3; for(i=0; i<=10; i++) { incrCounter(mod); 116
  • 117.
    printf("%d, ", count); } printf("...n"); return 0; } Program se kompilira komandom: c:>cl countmain.c counter.c U komandnoj liniji se navode imena obje izvorne datoteke. Konačni izvršni program će imati ime datoteke koja je prva zadana: countmod.exe. Rezultat izvršenja programa biti će isti kao i u prethodnom primjeru. Statičke globalne varijable Globalne varijable se također mogu deklarirati s prefiksom static. Takove varijable se nazivaju statičke globalne varijable. One su vidljive samo u kompilacijskoj jedinici (datoteci) unutar koje su i definirane. Ne može ih se koristiti u drugim datotekama. Zašto se koriste statičke globalne varijable? To će biti objašnjeno kada se objasni ideja modularnog programiranja i princip "skrivanja podataka" (eng. data hidding). 9.1.4 Moduli Modul sadrži skup međuovisnih globalnih varijabli i funkcija zapisanih u jednoj ili više datoteka. Moduli se obično formiraju u dvije grupe datoteka: 1. datoteke specifikacije modula (ime.h ) sadrže deklaracije funkcija i (eksternih) globalnih varijabli koje su implementirane unutar modula. 2. datoteke implementacije (ime.c ) sadrže definicije varijabli i funkcija Implementacijske se datoteke mogu kompilirati kao samostalne kompilacijske jedinice, a dobiveni objektni kod se može pohraniti u biblioteku potprograma. Neposrednu korist od ovakvog načina formiranja programa najbolje će pokazati sljedeći primjer. Primjer: bit će realiziran modul koji sadrži funkcije brojača po modulu mod. Problem će biti obrađen nešto općenitije nego u prethodnim primjerima. Najprije se vrši specifikacija modula. 1. Specifikacija modula je opisana u datoteci "counter.h". /* Datoteka: counter.h * specifikacija funkcija brojača po modulu mod */ void reset_count(int mod); /* Funkcija: inicira brojač na početnu vrijednost nula * i modul brojača na vrijednost mod. Ako je mod<=1, * modul brojaca se postavlja na vrijednost INT_MAX */ int getCount(void); /* Funkcija: vraća trenutnu vrijednost brojača */ int getModulo(void); /* Funkcija: vraća trenutnu vrijednost modula brojača */ int incrCounter(void); 117
  • 118.
    /* Funkcija: incrementiravrijednost brojača za 1 * Ako vrijednost brojača postane jednaka ili veća, * od zadamog modula vrijednost brojača postaje nula. * Vraća: trenutnu vrijednost brojača */ Prema ovoj specifikaciji predviđeno je da se brojačem upravlja pomoću dvije funkcije: incrCounter(), koja inkrementira brojač, i reset_count(), koja postavlja početno stanje brojača. Stanje brojača se očitava pomoću funkcija getCount() i getModulo(). Nije predviđeno da korisnik pristupa globalnim varijablama. 2. Implementacija modula je opisana u datoteci "counter.c". U toj datoteci su definirane dvije statičke globalne varijable _count i _mod. Prefiks static znači da su one vidljive samo u ovoj datoteci. Početno je vrijednost _mod postavljena na maksimalnu moguću cjelobrojnu vrijednost. Zatim slijede definicije funkcija koje su određene specifikacijom. /* Datoteka: counter.c * Implementacija funkcija brojača po modulu: mod */ #include <limits.h> /* zbog definicija INT_MAX*/ /* globalne varijable */ static int _count = 0; /* početno stanje brojača */ static int _mod = INT_MAX; /*2147483647*/ void resetCounter(int mod) { _count= 0; if(mod <= 1) _mod = INT_MAX; else _mod = mod; } int getCount(void) { return _count;} int getModulo(void){ return _mod; } int incrCounter(void) { _count++; if(_count >= _mod) _count = 0; return _count; } 3. Testiranje modula se vrši programom "testcount.c": /* Datoteka: testcount.c */ #include <stdio.h> #include "counter.h" int main(void) { int i; resetCounter(5); 118
  • 119.
    printf("Brojac po modulu%d n", getModulo()); for(i=0; i<=10; i++) { incrCounter(); printf("%d, ", getCount()); } printf("...n"); return 0; } c:>cl testcounter.c counter.c c:> testcounter Brojac po modulu 5 1, 2, 3, 4, 0, 1, 2, 3, 4, 0, 1, ... Nakon što je modul testiran, može ga se primijetiti i u drugim programima. Veza između glavnog programa i modula je opisana u deklaracijama specifikacijske datoteke "counter.h", pa za primjenu modula nije potrebno znati kako je implementiran, već samo kako se koriste njegove funkcije. Može se reći da modul opisuje apstraktni objekt brojača, koji sam nadgleda vlastito stanje. Sve varijable koje su potrebne za definiranje stanja ovog objekta su deklarirane kao statičke, pa nisu vidljive izvan modula. Ovaj se princip, “skrivanja varijabli” od korisnika modula, naziva "data hidding" ili "data encapsulation", a neobično je popularan u softverskom inženjerstvu jer se njime postiže neovisnost podataka iz različitih modula. Na ovaj je način lakše provoditi projekte u kojima sudjeluje više programera. 9.1.5 Formiranje programskih biblioteka Prethodni program se može kompilirati na sljedeći način: c:>cl testcounter.c counter.c Kada se radi s modulima, često je zgodniji način razvoja programa da se modul, koji je testiran, prevede u objektnu datoteku. To se vrši komandom: c:>cl –c counter.c (Parametar komandne linije –c je poruka kompilatoru da se prijevod izvrši u strojni kod, bez stvaranja izvršne datoteke). Nakon ove komande dobije se datoteka "counter.obj". Sada se izvršni program može dobiti i pomoću komande: c:>cl testcounter.c counter.obj Dobra strana ovakovog pristupa je da se ubrzava proces stvaranja programa, jer ne treba uvijek iznova kompilirati datoteku "counter.c". Više objektnih datoteka se može združiti u programske biblioteke. Uobičajeno, programske datoteke imaju ekstenziju .lib (ili .a na Unixu). Pokazat ćemo kako se formira biblioteka potprograma pomoću Microsoft program "lib.exe". Pretpostavka je da želimo objektnu datoteku "counter.obj" uvrstiti u biblioteku koja se zove "mylib.lib". To se ostvaruje komandom: c:>lib /OUT:mylib.lib counter.obj (/OUT: je parametar komandne linije iza kojeg se navodi ime biblioteke). 119
  • 120.
    Izvršni se programmože dobiti komandom: c:>cl testcount.c mylib.lib Očito je da kompilator kôd za modul brojača dobiva iz biblioteke "mylib.lib". Na sličan način formirana je standardna biblioteka C-jezika. 9.2 Funkcionalna dekompozicija programa "od vrha prema dolje" Kada se razvija neki program, polazište je zadatak kojeg treba obaviti. Analizom problema određuju se radnje i algoritmi kojima se može taj zadatak obaviti. U proceduralnim je jezicima najbolji način programske realizacije neke radnje da se ona specificira i implementira pomoću neke funkcije. Radnje, odnosno zadaci, koje treba obaviti neka funkcija, također se mogu realizirati pomoću niza funkcija. U tom slučaju kažemo da se razvoj programa vrši funkcionalnom dekompozicijom "od vrha prema dolje". Praksa je pokazala da je funkcionalna dekompozicija prihvatljiva kao metoda programiranja u većini slučajeva. Kao studijski primjer razvoja programa funkcionalnom dekompozicijom izradit ćemo program pomoću kojeg se igra popularna igra "točkice i kružići" (ili tic-tac-toe). Tic-Tac-Toe Tic-Tac-Toe je igra u kojoj se nadmeću dva igrača. Igrači, jedan za drugim označavaju polja u 9 kvadratića, prvi s križićem, a drugi s točkicom. Pobjednik je onaj koji redno, stupčano ili dijagonalno prvi ispuni 3 polja. Prvi igrač unosi križić, a drugi točkicu. Primjer igre u kojoj je pobijedio drugi Označavanje pozicije kvadratića na igrač, tj. onaj koji unosi točkicu. igračkoj ploči | o | X | o | | 1 | 2 | 3 | |---|---|---| |---|---|---| | X | o | X | | 4 | 5 | 6 | |---|---|---| |---|---|---| | o | | X | | 7 | 8 | 9 | Kako napisati program u kojem će jedan od igrača biti računalo, a drugi igrač je čovjek. Problem se može formulirati na sljedeći način: 1. Nacrtaj igraču ploču, i upute za igru. 2. Postavi početno stanje (ploča je prazna). 3. Ispitaj da li prvi potez vuče računalo ili čovjek. 4. Prati igru sve dok se ne ostvari pobjednička kombinacija, ili dok se ne ispune svi kvadratići. 5. Ako korisnik želi ponoviti igru vrati se na korak 2, inače završi program. Problem je postavljen sasvim općenito. Sada treba izvršiti razradu problema, tako da se detaljnije opišu pojedini koraci u opisanom apstraktnom algoritmu. Pored postupaka koje treba obaviti, neophodno je odrediti strukturu podataka, kojom će se pratiti stanje programa. U ovom slučaju, za stanje igrače ploče koristit će se znakovna matrica od 3x3 elementa, deklarirana s 120
  • 121.
    char kvadrat[3][3]; Pojedini elementmatrice može poprimiti samo tri vrijednosti: ' ', 'X' i 'o' (prazno, križić i točkica). Za označavanje igrača koristit će se dvije globalne varijable char racunalo, covjek; prva će sadržavati znak koji unosi računalo, a druga znak koji unosi čovjek. Ako jedna od ovih varijabli, primjerice covjek, ima vrijednost 'X', to znači da prvi potez vuče čovjek, inače, prvi potez vuče računalo. Na temelju početnog algoritma može se zaključiti da se problem može realizirati pomoću 5 neovisnih funkcija. Specifikacija tih funkcija je sljedeća: void Upute(void); /* Ispisuje igraču ploču, i upute za igru */ void OcistiPlocu(void); /* Postavlja početno stanje, * svi elementi matrice kvadrat imaju vrijednost ' ' */ void PostaviPrvogIgraca(void); /* Na temelju interakcije s korisnikom programa odlučuje da li * prvi potez vuče računalo ili čovjek. * Ako prvi potez vuče: racunalo = 'o'; covjek = 'X'; * inače: racunalo = 'X'; covjek = 'o'; */ void IgrajIgru(void); /* Ovom se funkcijom kontrolira tijek igre, interakcija s * korisnikom, odlučuje o potezima koje inteligentno izvršava * računalo i izvještava se o ispunjenosti igrače ploče. Funkcija * završava kada se ispune uvjeti za pobjedu prvog ili drugog * igrača ili ako su označeni svi kvadratići. */ int PonoviIgru(void); /* Ovom funkcijom se od korisnika traži da potvrdi da li želi * ponoviti igru. Ako korisnik želi ponoviti igru tada funkcija * vraća 1, inače vraća vrijednost 0. */ Uz pretpostavku da će se kasnije uspješno implementirati ove funkcije, glavni program se može napisati u obliku: /* Program TicTacToe.c */ #include <stdio.h> void Upute(void); void OcistiPlocu(void); void PostaviPrvogIgraca(void); void IgrajIgru(void); int PonoviIgru(void); char kvadrat[3][3]; char racunalo, covjek; 121
  • 122.
    int main(void) { Upute(); /* korak 1*/ do { OcistiPlocu(); /* korak 2*/ PostaviPrvogIgraca(); /* korak 3.*/ IgrajIgru(); /* korak 4.*/ }while(PonoviIgru()); /* korak 5.*/ return 0; } Što je do sada napravljeno? Problem je rastavljen (dekomponiran) na pet manjih, međusobno neovisnih problema. Neki od ovih problema se mogu odmah riješiti, primjerice funkcije Upute() i OcistiPlocu() se mogu implementirati na sljedeći način: void Upute() { printf("nIgra - tic tac toe - krizici i tockicenn"); printf("t | 1 | 2 | 3 | n"); printf("t |---|---|---| n"); printf("t | 4 | 5 | 6 | n"); printf("t |---|---|---| n"); printf("t | 7 | 8 | 9 | nn"); printf("Cilj igre je ispuniti tri kvadratica u redu: n"); printf("horizontalno, vertikalno ili dijagonalno. n" printf("Igra se protiv racunala.n"); printf("Prvi igrac je oznacen krizicem X, a drugi tockicom o.nn"); } void OcistiPlocu(void) { int redak, stupac; for (redak = 0; redak < 3; redak++) for (stupac = 0; stupac < 3; stupac++) kvadrat[redak][stupac] = ' '; } Za ostale funkcije potrebna je daljnja dorada problema. Posebno opsežan problem je definiranje funkcije IgrajIgru() u kojoj se obavlja više operacija. Prije definiranja te funkcije izvršit će se definiranje funkcija PostaviPrvogIgraca() i PonoviIgru() jer je njihova implementacija jednostavna i može se odrediti neposredno iz zadane specifikacije. Dorada funkcije PostaviPrvogIgraca(): 1. Izvijesti korisnika da on bira tko će povući prvi potez. Ako želi biti prvi na potezu neka pritisne tipku 'D', inače neka pritisne tipku 'N'. 2. Motri korisnikov odziv, sve dok se ne pritisne jedna od ove dvije tipke. 3. Ako je pritisnuta tipka 'D', tada: racunalo = 'o'; covjek = 'X'; inače: racunalo = 'X'; covjek = 'o';. void PostaviPrvogIgraca(void) { int key; printf("Da li zelite zapoceti prvi? (d/n)n"); do { key = toupper(getchar()); 122
  • 123.
    } while ((key!= 'D') && (key != 'N')); if (key == 'D') { racunalo = 'o'; covjek = 'X'; } else { racunalo = 'X'; covjek = 'o'; } } Dorada funkcije PonoviIgru(): 1. Upitaj korisnika da li želi ponoviti igru. 2. Motri korisnikov odziv, sve dok se ne pritisne tipku 'D' ili 'N'. 3. Ako je pritisnuta tipka 'D' funkcija vraća vrijednost 1, inače vraća vrijednost 0. int PonoviIgru(void) { int key; printf("Da li zelite ponovo igrati? (D/N) "); do { key = toupper(getchar()); } while ((key != 'D') && (key != 'N')); return ( key == 'D'); } Dorada funkcije IgrajIgru(): Prvo treba uočiti da može biti maksimalno 9 poteza, jer ploča ima 9 kvadratića. Ako poteze numeriramo od 1 do 9 onda vrijedi da prvi igrač vuče poteze koji su numerirani 1,3,5... Dakle, kada je neparna vrijednost poteza, prvi igrač unosi 'X' u izabrani kvadratić, a kada je parna vrijednost poteza drugi igrač unosi 'o'. Operacije koje izvodi ova funkcija mogu se iskazati algoritmom: 1. Za maksimalno 9 poteza 1.1 Dobavi izbor aktivnog igrača (računalo ili korisnik) 1.2 Nacrtaj ploču s točkicama i križićima 1.3 Ako je pobjednik računalo ispiši: "Pobijedio sam te!" i prekini igru, inače, ako je pobjednik korisnik, ispiši: "Pobijedio si!" i prekini igru. 2. Ako nije određen pobjednik ni nakon 9 poteza, ispiši poruku: "Ovaj put nema pobjednika" Ovaj se algoritam može programski realizirati pomoću varijable potez, u kojoj se bilježi redni broj poteza, i sljedeće tri funkcije: void DobaviPotez(int potez); /* na temelju rednog broja poteza određuje se koji je igrač * na potezu, dobavlja njegov izbor i * označava novo stanje matrice kvadrat */ void NacrtajPlocu(void); /* crta ploču na temelju stanja matrice kvadrat */ int PostajeDobitnik(char simbol); /* određuje da li simbol (X ili o) ispunja matricu na način da * je postignuta dobitnička kombinacija */ Uz pretpostavku uspješne implementacije ovih funkcija, može se funkcija IgrajIgru() napisati u obliku: 123
  • 124.
    void IgrajIgru(void) { int potez = 1; while (potez <= 9) { DobaviPotez(potez); NacrtajPlocu(); if (PostajeDobitnik(racunalo)){ printf("nPobijedio sam te!!!nn"); break; } else if (PostajeDobitnik(covjek)) { printf("nCestitam, pobijedio si!nn"); break; } potez++; } if (potez > 9) printf("nOvaj put nema pobjednika.nn"); } Realizacija funkcija DobaviPotez(), NacrtajPlocu() i PostajeDobitnik() /* Funkcija: DobaviPotez(int potez) * Izbor se dobije tako da se utvrdi * da li je varijabla potez parna ili neparna. * Ako je parna, igra 'X', inače igra 'o' */ void DobaviPotez(int potez) { if (potez % 2 == 1) if (racunalo == 'X') PotezRacunala(); else PotezCovjeka(); else if (racunalo == 'o') PotezRacunala(); else PotezCovjeka(); } /* Funkcija: NacrtajPlocu() * prikazuje igraču ploču na standardnom izlazu */ void NacrtajPlocu(void) { int redak, stupac; printf("n"); for (redak = 0; redak < 3; redak++) { printf("t| %c | %c | %c |n", kvadrat[redak][0], kvadrat[redak][1], kvadrat[redak][2]); if (redak != 2) printf("t|---|---|---|n"); } printf("n"); return; } /* Funkcija: int PostajeDobitnik(char simbol) * Provjera da li je simbol (X i o) pobjednik, ispitivanjem 124
  • 125.
    * ispunjenosti redaka,stupaca ili dijagonala ploče */ int PostajeDobitnik(char simbol) { int redak, stupac; for (redak = 0; redak < 3; redak++) /* ispitajmo 3 retka */ { if ( (kvadrat[redak][0] == simbol) && (kvadrat[redak][1] == simbol) && (kvadrat[redak][2] == simbol)) return 1; } for (stupac = 0; stupac < 3; stupac++) /* ispitajmo 3 stupca */ { if ( (kvadrat[0][stupac] == simbol) && (kvadrat[1][stupac] == simbol) && (kvadrat[2][stupac] == simbol)) return 1; } /* i konačno dvije dijagonalne kombinacije */ if ( (kvadrat[0][0] == simbol) && (kvadrat[1][1] == simbol) && (kvadrat[2][2] == simbol)) return 1; if ( (kvadrat[0][2] == simbol) && (kvadrat[1][1] == simbol) && (kvadrat[2][0] == simbol)) return 1; return 0; } /* Funkcija: PotezCovjeka(void) */ /* vrši dobavu poteza čovjeka */ void PotezCovjeka(void) { int pozicija; do { printf("Otipkaj poziciju znaka %c (1..9): ", covjek); scanf("%d", &pozicija); } while (!IspravnaPozicija(pozicija)); kvadrat[(pozicija - 1) / 3][ (pozicija - 1) % 3] = covjek; } /* Funkcija: IspravnaPozicija(int pozicija) * vraća 1 ako pozicija prazna, inače vraća 0 */ int IspravnaPozicija(int pozicija) { int redak, stupac; redak = (pozicija - 1) / 3; stupac = (pozicija - 1) % 3; if ((pozicija >= 1) && (pozicija <= 9)) 125
  • 126.
    if (kvadrat[redak][stupac] ==' ') return 1; return 0; } /* Funkcija: PotezRacunala() * inteligentno određuje potez racunala * (algoritam je napisan u obliku komentara) */ void PotezRacunala(void) { int pozicija; /* pronađi kvadrat u kojem */ pozicija = DobitnaPozicija(racunalo); /* može pobijediti racunalo */ if (!pozicija) /* ako ga nema, pronađi */ pozicija = DobitnaPozicija(covjek); /* gdje čovjek pobijeđuje */ if (!pozicija) /* ako ga nema */ pozicija = PraznaSredina(); /* centar je najbolji potez */ if (!pozicija) /* ako ga nema */ pozicija = PrazanUgao(); /* najbolji je potez u kutovima */ if (!pozicija) /* ako ga nema */ pozicija = PrazanaStrana(); /* ostaje mjesto na stranicama */ printf("nJa sam izabrao kvadratic: %d!n", pozicija); kvadrat[(pozicija - 1) / 3][ (pozicija - 1) % 3] = racunalo; } /* * Funkcija: int DobitnaPozicija(char simbol), * ako postoji dobitna kombinacija za simbol, * vraća poziciju kvadratića, inaće vraća 0; */ int DobitnaPozicija(char simbol) { int pozicija, redak, stupac; int rezultat = 0; /* Analiziraj stanje u svih 9 kvadratića. Za svaki kvadratić: * ako je prazan, ispuni ga danim simbolom, i provjeri da li je * je to dobitni potez. Ako jest, zapamti ga u varijabli rezultat * i ponovo poništi taj kvadratić. * Nakon završetka petlje rezultat sadrži dobitni potez ili nulu ako * nije pronađen dobitni potez. * Funkcija vraća vrijednost varijable rezultat */ for (pozicija = 1; pozicija <= 9; pozicija++) { redak = (pozicija - 1) / 3; stupac = (pozicija - 1) % 3; if (kvadrat[redak][stupac] == ' ') { kvadrat[redak][stupac] = simbol; if (is_wining(simbol)) rezultat = pozicija; kvadrat[redak][stupac] = ' '; } } return rezultat; 126
  • 127.
    } /* Funkcija: intPraznaSredina() * vraća 5 ako je srednji kvadratić prazan, inaće vraća 0 */ int PraznaSredina(void) { if (kvadrat[1][1] == ' ') return 5; else return 0; } /* Funkcija: int PrazanUgao() * vraća poziciju jednog od praznih kuteva, * ako su svi kutevi zauzeti, vraća 0 */ int PrazanUgao(void) { if (kvadrat[0][0] == ' ') return 1; if (kvadrat[0][2] == ' ') return 3; if (kvadrat[2][0] == ' ') return 7; if (kvadrat[2][2] == ' ') return 9; return 0; } /* Funkcija: int PrazanaStrana() * vraća poziciju jedne od praznih stranica kvadrata, * ako su sve pozicije zauzete vraća 0 */ int PrazanaStrana(void) { if (kvadrat[0][1] == ' ') return 2; if (kvadrat[1][0] == ' ') return 4; if (kvadrat[1][2] == ' ') return 6; if (kvadrat[2][1] == ' ') return 8; return 0; } Slijed dekompozicije funkcija tipa "od vrha prema dolje" ilustriran je na slici 9.1. 127
  • 128.
    TIc-tac-Toe main() Upute OcistiPlocu PostaviPrvogIgraca IgrajIgru Ponovi Igru NacrtajPlocu DobaviPotez PostajeDobitnik PotezKompjutera PotezCovjeka DobitnaPozicija PraznaSredina PrazanUgao PraznaStrana IspravnaPozicija Slika 9.1 Dekompozicije funkcija u programu Tic-Tac-Toe 9.3 Zaključak Programi se u C jeziku mogu pisati u više odvojenih datoteka - modula. Ponovno prevođenje cijelog programa uzima dosta vremena, dok se pojedina datoteka, koja je manja od ukupnog programa, prevodi mnogo brže. U modulu se može definirati određeni skup funkcija koji se može koristiti i u drugim programima. Module, koji sadrže često korištene funkcije, može se u obliku strojnog koda uvrstiti u biblioteke potprograma. Modul se može pisati, testirati, i ispravljati neovisno od ostatka programa. Proces ispravljanja je pojednostavljen, jer se analizira manji dio programa. Moduli omogućavaju veću preglednost i logičku smislenost programskog koda, jer se u njima obično obrađuje jedinstvena problematika. Primjerice, za obradu matematičkih problema postoje različiti programski paketi s odvojenim modulima za rad s kompleksnim brojevima, vektorima, matricama, itd. Korištenjem principa odvajanja specifikacije od implementacije modula, i skrivanjem podataka koji bilježe stanja objekta koji modul opisuje, dobivaju se moduli neovisni od programa u kojem se koriste. To znatno olakšava timski rad u razvoju softvera. 128
  • 129.
    10 Rad spokazivačima Naglasci: • tip pokazivača • operacije s pokazivačima • ekvivalentnost reference niza i pokazivača • prijenos varijable u funkciju • void pokazivači • pokazivači na funkcije • polimorfne funkcije Pokazivači su varijable koje sadrži adresu nekog memorijskog objekta: varijable ili funkcije. Njihova primjena omogućuje napredne programske tehnike: dinamičko alociranje memorije, apstraktni tip podataka i polimorfizam funkcija. Da bi se moglo shvatiti ove tehnike programiranja, u ovom će poglavlju biti pokazano kako se vrše temeljne operacije s pokazivačima, a u sljedećim će poglavljima biti pokazana njihova primjena u programiranju. 10.1 Tip pokazivača Pokazivačkim varijablama se deklaracijom pridjeljuje tip. Deklariranje pokazivačke varijable se vrši na način da se u deklaraciji ispred imena pokazivačke varijable upisuje zvjezdica *. Primjerice, int *p,*q; /* p i q su pokazivači na int */ označava da su deklarirane pokazivačke varijable p i q, kojima je namjena da sadrže adresu objekata tipa int. Kaže se da su p i q "pokazivač na int". Pokazivači, prije upotrebe, moraju biti inicijalizirani na neku realnu adresu. To se ostvaruje tzv. adresnim operatorom &: p = &sum; /* p iniciran na adresu varijable sum */ q = &arr[2]; /* q iniciran na adresu trećeg elementa niza arr*/ Adresa Vrijednost Identifikator 0x09AC -456 sum 0x09B0 ...... ...... ...... ...... ...... 0x0F10 ...... arr[0] 0x0F14 arr[1] 0x0F18 arr[2] ...... ...... 0x1000 0x09AC p 0x1004 0x0F18 q Slika 10.1 Adresa i vrijednost varijabli 129
  • 130.
    10.2 Operacije spokazivačima Temeljne operacije s pokazivačima su: 1. Deklariranje pokazivača je postupak kojim se deklarira identifikator pokazivača, na način da se između oznake tipa na koji pokazivač pokazuje i identifikatora pokazivača upisuje se operator indirekcije *. int x, *p; /* deklaracija varijable x i pokazivača p */ 2. Inicijalizacija pokazivača je operacija kojom se pokazivaču pridjeljuje vrijednost koja je jednak adresi objekta na koji on pokazuje. Za dobavu adrese objekta koristi se unarni adresni operator '&' . p = &x; /*p sadrži adresu od x */ 3. Dereferenciranje pokazivača je operacija kojom se pomoću pokazivača pristupa memorijskom objektu na kojega on pokazuje, odnosno, ako se u izrazima ispred identifikatora pokazivača zapiše operator indirekcije *, dobiva se dereferencirani pokazivač (*p). Njega se može koristiti kao varijablu, odnosno referencu memorijskog objekta na koji on pokazuje. y = *p; /* y dobiva vrijednost varijable koju p pokazuje*/ /* isti učinak kao y = x */ *p = y; /* y se pridjeljuje varijabli koju p pokazuje */ /* isti učinak kao x = y */ Djelovanje adresnog operatora je komplementarno djelovanju operatora indirekcije, i vrijedi da naredba y = *(&x); ima isti učinak kao i naredba y = x;. Unarni operatori * i & su po prioritetu iznad većine operatora (kada se koriste u izrazima). y = *p + 1; ⇔ y = (*p) + 1; Jedino postfiksni unarni operatori (-- ++, [], ()) imaju veći prioritet od * i & prefiks operatora: y = *p++; ⇔ y = *(p++); Pokazivač kojem je vrijednost nula (NULL) naziva se nul pokazivač. p = NULL; /* p pokazuja na ništa */ S dereferenciranim pokazivačem (*p) se može manipulirati kao sa varijablom pripadnog tipa, primjerice, int x, y, *px, *py; px = &x; /* px sadrži adresu od x – ne utječe na x */ *px = 0; /* vrijednost x postaje 0 - ne utječe na px */ py = px; /* py također pokazuje na x - ne utječe na px ili x */ *py += 1; /* uvećava x za 1 - ne utječe na px ili py */ y = (*px)++; /* y = 1, a x =2 - ne utječe na px ili py */ 130
  • 131.
    10.3 Pokazivači kaoargumenti funkcije Pokazivači se često koriste kao argumenti funkcije, jer se na taj način može prenositi varijable u funkciju. To se postiže na način da se kao parametar funkcije deklarira pokazivač na neki objekt, primjerice: void Increment(int *pVar); Prema pravilu C jezika, pri pozivu funkcije se prenosi vrijednost stvarnog argumenta, a u funkciji se parametar funkcije tretira kao lokalna varijabla koja ima vrijednost stvarnog argumenta. U slučaju kada je parametar funkcije pokazivač, stvarni argument funkcije je adresa objekta koji ima isti tip kao pokazivač. Korištenjem indirekcije pokazivača može se pristupiti tom objektu i mijenjati njegov sadržaj, dakle taj se objekt može tretirati kao varijabla koju se koristi i u funkciji. Ovaj način prenošenja parametara funkcije je prikazan u programu ptr-parm.c. U njemu se pomoću funkcije void Increment(int *pVar){(*pVAr)++;} može inkrementirati vrijednost bilo koje cjelobrojne varijable imena var. To se vrši s pozivom funkcije u obliku Increment(&var);. Uočite da se u definiciji funkcije koristi pokazivački parametar, a pri pozivu funkcije se kao argument koristi adresa varijable na koju ova funkcija djeluje. /* Datoteka: ptr-parm.c */ #include <stdio.h> void Increment(int *pVar) { /* Funkcija inkrementira vrijednost varijable, čija se adresa * prenosi u funkciju kao vrijednost pokazivača pVar * Varijabli se pristupa pomoću indirekcije pokazivača pVar */ (*pVar)++; } int main() { int var = 7; Increment(&var); /* argument je adresa varijable */ printf ("var = %dn", var); return 0; } Ispis je: var = 8 U nekim knjigama se ovaj način prijenosa argumenata naziva "prijenos reference" (call by reference), međutim taj naziv nije ispravan jer se u ovom slučaju ne prenosi referenca varijable ( ime koje označava adresu) već se prenosi vrijednost pokazivača (adresa), a varijabli na koju on pokazuje pristupa se indirekcijom pokazivača. Prijenos varijabli u funkcije pomoću pokazivača koristi se uvijek kada želimo da se pomoću jedne funkcije istovremeno mijenja vrijednost više varijabli. 131
  • 132.
    Primjer: Definirana jefunkciju swap(), pomoću koje se može izvršiti zamjena vrijednost dvije varijable: /* Datoteka: swap.c * Zamjena vrijednosti dvije varijable pomoću swap funkcije */ */ #include <stdio.h> void swap( int *x, int *y) { int t; t = *x; *x = *y; *y = t; } int main() { int a = 1, b = 2; printf("a=%d b=%dn", a, b); swap(&a , &b); printf("Nakon poziva swap(&a, &b)n"); printf("a=%d b=%dn", a, b); } Ispis je: a=1 b=2 Nakon poziva swap(&a, &b) a=2 b=1 10.4 Pokazivači i nizovi Potrebno je najprije navesti nekoliko pravila koje vrijede za reference niza i pokazivače koji pokazuju na nizove. Analizirat ćemo niz a i pokazivač p: int a[10]; int *p; Pravila su: 1. Ime niza, iza kojeg slijedi oznaka indeksa, predstavlja element niza s kojim se manipulira na isti način kao i sa prostim varijablama. 2. Ime niza, zapisano bez oznake indeksa je "pokazivačka konstanta - adresa" koja pokazuje na prvi element niza. Ona se može pridijeliti pokazivačkoj varijabli. Naredbom p = a; za vrijednost pokazivača p postavlja se adresa a[0]. Ovo je ekvivalentno naredbi: 132
  • 133.
    p = &a[0]; 3.Pravilo pokazivačke aritmetike: Ako p pokazuje na a[0], tada (p + i) pokazuje na a[i]. Ako pi pokazuje na a[i], tada pi + k pokazuje na a[i+k], odnosno, ako je pi = &a[i]; tada vrijedi odnos; *(pi+k) ⇔ *(p+i+k) ⇔ a[i+ k] Gornja pravila određuju da se aritmetičke operacije s pokazivačima ne izvode na isti način kao što se izvode aritmetičke operacije s cijelim brojevima. Za prosti cijeli broj x vrijedi: vrijednost(x ± n) = vrijednost(x) ± n dok za pokazivač p vrijedi vrijednost(p ± n) = vrijednost(p) ± n*sizeof(*p) 4. Ekvivalentnost indeksne i pokazivačke notacije niza. Ako je deklariran niz array[N], tada izraz *array označava prvi element, *(array + 1) predstavlja drugi element, itd. Poopći li se ovo pravilo na cijeli niz, vrijede sljedeći odnosi: *(array) ⇔ array[0] *(array + 1) ⇔ array[1] *(array + 2) ⇔ array[2] ... *(array + n) ⇔ array[n] što odgovara činjenici da ime niza predstavlja pokazivačku konstantu. S pokazivačima se također može koristiti indeksna notacija. Tako, ako je inicijaliziran pokazivač p: p = array; tada vrijedi: p[0] ⇔ *p ⇔ array[0] p[1] ⇔ *(p + 1) ⇔ array[1] p[2] ⇔ *(p + 2) ⇔ array[2] ... p[n] ⇔ *(p + n) ⇔ array[n] Jedina razlika u korištenju reference niza i pokazivača na taj niz je u tome da se memorijskim referencama ne može mijenjati vrijednost adrese koju oni označavaju. a++; je nedozvoljen izraz, jer se ne može mijenjati konstanta 133
  • 134.
    p++; je dozvoljenizraz, jer je pokazivač p varijabla Pošto se pokazivaču može mijenjati vrijednost, obično se kaže da se pomoću pokazivača može "šetati po nizu". Primjer: U obje sljedeće petlje ispisuje se vrijednost 10 elemenata niza a. int *p = a; for (i = 0; i < 10; i++, p++) for (i = 0; i < 10; i++) printf("%dn", *p); printf("%dn", a[i]); U prvoj petlji se koristi pokazivač p za pristup elementima niza a[]. Početno on pokazuje na prvi element niza a[]. U petlji se zatim ispisuje vrijednost tog elementa (dereferencirani pokazivač *p), i inkrementira vrijednost pokazivača, tako da se u narednom prolazu petlje s njime referira sljedeći element. Kompilator ne prevodi ove petlje na isti način, iako je učinak u oba slučaja isti: bit će ispisana vrijednost 10 elemenata niza a[]. Ako je učinak isti, možemo se dalje upitati koja će se od ovih petlji brže izvršavati. To ovisi o kvaliteti kompilatora i o vrsti procesora. Kod starijih procesora brže se izvršava verzija s pokazivačem, jer se u njoj u jednom prolazu petlje vrše dva zbrajanja, dok se u drugom slučaju mora (skriveno) izvršiti i jedno množenje. Ono je potrebno da bi se odredila adresa elementa a[i], jer je adresa(a[i])= adresa(a[0]) + i*sizeof(int). Kod novijih se procesora operacija indeksiranja izvršava veoma brzo, pa se u tom slučaju preporučuje korištenje verzije s indeksnim operatorom. 10.5 Pokazivači i argumenti funkcije tipa niza Pri pozivu funkcije, stvarni se argument kopira u formalni argument (parametar) funkcije. U slučaju da je argument funkcije ime niza kopira se adresa prvog elementa, dakle stvarni argument koji se prenosi u funkciju je vrijednost pokazivača na prvi element niza. Stoga se prijenos niza u funkciju može deklarirati i pomoću pokazivača. Primjer: Sljedeće tri funkcije imaju isti učinak i mogu se pozvati s istim argumentima: void print(int x[], void print(int *x, void print(int *x, int N ) int N) int N) { { { int i; while (N--) { int i; for (i = 0; i < N; i++) for (i=0; i<N;i++) printf("%dn", x[i]); printf("%dn", *x); printf("%dn", x[i]); } x++; } } } /* poziv funkcije print */ int niz[10], size=10; . . . . . print(niz, size); 134
  • 135.
    const osigurači U prethodnom primjeru u funkciji print() se koriste vrijednosti elemenata niza x, a ne mijenja se njihova vrijednost. U takovim slučajevima je preporučljivo da se parametri funkcije deklariraju s prefiksom const. void print(const int x[], int N ); ili void print(const int *x, int N ); Ovakva deklaracija je poruka kompilatoru da dojavi grešku ako u funkciji postoji naredba kojom se mijenja sadržaja elemenata niza, a programeru služi kao dodatno osiguranje da će izvršiti implementaciju funkcije koja neće mijenjati elemente niza. Pomoću pokazivača se u funkcije mogu prenositi i proste varijable i nizovi. Primjer: Napisat ćemo funkciju getMinMax() kojom se određuje maksimalna i minimalna vrijednost niza. Testirat ćemo je programom u kojem korisnik unosi 5 brojeva, a program ispisuje maksimalnu i minimalnu vrijednost. /* Datoteka: minmax.c */ #include <stdio.h> #define N 5 /* radit ćemo s nizom od N elemenata */ void getMinMax(double *niz, int nelem, double *pMin, double *pMax) { /* funkcija određuje minimalni i maksimalni element niza * Parametri funkcije su: * niz – niz realnih brojeva * nelem – broj elemenata u nizu * pMin – pokazivac na minimalnu vrijednost * pMax - pokazivac na maksimalnu vrijednost /* int i= 0; *pMin = *pMax = niz[0]; for(i=1; i<N; i++) { if(niz[i] > *pMax ) *pMax = niz[i]; if(niz[i] < *pMin) *pMin = niz[i]; } } int main() { int i; double min, max, data [N]; /* 1. izvjesti korisnika da otkuca 5 realnih brojeva */ printf("Otkucaj %d realnih brojeva:n", N); for (i=0; i<N; i++) scanf("%lg", &data[i]); getMinMax(data, N, &min, &max); /* ispisi minimalnu i maksimalnu vroijednost */ printf ("min = %lf max = %lfn", min, max); return 0; } 135
  • 136.
    10.6 Patrametri funkcijetipa void pokazivača Ako se neki pokazivač deklarira pomoću riječi void, void *p; tada nije određeno na koji tip podatak on pokazuje. Njemu se može pridijeliti adresa bilo kojeg memorijskog objekta, ali se ne može vršiti pristup memorijskim objektima pomoću operatora indirekcije, jer on pokazuja na ništa. Većina današnjih kompilatora ne dozvoljava aritmetičke operacije s void pokazivačima. Očito je da nema smisla koristiti void pokazivače kao regularne varijable. Oni pak mogu biti korisni kao parametri funkcija. Kada se void pokazivač koristi kao parametar funkcije tada se pri pozivu funkcije tom pokazivaču može pridijeliti adresa bilo kojeg memorijskog objekta, a unutar same funkcije se može s prefiksom (tip *) vršiti forsirana pretvorba pokazivačkog tipa. Primjer: Definirana je funkcija void UnesiVrijednost(void *p, int tip), pomoću koje se može izvršiti unos različitih tipova podataka /* Datoteka: unos.c * koristenje void pokazivaca kao parametra funkcije */ #include <stdio.h> #define CHAR 0 #define INT 1 #define FLOAT 2 #define DOUBLE 3 void UnesiVrijednost(void *p, int tip) { switch (tip) { case CHAR: printf( "Unesite jedan znak: n"); scanf("%c", (char *) p); break; case INT: printf( "Unesite cijeli broj:n"); scanf("%d", (int *) p); break; case FLOAT: printf( "Unesite realni broj:n"); scanf("%g", (float *) p); break; case DOUBLE: printf( "Unesite realni broj:n"); scanf("%lg", (double *) p); break; } fflush(stdin); /* odstrani višak znakova s ulaza*/ } int main() { double dval; int ival; 136
  • 137.
    UnesiVrijednost(&ival, INT); printf("Vrijednost je %dn" , ival); UnesiVrijednost(&dval, DOUBLE); printf("Vrijednost je %lgn" , dval); return 0; } Uočite kako je korištena funkcija scanf(). Ispred imena argumenta nije korišten adresni operator jer je vrijednost pokazivača p adresa. Ispred argumenta je eksplicitno označen tip. Ovakvi način korištenje void pokazivača je opasan, jer ako se pri pozivu funkcije ne pozovu kompatibilni argumenti, može doći do nepredvidivih rezultata, čak i do blokade računala. 10.7 Pokazivači na funkcije Funkcije su također memorijski objekti pa se može deklarirati i inicijalizirati pokazivače na funkcije. Pravilo je da se za funkciju imena F, koja je deklarirana (ili definirana) u obliku: oznaka_tipa F (list_ parametara); pokazivač na tu funkciju, imena pF, deklarira u obliku: oznaka_tipa ( *pF) (list_ parametara); Ime funkcije, napisano bez zagrada predstavlja adresu funkcije, pa se pridjelom vrijednosti: pF = F; inicira pokazivač pF na adresu funkcije F. Kada je pokazivač iniciran, indirekcijom pokazivača može se izvršiti poziv funkcije u obliku: (*pF)(lista_argumenata); ili još jednostavnije, sa pF(lista_argumenata); jer, oble zagrade predstavljaju operator poziva funkcije (kompilator sam vrši indirekciju pokazivača, ako se iza njega napišu oble zagrade). Primjerice, iskazom double (*pMatFun)(double); deklarira se pokazivač pMathFun kojem možemo pridijeliti adresu standardnih matematičkih funkcija jer one imaju isti tip parametara i rezultata funkcije (pr. double(sin(double)). Pokazivač na funkciju može biti i argument funkcije, primjerice funkcija void Print( double (*pMatFun)(double), double x) { printf( "%lf", pMatFun (x)); } 137
  • 138.
    se može koristitiza ispis vrijednosti matematičkih funkcija. Print(sin, 3.1); /* ispisuje se vrijednost sinusne funkcije za vrijednost argumenta 3.1*/ Print(cos, 1.7) /* ispisuje se vrijednost kosinus funkcije za vrijednost argumenta 1.7*/ Primjer: U programu pfunc.c koriste se pokazivači na funkciju za ispis vrijednosti standardnih i korisnički definiranih matematičkih funkcija. Izbor funkcije i argumenta funkcije vrši se u interakciji s korisnikom programa. /* Datoteka: pfun.c * korištenje pokazivača na funkciju */ #include <stdio.h> #include <math.h> double Kvadrat (double x) { return x*x; } void PrintVal( double (*pFunc)(), double x) { printf( "nZa x: %lf dobije se %lfn", x, pFunc(x)); } int main() { double val=1; int choice; double (*pFunc)(double); printf("Upisi broj:"); scanf("%lf", &val); fflush(stdin); /* odstrani višak znakova s ulaza */ printf( "n(1)Kvadrat n(2)Sinus n(3)Kosinus n"); printf( "nOdaberi 1, 2 li 3n"); choice = getchar(); switch (choice) { case '1': pFunc = Kvadrat; break; case '2': pFunc = sin; break; case '3': pFunc = cos; break; default: return 0; } PrintVal (pFunc, val); return 0; } Često se koriste nizovi pokazivača na funkciju. Primjerice, deklaracijom #include math.h 138
  • 139.
    double (*pF[4])(double) ={sin, cos, tan, exp}; deklariran je niz od 4 elementa koji sadrže pokazivač na funkciju kojoj je parametar tipa double i koja vraća vrijednost tipa double. Također je izvršena i inicijalizacija elemenata niza na adresu standardnih matematičkih funkcija. Sada je naredba x = (*pF[1])(3.14) ekvivalentna naredbi x= cos(3.14). Primjer: U programu niz-pfun.c koristi se niz pokazivača na funkciju. U nizu se bilježe adrese funkcija sin(), cos() i korisnički definirane funkcije Kvadrat(). Zatim se od korisnika traži da unese broj i da odabere funkciju. Na kraju se ispisuje rezultat primjene funkcije na uneseni broj. /* Datoteka: niz-pfun.c * korištenje niza pokazivača na funkciju */ #include <stdio.h> #include <math.h> double Kvadrat (double x) {return x*x;} int main() { double val=1; int izbor; double (*pF[3])(double)= {Kvadrat, sin, cos}; printf("Upisi broj:"); scanf("%lf", &val); fflush(stdin); printf( "n(1)Kvadrat n(2)Sinus n(3)Kosinus n"); printf( "nOdaberi 1, 2 li 3n"); scanf("%d" ,&izbor); if (izbor >=1 && izbor <=3) printf( "nRezultat je %lfn", (*pF[izbor-1])(val)); return 0; } 10.8 Kompleksnost deklaracija Očito je da se u C jeziku koriste vrlo kompleksne deklaracije. One na prvi pogled ne otkrivaju o kakovim se tipovima radi. Sljedeća tablica pokazuje deklaracije koje se često koriste. U deklaraciji: x je ime koje predstavlja ... T x; objekt tipa T T x[]; (otvoreni) niz objekata tipa T T x[n]; niz od n objekata tipa T T *x; pokazivač na objekt tipa T T **x; pokazivač na pokazivač tipa T T *x[]; niz pokazivača na objekt T T *(x[]); niz pokazivača na objekt T 139
  • 140.
    T (*x)[]; pokazivač na niz objekata tipa T T x(); funkcija koja vraća objekt tipa T T *x() funkcija koja vraća pokazivač na objekt tipa T T (*x()); funkcija koja vraća pokazivač na objekt tipa T T (*x)(); pokazivač na funkciju koja vraća objekt tipa T T (*x[n])(); niz od n pokazivača na funkciju koja vraća objekt tipa T Dalje će biti pokazano: 1. Kako sistematski pročitati ove deklaracije. 2. Kako se uvođenjem sinonima tipova (pomoću typedef) može znatno smanjiti kompleksnost deklaracija. Deklaracija nekog identifikatora se čita ovim redom: Identifikator je (... desna strana deklaracije) (.. lijeva strana deklaracije) S desne strane identifikatora mogu biti uglate ili oble zagrade. Ako su uglate zagrade čitamo : identifikator je niz , Ako su oble zagrade čitamo identifikator je funkcija. Ukoliko ima više operatora s desne strane nastavlja se čitanje po istom pravilu. Zatim se analizira zapis s lijeve strane identifikatora ( tu može biti operator indirekcije i oznaka tipa). Ako postoji operator indirekcije, čitamo: identifikator je (... desna strana) pokazivač na tip. Ako postoji dvostruki operator indirekcije, čitamo: identifikator je (... desna strana) pokazivač na pokazivač tip Ukoliko je dio deklaracije napisan u zagradama onda se najprije čita značaj zapisa u zagradama. Primjerice, u deklaraciji T (*x[n])(double); najprije se čita dio deklaracije (*x[n]) koji znači da je x niz pokazivača. Pošto je s desne strane ovog izraza (double) znači da je x niz pokazivača na funkciju koja prima argument tipa double i koja vraća tip T. Znatno jednostavniji i razumljiviji način zapisa kompleksnih deklaracija postiže se korištenjem sinonima za kompleksne tipove. Sinonimi tipova se definiraju pomoću typedef. Primjerice, typedef deklaracijom typedef double t_Fdd(double); /* tip funkcije ... */ uvodi se oznaka tipa t_fdd koja predstavlja funkciju koja vraća double i prima argument tipa double. Dalje se može definirati tip t_pFdd koji je pokazivač na funkciju koja vraća double i prima argument tipa double, s deklaracijom: typedef t_Fdd *t_pFdd; /* tip pokazivača na funkciju ...*/ 140
  • 141.
    što je ekvivalentnodeklaraciji sinonima tipa: typedef double (*t_pFdd)(double); Pomoću tipa t_pFdd može se deklarirati niz pokazivača na funkciju iz prethodnog programa: t_pFdd pF[3] = {Kvadrat, sin, cos}; Očito da je ovakovu deklaraciju znatno lakše razumjeti. Primjer: Primjeni li se navedene typedef deklaracije u programu niz-pfun.c, dobije se /* Datoteka: niz-pfun1.c */ #include <stdio.h> #include <math.h> typedef double t_Fdd(double); /* tip funkcije koja ... */ typedef t_pFdd *t_pFdd; /* tip pokazivača na funkciju ...*/ double Kvadrat (double x) {return x*x;} void PrintVal(t_pFdd pFunc, double x) { printf( "nZa x: %lf dobije se %lfn", x, pFunc(x)); } int main() { double val=1; int izbor; t_pFdd pF[3] = {Kvadrat, sin, cos}; printf("Upisi broj:"); scanf("%lf", &val); fflush(stdin); printf( "n(1)Kvadrat n(2)Sinus n(3)Kosinus n"); printf( "nOdaberi 1, 2 li 3n"); scanf("%d" ,&izbor); if (izbor >=1 && izbor <=3) printf( "nRezultat je %lfn", (*pF[izbor-1])(val)); return 0; } 10.9 Polimorfne funkcije Funkcije koje se mogu prilagoditi različitim tipovima argumenata nazivaju se polimorfne funkcije. Polimorfne funkcije se u C jeziku realiziraju pomoću parametara koji imaju tip void pokazivača i pokazivača na funkcije. Dvije takove funkcije implementirane su u standardnoj biblioteci C jezika. To su qsort() i bsearch() funkcija, čija je deklaracija dana u <stdlib.h>. void qsort(void *a, /* pokazivač niza */ size_t n, /* broj elemenata */ size_t elsize, /* veličina elementa u bajtima */ 141
  • 142.
    int (*pCmpF)(void *,void *)) qsort() funkcija služi za sortiranje elemenata niza a, koji sadrži n elemenata veličine elsize bajta. Elementi se sortiraju od manje prema većoj vrijednosti, odnosno prema kriteriju usporedbe koji određuje funkcija (*pCmpF)(). Usporedna funkcija mora biti deklarirana u obliku int ime_funkcije(const void *p1, const void *p2); Argumenti ove funkcije su pokazivači na dva elementa niza. Funkcija mora vratiti vrijednost nula ako su ta dva elementa jednaka, pozitivnu vrijednost ako je prvi element veći od drugoga, a negativnu vrijednost ako je prvi element manju od drugoga. Primjerice, za sortiranje niza cijelih brojeva usporedna funkcija ima oblik: int CmpInt(const void *p1, const void *p2) { int i1 = *((int *)p1); int i2 = *((int *)p2); if( i1 == i2) return 0; else if( i1 > i2) return 1; else return -1; /* i2 > i1 */ } Najprije se s adresa p1 i p2 dobavlja cjelobrojne vrijednosti i1 i i2. Dobava vrijednosti se vrši tako da se najprije izvrši pretvorba void pokazivača u int*, a zatim se primijeni indirekcija pokazivača. Nakon toga se vrši usporedba vrijednosti i vraća dogovorena vrijednost. Testiranje primjene funkcije qsort() je u programu sorti.c. U programu se vrši sortiranje niza cijelih brojeva. /* Datoteka: sorti.c */ /* koristenje polimorfne qsort funkcije */ #include <stdio.h> #include <stdlib.h> int CmpInt(const void *p1, const void *p2) { ...... prethodna definicija ... } int main() { int i, A[] = {3,1,13,2,17}; int numel=sizeof(A)/sizeof(A[0]); int elsize = sizeof(A[0]); for(i=0; i <numel; i++) printf(" %d", A[i]); printf("n Nakon sortiranjan"); qsort(A, numel, elsize, CmpInt); 142
  • 143.
    for(i=0; i <numel;i++) printf(" %d", A[i]); printf("n"); return 0; } Nakon izvršenja dobije se ispis: 3 1 13 2 17 Nakon sortiranja 1 2 3 13 17 Zadatak: Napišite program sortf.c u kojem se vrši sortiranje niza realnih brojeva. Koristite qsort() funkciju. Definirajte prikladnu usporednu funkciju int FloatCmp(..). Funkcija za polimorfno traženje elementa niza Slijedi opis polimorfne funkcije search() kojom se određuje da li u nekom nizu postoji element zadane vrijednosti. Funkcija vraća pokazivač na traženi element, ako postoji , ili NULL ako u tom nizu ne postoji zadana vrijednost. Koristi se deklaracija funkcije slična funkciji qsort(), jedino se još dodaje argument tipa void pokazivača na varijablu koja sadrži vrijednost koju se traži u nizu A. void * search(const void *x, /* pokazivač na zadanu vrijednost */ const void *A, /* pokazivač niza A*/ size_t n, /* broj elementa niza */ size_t elsize, /* veličina elementa u bajtima */ int (*pCmpF)( const void *, const void *)) /*funkcija */ { int indx; char *adr; for(indx = 0; indx < n; indx++) { adr= (char*)A +indx*elsize; /* adresa elementa niza */ if( (*pCmpF)((void *)adr, x) == 0) break; } if(indx == n) /* tada ni jedan element nema vrijednost x*/ return NULL; else return (void *) adr; /*vrati adresu elementa */ } Testiranje primjene funkcije search() vrši se programom searchi.c: /* Datoteka: searchi.c*/ #include <stdio.h> #include <stdlib.h> void *search(const void *x, const void *A, size_t n, size_t elsize, int (*pCmpF)( const void *, const void *)) { 143
  • 144.
    ....... prema prethodnojdefiniciji } int CmpInt(const void *p1, const void *p2) { ....... prema prethodnoj definiciji } int main() { int i, indx, x = 2; /* x- tražena vrijednost */ int A[] = {3,1,13,2,17, 7, 0, 11}; /* u nizu A*/ int numel=sizeof(A)/sizeof(A[0]); int elsize = sizeof(A[0]); int *pEl; for(i=0; i <numel; i++) printf(" %d", A[i]); pEl=(int *) search(&x, A, numel, elsize, CmpInt); printf("n Element vrijednosti %d, na adresi %Fpn", *pEl, pEl); printf("n"); return 0; } Nakon izvršenja dobije se ispis: 3 1 13 2 17 7 0 11 Element vrijednosti 2, na adresi 0022FF4C U standardnoj biblioteci je implementirana funkcija bsearch() koja ima istu deklaraciju kao funkcija search(). Razlika ove dvije funkcije je u tome što je bsearch() funkcija specijalizirana i optimirana za slučaj da se traženje elementa niza provodi na prethodno sortiranom nizu. Kasnije ćemo pokazati kako su realizirane funkcije bsearch() i qsort(). 10.10 Zaključak Pokazivači zauzimaju središnje mjesto u oblikovanju C programa. Pokazivač je varijabla koja sadrži adresu. Ako je to adresa varijable, kaže se da pokazivač "pokazuje" na tu varijablu. U radu s pokazivačima koriste se dva specifična operatora: adresni operator (&) i operator indirekcije(*). Adresni operator napisan ispred imena varijable vraća u izraz adresu varijable, a operator indirekcije *, postavljen ispred imena pokazivača, referira sadržaj varijable na koju pokazivač pokazuje. Pokazivači i nizovi su u specijalnom odnosu. Ime niza, napisano bez uglatih zagrada predstavlja pokazivačku konstanu koja pokazuje na prvi element niza. Indeksna notacija ima ekvivalentni oblik pokazivačke notacije. Uglate zagrade imaju karakter indeksnog operatora, jer kada se koriste iza imena pokazivača, uzrokuju da se tim pokazivačem može operirati kao s nizom. Nizovi se prenose u funkcije na način da se u funkciju prenosi pokazivač na prvi element niza, odnosno adresa prvog elementa niza. Pošto funkcija zna adresu niza, u njoj se mogu 144
  • 145.
    koristiti naredbe kojemijenjaju sadržaj elemenata niza bilo u indeksnoj ili u pokazivačkoj notaciji. Deklaracija niza kao parametra funkcije može se izvršiti u indeksnoj ili pokazivačkoj notaciji. Preporučuje se upotreba pokazivačke notacije jer sa tada poziv funkcije može vršiti sa statičkim nizovima i pokazivačima koji pokazuju na neki niz. Funkcija ne raspolaže s podatkom o broju elemenata niza. Zadatak je programera da tu vrijednost, ukoliko je potrebna, predvidi kao parametar funkcije. Korištenjem void pokazivača i pokazivača na funkcije mogu se realizirati polimorfne funkcije. Ime funkcije je konstantni pokazivač na funkciju. 145
  • 146.
    11 Nizovi znakova- string Naglasci: • ASCIIZ stringovi • standardne funkcije za rad sa stringovima • ulazno izlazne operacije sa stringovima • konverzije stringa • nizovi stringova • argumenti komandne linije operativnog sustava Bit će pokazano kako se formiraju i obrađuju nizovi znakova. Od posebnog interesa su nizovi znakova koji imaju karakteristike stringa. 11.1 Definicija stringa String je naziv za memorijski objekt koji sadrži niz znakova, a posljednji znak u nizu mora biti nulti znak ('0'). Deklarira se kao niz znakova (pr. char str[10]) , ili kao pokazivač na znak (char *str), ali pod uvjetom da se pri inicijalizaciji niza i kasnije u radu s nizom uvijek vodi računa o tome da posljednji element niza mora biti jednak nuli. Zbog ove se karakteristike stringovi u C jeziku nazivaju ASCIIZ stringovi. Sastoje od niza ASCII znakova i nule (eng. Z - zero). Duljina stringa je cjelobrojna vrijednost koja je jednaka broju znakova u stringu (bez nultog znaka). Indeks "nultog" znaka jednak je broju znakova u stringu, odnosno duljini stringa. Primjerice, string koji sadrži tekst: Hello, World!, u memoriji zauzima 14 bajta. Njegova duljina je 13, jer je indeks nultog znaka jednak 13. 0 1 2 3 4 5 6 7 8 9 10 11 12 13 H e l l o , W o r l d ! 0 Pogledajmo primjer u kojem se string tretira kao niz znakova. /* Prvi C program – drugi put. */ #include <stdio.h> #include <string.h> int main() { char hello[14] = { 'H', 'e', 'l', 'l', 'o', ',', ' ', 'W', 'o', 'r', 'l', 'd', '!', '0' }; printf("%sn", hello); printf("Duljina stringa je %d.n", strlen(hello)); return 0; } 146
  • 147.
    Nakon izvršenja programa,dobije se poruka: Hello, World! Duljina stringa je 13. Ovaj program vrši istu funkciju kao i prvi C-program iz ove knjige, tiska poruku: Hello, World!. U programu je prvo definirana i inicijalizirana varijabla hello. Ona je tipa znakovnog niza od 14 elemenata. Inicijalizacijom se u prvih 13 elemenata upisuju znakovi (Hello World!), a posljednji element se inicira na nultu vrijednost. Ispis ove varijable se vrši pomoću printf() funkcije sa specifikatorom formata ispisa %s. Duljina stringa je određena korištenjem standardne funkcije size_t strlen(char *s); Parametar funkcije je pokazivač na char, odnosno, pri pozivu funkcije argument je adresa početnog elementa stringa. Funkcija vraća vrijednost duljine stringa (size_t je sinonim za unsigned). Funkcija strlen() se može implementirati na sljedeći način: unsigned strlen(const char *s) { unsigned i=0; while (s[i] != '0') /* prekini petlju za s[i]==0, inače */ i++; /* inkrementiraj brojač znakova */ return i; /* i sadrži duljinu stringa */ } ili pomoću pokazivačke aritmetike: unsigned strlen(const char *s) { unsigned i = 0; while (*s++ != '0') /* prekini petlju za *s==0, inače */ i++; /* inkrementiraj brojač i pokazivač*/ return i; /* i sadrži duljinu stringa */ } String se može inicijalizirati i pomoću literalne konstante: char hello[] = "Hello, World!"; /* kompilator rezervira mjesto u memoriji */ Elementi stringa se mogu mijenjati naredbom pridjele vrijednosti, primjerice naredbe: hello[0] = 'h'; hello[6] = 'w'; mijenjaju sadržaj stringa u "hello world"; Ako se procjenjuje da će trebati više mjesta za string, nego se to navodi inicijalnim literalnim stringom, tada treba eksplicitno navesti dimenziju stringa. Primjerice, deklaracijom char hello[50] = "Hello, World!"; 147
  • 148.
    kompilator rezervira 50mjesta u memoriji te inicira prvih 14 elemenata na "Hello, World!", zaključno s nultim znakom. String je i svaki pokazivač koji se inicijalizira na adresu memorijskog objekta koji ima karakteristike stringa. Stoga, i sljedeći iskaz predstavlja deklaraciju stringa: char *digits ="0123456789ABCDEF"; /* kompilator inicijalizira pokazivač */ /* na adresu od "0123456789ABCDEF"; */ Ovakva inicijalizacija je moguća jer kompilator interno literalni string tretira kao referencu, pa se njegova adresa pridjeljuje pokazivaču digits. Dozvoljeno je čak literalnu string konstantu koristiti kao referencu niza, primjerice printf("%c", "0123456789ABCDEF"[n]); ispisuje n-ti znak stringa "0123456789ABCDEF". 11.2 Standardne funkcije za rad sa stringovima U standardnoj biblioteci postoji niz funkcija za manipuliranje sa stringovima. One su deklarirane u datoteci "string.h". Funkcija im je: size_t strlen(const char *s) Vraća duljinu stringa s. char *strcpy(char *s, const char *t) Kopira string t u string s, uključujući '0'; vraća s. char *strncpy(char *s, const char *t, size_t n) Kopira najviše n znakova stringa t u s; vraća s. Dopunja string s sa '0' znakovima ako t ima manje od n znakova. char *strcat(char *s, const char *t) Dodaje string t na kraj stringa s; vraća s. char *strncat(char *s, const char *t, size_t n) Dodaje najviše n znakova stringa t na string s, i znak '0'; vraća s. int strcmp(const char *s, const char *t) Uspoređuje string s sa stringom t, vraća <0 ako je s<t, 0 ako je s==t, ili >0 ako je s>t. Usporedba je leksikografska, prema ASCII "abecedi". int strncmp(const char *s, const char *t, size_t n) Uspoređuje najviše n znakova stringa s sa stringom t; vraća <0 ako je s<t, 0 ako je s==t, ili >0 ako je s>t. char *strchr(const char *s, int c) Vraća pokazivač na prvu pojavnost znaka c u stringu s, ili NULL znak ako c nije sadržan u stringu s. 148
  • 149.
    char *strrchr(const char*s, int c) Vraća pokazivač na zadnju pojavnost znaka c u stringu s, ili NULL znak ako c nije sadržan u stringu s. char *strstr(const char *s, const char *t) Vraća pokazivač na prvu pojavu stringa t u stringu s, ili NULL ako string s ne sadrži string t. size_t strspn(const char *s, const char *t) Vraća duljinu prefiksa stringa s koji sadrži znakove koji čine string t. size_t strcspn(const char *s, const char *t) Vraća duljinu prefiksa stringa s koji sadrži znakove koji nisu prisutni u stringu t. char *strpbrk(const char *s, const char *t) Vraća pokazivač na prvu pojavu bilo kojeg znaka iz string t u stringu s, ili NULL ako nije prisutan ni jedan znak iz string t u stringu s. char *strerror(int n) Vraća pokazivač na string kojeg interno generira kompilator za dojavu greške u nekim sistemskim operacijama. Argument je obično globalna varijabla errno, čiju vrijednost također postavlja kompilator pri sistemskim operacijama. char *strtok(char *s, const char *sep) vrši razlaganje stringa s na niz leksema koji su razdvojeni znakovima-separatorima. Skup znakova-separatora se zadaje u stringu sep. Funkcija vraća pokazivač na leksem ili NULL ako nema leksema. Korištenje funkcije strtok() je specifično jer u stringu može biti više leksema, a ona vraća pokazivač na jedan leksem. Da bi se dobili sljedeći leksemi treba ponovo zvati istu funkciju, ali s prvim argumentom jednakim NULL. Primjerice, za string char * s = "Prvi drugi,treci"; ako odaberemo znakove separatore: razmak, tab i zarez, tada sljedeći iskazi daju ispis tri leksema (Prvi drugi i treci): char *leksem = strtoken(s, " ,t"); /* dobavi prvi leksem */ while( leksem != NULL) { /* ukoliko postoji */ printf("", leksem); /* ispiši ga i */ lexem = strtok(NULL, " ,t"); /* dobavi sljedeći leksem */ } /* pa ponovi postupak */ Sljedeća dva primjera pokazuju kako se mogu napisati standardne funkcije za kopiranje stringa (strcpy) i leksičku usporedbu dva stringa (strcmp). Funkcija strcpy() kopira znakove stringa src u string dest, a vraća adresu od dest. 1. Verzija s indeksnim operatorom: 149
  • 150.
    char *strcpy( char*dst, const char *src) { int i; for (i = 0; src[i] != '0'; i++) dst[i] = src[i]; dst[i] = '0'; return dst; } 2. Verzija s pokazivačkom aritmetikom char *strcpy(char *dest, const char *src) { char *d = dest; while(*d = *src) /* true dok src ne bude '0'*/ { d++; src++; } return dest; } Funkcija strcmp()služi usporedbi dva stringa s1 i s2. Deklarirana je s: int strcmp(const char *s1, const char *s2) Funkcija vraća vrijednost 0 ako je sadržaj oba stringa isti, negativnu vrijednost ako je s1 leksički manji od s2, ili pozitivnu vrijednost ako je s1 leksički veći od s2. Leksička usporedba se izvršava znak po znak, prema kodnoj vrijednosti ASCII standarda. /*1. verzija*/ int strcmp( char *s1, char *s2) { int i = 0; while(s1[i] == s2[i] && s1[i] != '0') i++; if (s1[i] < s2[i]) return -1; else if (s1[i] > s2[i]) return +1; else return 0; } Najprije se u glavi petlje uspoređuje da li su znakovi s istim indeksom isti i da li je dostignut kraj niza (znak '0'). Kad petlja završi, ako su oba znaka jednaka, to znači da su i svi prethodni znakovi jednaki. U tom slučaju funkcija vraća vrijednost 0. U suprotnom funkcija vraća 1 ili –1 ovisno o numeričkom kodu znakova koji se razlikuju. /*2. verzija s inkrementiranjem pokazivača*/ int strcmp(char *s1, char *s2) { while(*s1 == *s2) { if(*s1 == '0') return 0; s1++; 150
  • 151.
    s2++; } return *s1 - *s2; } Verzije u kojima se koristi inkrementiranje pokazivačka često su kod starije generacije procesora rezultirale bržim izvršenjem programa. Kod novije generacije procesora to nije slučaj, pa se preporučuje korištenje indeksne notacije. 11.3 Ulazno-izlazne operacije sa stringovima Najjednostavniji način dobave i ispisa stringa je da se koriste printf() i scanf() funkcije s oznakom formata %s. Primjerice, s char line[100]; scanf("%s",str); printf("%s", str); najprije se vrši unos stringa (korisnik otkuca niz znakova i <enter>). Zatim se vrši ispis tog stringa pomoću printf() funkcije. Funkcija scanf() nije pogodna za unos stringova u kojima ima bijelih znakova, jer i oni znače kraj unosa, stoga se za unos i ispis stringa češće koriste funkcije: char *gets(char *str); int puts(char *str); Funkcija gets() dobavlja liniju teksta sa standardnog ulaza (unos se prekida kada je na ulazu 'n') i sprema je u string str, kojeg zaključuje s nul znakom. Podrazumijeva se da je prethodno rezervirano dovoljno memorije za smještaj stringa str. Funkcija vraća pokazivač na taj string ili NULL ako nastupi greška ili EOF. Funkcija puts() ispisuje string str na standardni izlaz. Vraća pozitivnu vrijednost ili -1 (EOF) ako nastupi greška. Jedina razlika između funkcije printf("%s", str) i puts(str) je u tome da funkcija puts(str) uvijek na kraju ispisa dodaje i znak nove linije. Primjer: U programu str-io.c pokazana je upotreba raznih funkcija za rad sa stringovima. Program dobavlja liniju po liniju teksta sve dok se ne otkuca: "kraj". Nakon toga se ispisuje ukupno uneseni tekst. /* str-io.c */ #include <stdio.h> #include <ctype.h> #include <string.h> #define MAXCH 2000 int main( void) { char str[100]; /* string u kojeg se unosi jedna linija teksta */ char text[MAXCH]; /* string u kojeg se zapisuje ukupno uneseni tekst*/ /* iniciraj string text sa sljedecim tekstom */ 151
  • 152.
    strcpy(text, "Unijeli stetekst;n"); puts("Otkucaj nekoliko linija teksta."); /* informiraj korisnika */ puts("Za kraj unosa otkuca: kraj"); /* kako se vrši unos */ while (gets(str) != NULL ) /* dobavljaj string */ { if(strcmp(str, "kraj") == 0) /* prekini ako je otkucan kraj */ break; /* prekini ako duljina texta premaši veličinu MAXCH */ if(strlen(str)+strlen(text) >= MAXCH) break; strcat(text, str); /* dopuni ukupan tekst sa str */ strcat(text, "n"); /* i oznaci novi red */ } puts(text); /* ispisi tekst koji je unesen */ return 0; } Početno je string text inicijaliziran na način da je u njega kopiran string "Unijeli ste tekst;n" pomoću funkcije strcpy(). Kasnije se taj string dopunjuje sa sadržajem stringa str koje unosi korisnik programa u petlji: while (gets(str) != NULL ) U tijelu petlje prvo se ispituje sadržaj unesenog stringa. Ako je unesen string "kraj", petlja se prekida. To se ispituje iskazom: if(strcmp(str, "kraj") == 0) /* prekini ako je otkucan kraj*/ break; Dopuna stringa text vrši se iskazima: strcat(text, str); /* dopuni ukupan tekst sa str*/ strcat(text, "n"); /* i oznaci novi red */ Za string tekst je rezervirano MAXCH (2000) bajta, što je vjerojatno dovoljno za prihvat teksta. Ako se pokuša unijeti više znakova, unos se prekida iskazom: if(strlen(str)+strlen(text) >= MAXCH) break; Na kraju se ispisuje string text, koji sadrži ukupno otkucani tekst. Zadatak: Modificirajte prethodni program na način da se program prekine i u slučaju ako se bilo gdje unutar unesenog stringa nalazi riječ "kraj". U tu svrhu koristite funkciju strstr(). Zadatak: Modificirajte prethodni program na način da se na kraju programa ispiše koliko ukupno ima riječi u stringu tekst s više od tri slova. U tu svrhu koristite funkciju strtok() i strlen(). 11.4 Korisnički definirane ulazne operacije sa stringovima Veliki nedostatak funkcija scanf() i gets() je u tome da se ne može ograničiti broj unesenih znakova, pa treba koristiti stringove za koje je rezerviran veliki broj bajta. Zbog ovog ograničenja mnogi programeri radije sami definiraju funkciju za unos stringa u obliku: 152
  • 153.
    int getstring(char *str,int maxchar); Parametri ove funkcije su string str i cijeli broj maxchar, kojim se zadaje maksimalno dozvoljeni broj znakova koji će biti spremljen u string. Funkcija vraća broj znakova ili 0 ako je samo pritisnut znak nove linije ili EOF ako je na početku linije otipkano Ctrl-Z. Funkcija getstring() se može implementirati na sljedeći način: #include <stdio.h> int getstring(char *str, int maxchar) { int ch, nch = 0; /* početno je broj znakova = 0 */ --maxchar; /* osiguraj mjesto za '0' */ while((ch = getchar()) != EOF) /* dobavljaj znak */ { if(ch == 'n') /* prekini unos na kraju linije */ break; /* prihvati unos samo ako je broj znakova < maxchar */ if(nch < maxchar) str[nchar++] = ch; } if(ch == EOF && nch == 0) return EOF; str[nch] = '0'; /* zaključi string s nul znakom */ return nch; } Testiranje ove funkcije provodi se programom getstr.c. U njemu korisnik unosi više linija teksta, a unos završava kada se otkuca Ctrl-Z. #include <stdio.h> int getstring(char *, int); main() { char str[256]; while(getstring(str, 256) != EOF) printf("Otipkali ste "%s"n", str); return 0; } 11.5 Pretvorba stringa u numeričku vrijednost Funkciju getstring()se može iskoristiti i za unos numeričkih vrijednosti, jer u standardnoj biblioteci postoje funkcije 153
  • 154.
    int atoi(char *str); double atof(char *str); koje vrše pretvorbu znakovnog zapisa u numeričku vrijednost. Ove funkcije su deklarirane u <stdlib.h>. Funkcija atoi() pretvara string u vrijednost tipa int a funkcija atof() pretvara string u vrijednost tipa double. Podrazumijeva se da string sadrži niz znakova koji se koriste za zapis numeričkih literala. U slučaju greške ove funkcije vraćaju nulu. Greška se uvijek javlja ako prvi znak nije znamenka ili znakovi + i -. Primjer: U programu str-cnv.c prikazano je kako se može vršiti unos numeričkih vrijednosti pomoću funkcija getstring(), atoi() i atof(): /* str-cnv.c */ #include <stdio.h> #include <stdlib.h> int getstring(char *, int); #define NCHARS 30 int main() { int i; double d; char str [NCHARS]; puts("Otipkajte cijeli broj"); getstring(str, NCHARS); i = atoi(str); printf("Otipkali ste %dn", i); puts("Otipkajte realni broj"); getstring(str, NCHARS); d = atof(str); printf("Otipkali ste %lgn", d); return 0; } Drugi način da se iz stringa dobije numerička vrijednost je da se koristi funkcija sscanf(). int sscanf (char *str, char *format, ... ); Ova funkcija ima isto djelovanje kao funkcija scanf(). Razlika je u tome da sscanf() prima znakove iz stringa str, dok funkcija scanf() prima znakove sa standardnog ulaza. Primjer: pretvorba stringa, koji sadrži znakovni zapis cijelog broja, u numeričku vrijednost cjelobrojne varijable vrši se iskazom sscanf( str, "%d", &i); Ponekad će biti potrebno izvršiti pretvorbu numeričke vrijednosti u string. U tom slučaju se može koristiti standardna funkcija: int sprintf(char *str, char *format, ... ); 154
  • 155.
    koja ima istodjelovanje kao printf() funkcija, ali se ispis vrši u string, a ne na standardni izlaz. 11.6 Nizovi stringova Nizovi stringova se jednostavno deklariraju i inicijaliziraju kao nizovi pokazivača na char. Primjerice, za rad s igračkim kartama može se koristiti dva niza stringova: jedan za boju, a drugi za lik karata. Deklariraju se na sljedeći način: char *boja[] = {"Srce", "Tref", "Karo", "Pik "}; char *lik[] = {"As", "2", "3", "4", "5", "6", "7", "8", "9", "10", "Jack", "Dama", "Kralj" }; Pojedinom stringu se pristupa pomoću indeksa, primjerice boja[2] označava string "Karo". Pri deklaraciji se koristi pravilo da operator indirekcije ima niži prioritet od uglatih zagrada, tj. da je deklaracija char *boja[] ekvivalentna deklaraciji: char *( boja []), pa se iščitava: boja je niz pokazivača na char. Isto vrijedi i za upotrebu varijable lik. Primjerice, *lik[i] se odnosi na prvi znak i-tog stringa (tj. lik[i][0]). Primjer: Dijeljenje karata Program karte.c služit će da se po slučajnom uzorku izmiješaju igrače karte. Za miješanje karate koristi se proizvoljna metoda: Postoje 52 karte. Njih se registrira u nizu cijelih brojeva int karte[52]. Svaka karta[i] sadrži vrijednost iz intervala 0..51 koja označava kombinaciju boje i lika karte, prema pravilu : oznaka boje: boja[karte[i] / 13]; /* string boja[0 do 3] */ oznaka lika: lik [karte[i] % 13]; /* string lik[0 do 12] */ jer ima 13 likova i 4 boje. To znači da ako se početni raspored inicijalizira s for (i = 0; i < BROJKARATA; i++) karte[i] = i; karte će biti složene po bojama, od asa prema kralju (prva će biti as-srce a posljednja kralj-pik). Za miješanje karata koristit će se standardna funkcija int rand(); koja je deklarirana u <stdlib.h>. Ova funkcija pri svakom pozivu vraća cijeli broj iz intervala 0 do RAND_MAX (obično je to vrijednost 32767) koji se generira po slučajnom uzorku. Kasnije će biti pokazano kako je implementirana ova funkcija. Miješanje karata se provodi na način da se položaj svake karte zamijeni s položajem koji se dobije po slučajnom zakonu: for (i = 0; i < BROJKARATA; i++) { int k = rand() % BROJKARATA; swap (&karte[i], &karte[k]); } /* Datoteka: karte.c - dijeljenje karata*/ 155
  • 156.
    #include <stdio.h> #include <stdlib.h> char *boja[] = {"Srce", "Tref", "Karo", "Pik "}; char *lik[] = {"As", "2", "3", "4", "5", "6", "7", "8", "9", "10", "Jack", "Dama", "Kralj" }; #define BROJKARATA 52 void swap(int *x, int *y) { int t = *x; *x = *y; *y = t; } int main( void) { int i, karte[BROJKARATA]; /* ukupno 52 karte */ for (i = 0; i < BROJKARATA; i++) karte[i] = i; for (i = 0; i < BROJKARATA; i++) { int k = rand() % BROJKARATA; swap (&karte[i], &karte[k]); } for (i = 0; i < BROJKARATA; i++) printf("%s - %sn", boja[karte[i]/13], lik[karte[i]%13]); return 0; } Dobije se ispis: Pik - 3 Srce - 9 Tref - 10 Srce - 10 Tref - 5 Tref - 8 Tref - Kralj . . . . . Pik - 7 Srce - 3 Tref - Dama Tref - Jack Tref - 6 Karo - 8 156
  • 157.
    11.7 Generator slučajnihbrojeva Znanstvenici su pokazali da se može realizirati niz slučajnih cijelih brojeva pomoću tzv. linearne kongruencijske sekvence; xi = (a * xi-1 + b) % c u kojoj se vrijednost i-tog broja dobije iz prethodno izračunate vrijednosti (xi-1) i tri konsatante a,b,c. Vrijednost ovih konstanti se određuje iz uvjeta da brojevi iz ove sekvence imaju jednaku vjerojatnost pojavljivanja, odnosno da imaju uniformnu razdiobu gustoće vjerojatnosti. U ANSI/ISO standardnoj biblioteci implementiran je generator s konstantama a=1103515245, b=12345 i c= 232, pomoću jedne globalne varijable seed i dvije funkcije rand() i srand(). static unsigned int seed = 1; int rand(void) { /* vraća pseudo-slučajni broj iz intervala 0..32767 */ seed = seed * 1103515245 + 12345; return seed >> 16; } void srand(unsigned int start) { /* postavlja početnu vrijednost za rand() */ seed = start; } Slučajni broj generira funkcija rand() iskazom: seed = seed * 1103515245 + 12345; Uočite da se ne vrši operacija mod 232, jer maksimalna veličina unsigned long je 232-1, pa nije potrebno tražiti ostatak dijeljenja s tim brojem. Funkcija rand() vraća vrijednost koja je sadržana u donjih 16 bita (seed>>16), jer se pokazalo da to daje najbolji statistički rezultat (time je maksimalna veličina broja svedena na 32767). Postoje i kvalitetniji algoritmi za generator slučajnih brojeva, ali ovaj način je odabran kao dobar kompromis između kvalitete i brzine generiranja slučajnih brojeva. Početno, pri pokretanju programa, globalna varijabla seed ima vrijednost 1, a bilo bi poželjno da to bude uvijek druga vrijednost. U tu svrhu se koristi funkcija srand(), pomoću koje se početna vrijednost varijable seed postavlja na vrijednost argumenta ove funkcije. Postavlja se pitanje kako osigurati da seed pri pokretanju programa uvijek ima drugačiju vrijednost. Programeri obično koriste činjenicu da će startanje programa biti u nekom jedinstvenom vremenu. Za očitanje vremena na raspolaganju je funkcija time() koja je deklarirana u <time.h>: time_t time(time_t *tp) Tip time_t je typedef za cjelobrojnu vrijednost . U većini implementacija funkcija time() vraća vrijednost koja je jednaka vremenu u sekundama koje je proteklo od 01.10.1970. godine. Ista se vrijednost postavlja i u dereferencirani pokazivač tp, ukoliko on nije NULL. Sada se inicijalizacija generatora slučajnih brojeva može provesti sljedećim iskazom: 157
  • 158.
    srand((unsigned int)time(NULL)); Zadatak: Uvrstiteprethodni iskaz u program karte.c (također, dodajte direktivu #include<time.h>). Pokrenite program nekoliko puta i provjerite da li se pri svakom izvršenju programa dobije drugačija podjela karata. 11.8 Argumenti komandne linije operativnog sustava Funkcija main() također može imati argumente. Njoj argumente prosljeđuje operativni sustav s komandne linije. Primjerice, pri pozivu kompilatora (c:>cl karte.c ) iz komandne linije se kao argument prosljeđuje string koji sadrži ime datoteke koja se kompilira. Opći oblik deklaracije funkcije main(), koja prima argumente s komandne linije, glasi: int main( int argc, char *argv[]) Parametri su: argc sadrži broj argumenata. Prvi argument (indeks nula) je uvijek ime samog programa, dakle broj argumenata je uvijek veći ili jednak 1. argv je niz pokazivača na char, svaki pokazuje na početni znak stringova koji su u komandnoj liniji odvojeni razmakom. Primjer: Programom cmdline.c ispituje se sadržaj komandne linije /* Datoteka: cmdline.c */ #include <stdio.h> int main( int argc, char *argv[]) { int i; printf("Ime programa je: %sn", argv[0]); if (argc > 1) for (i = 1; i < argc; i++) printf("%d. argument: %sn", i, argv[i]); return 0; } Ako se otkuca: c:> cmdline Hello World dobije su ispis: Ime programa: C:CMDLINE.EXE 1. argument: Hello 2. argument: World jer je: argc = 3 argv[0] = "C:CMDLINE.EXE" argv[1] = "Hello" argv[2] = "World" Primjer: Program cmdsum.c računa sumu dva realna broja koji se unose u komandnoj liniji iza imena izvršnog programa. Primjerice, ako se u komandnoj liniji otkuca: 158
  • 159.
    C:>cmdsum 6.5 4e-2 programtreba dati ispis Suma: 6.500000 + 0.040000 = 6.540000 /* Datoteka: cmdsum.c */ #include <stdio.h> #include <stdlib.h> int main( int argc, char *argv[]) { float x,y; if (argc > 2) /* unose se dva argumenta */ { x = atof(argv[1]); y = atof(argv[2]); printf ("Suma: %f + %f = %fn", x, y, x+y); } else printf("Otkucaj: cmdsum broj1 broj2 n"); return 0; } 159
  • 160.
    12 Dinamičko alociranjememorije Naglasci: • slobodna memorija i alociranje memorije • malloc(), calloc(), realloc() i free() • dinamički nizovi • dinamičke matrice • brzi pristup memoriji Kada operativni sustav učita program, koji je formiran C-kompilatorom, on tom programu dodijeli dio radne memorije koji čine tri cjeline: memorija izvršnog kôda programa, memorija za statičke podatke (globalne i statičke varijable te literalne konstante), te memorija nazvana stog (eng. stack) koja se koristi za privremeni smještaj automatskih varijabli (lokalne varijable i formalni argumenti funkcija). Ostatak memorije se naziva slobodna memorija ili "heap". Operativni Korisnički Statički Stog Slobodna memorija program: sustav podaci automatske varijable "heap" strojni kod Slika 12.1 Raspodjela memorije između operativnog sustava i korisničkog programa Nadzor nad korištenjem slobodne memorije vrši operativni sustav. U samom C-jeziku nije implementirana mogućnost direktnog raspolaganja slobodnom memorijom, ali se, uz pomoć pokazivača i funkcija biblioteke: malloc(), calloc(), realloc() i free(), može na indirektan način koristiti slobodna memorija. Postupak kojim se dobiva na upotrebu slobodna memorija naziva se dinamičko alociranje memorije. Pojam alociranje memorije asocira na činjenicu da se dio slobodne memorije dodjeljuje korisničkom programu, dakle apstraktno se mijenja njegova lokacija. Alociranje memorije se vrši tijekom izvršenja programa, pa se kaže da je taj proces dinamički. Postupak kojim se alocirana memorija vraća na raspolaganje operativnom sustavu naziva se oslobađanje ili dealociranje memorije. U ovom poglavlju biti će opisano kako se vrši dinamičko alociranje memorije i kako ta programska tehnika omogućuje efikasno korištenje memorijskih resursa računala. 12.1 Funkcije za dinamičko alociranje memorije Najprije će biti opisane funkcije za dinamičko alociranje memorije. One su deklarirane u datoteci <stdlib.h>. void *malloc(size_t n) void *calloc(size_t n, size_t elsize) Funkcijom malloc() alocira se n bajta slobodne memorije. Ako je alociranje uspješno funkcija vraća pokazivač na tu memoriju, u suprotnom vraća NULL pokazivač. Primjerice, naredbom double *dp = malloc(10 * sizeof(double)); 160
  • 161.
    dobije se pokazivačdp, koji pokazuje na niz od 10 elemenata tipa double. U deklaraciji funkcije malloc() označeno je da ona vraća je void *. To omogućuje da se adresa, koju vraća funkcija malloc(), može pridijelili pokazivaču bilo kojeg tipa. Funkcija calloc(n, elsize) je ekvivalentna malloc(n * elsize), uz dodatni uvjet da calloc() inicijalizira sve bitove alocirane memorije na vrijednost nula. Za dealociranje memorije poziva se funkcija: void free(void *p) Funkcija free() prima kao argument pokazivač p. Uz pretpostavku da p pokazuje na memoriju koja je prethodno alocirana fukcijom malloc(), calloc() ili realloc(), ova funkcija dealocira (ili oslobađa) tu memoriju. Promjenu veličine već alocirane memorije vrši se pozivom funkcije: void *realloc(void *ptr, size_t newsize) Funkcija realloc() vrši promjenu veličine prethodno alocirane memorije, koja je pridijeljena pokazivaču ptr, na veličinu newsize. Funkcija realloc() vraća pokazivač na tu memoriju. Vrijednost toga pokazivača može biti ista kao i vrijednost od ptr, ako memorijski alokator može prilagoditi veličinu zahtijevanog području slobodne memorije veličini newsize. Ukoliko se to ne može ostvariti funkcija realloc() alocira novo područje memorije pa u njega kopira i zatim oslobađa dio memorije na koju pokazuje ptr. Ukoliko se ne može izvršiti alokacija memorije funkcija realloc() vraća NULL. Napomena: poziv realloc(p, 0) je ekvivalentan pozivu free(p), a poziv realloc(0, n) je ekivalentan pozivu malloc(n). Dobra je programerska praksa da se uvijek pri pozivu funkcije za alociranje memorije provjeri da li ona vraća NULL. Ako se to dogodi u većini je slučajeva najbolje odmah prekinuti program. U tu svrhu može se koristiti standardna funkcija exit() kojom se prekida korisnički program, a operativnom se sustavu prosljeđuje argument ove funkcije. Primjerice, p = malloc(n); if(p == NULL) { printf("out of memoryn"); exit(1); } /* … koristi pokazivač p */ Napomena: Prije uvođenja ANSI/ISO standarda prototip za malloc() je kod mnogih kompilatora bio deklariran s: char *malloc(size_t n). Kod ovakvih kompilatora potrebno je izvršiti eksplicitnu pretvorbu tipa vrijednosti koju vraća malloc(), primjerice double *p; p = (double *)malloc(n); Ako se ne izvrši eksplicitna pretvorba tipa kompilator dojavljuje grešku o nekompatibilnosti tipova. Primjer: Dinamičko alociranje niza. 161
  • 162.
    U programu allocarr.calocira se niz od maxsize=5 cjelobrojnih elemenata. Korisnik zatim unosi niz brojeva. Ako broj unesenih brojeva premaši maxsize, tada se povećava alocirana memorija za dodatnih 5 elemenata. Unos se prekida kada korisnk otkuca slovo. Na kraju se ispisuje niz i dealocira memorija. /*Datoteka: allocarr.c*/ #include <stdio.h> #include <stdlib.h> void izlaz(char *string) { printf("%s", string); exit(1); } int main( void ) { int *pa ; /* pokazivač na niz cijelih brojeva */ int maxsize = 5; /* početna maksimalna veličina niza */ int num = 0; /* početno je u nizu 0 elemenata */ int j,data; /* * Početno alociraj memoriju za maxsize = 5 cijelih brojeva. * Koristi funkciju malloc koji vraća pokazivač pa. * Izvrši provjeru NULL vrijednosti pokazivača */ pa = malloc(maxsize * sizeof( int )); if(pa == NULL ) izlaz("Nema slobodne memorije za malloc.n") ; /* * Učitaj proizvoljan broj cijelih brojeva u niz pa[] * Ako broj premaši maxsize: * povećaj allocirani niz za 5 elemenata pomoću funkcije realloc * Kraj unosa je ako se umjesto broja pritisne neko slovo */ while(scanf("%d", &data) == 1) { pa[num++] = data; /* upisi podatak u memoriju */ if(num >= maxsize) /* ako je memorija popunjena */ { /* povećaj alociranu memoriju */ maxsize += 5; /* za dodatnih 5 elemenata */ pa = realloc( pa, maxsize * sizeof(int)); if(pa== NULL) izlaz("Nema slobodne memorije za realloc.n") ; } } /* Ispiši podatke iz dinamički alociranog niza */ for(j = 0; j < num; j ++ ) printf("%d n", pa[j]) ; /* Konačno vrati memoriju na heap koristeći free() */ free( pa ) ; return 0 ; } 162
  • 163.
    Primjer: Pokazana jeimplementacija funkcije char * NumToString(int n), koja pretvara cijeli broj n u string i vraća pokazivač na taj string. char *NumToString( int n) { char buf[100], *ptr; sprintf( buf, "%d", n); ptr = malloc( strlen( buf) + 1); strcpy( ptr, buf); return ptr; } Primjer primjene funkcije NumToString: char *s; s = NumToString(56); printf("%sn", s); free(s); /* !!! oslobodi memoriju kad više ne trebaš s */ Napomena: Ako se u nekoj funkciji alocira memorija i pridijeli pokazivaču koji je lokalna varijabla, nakon izlaza iz funkcije taj pokazivač više ne postoji (zašto?). Alocirana memorija će i dalje postojati ukoliko nije prije završetka funkcije eksplicitno dealocirana funkcijom free(). Može se dogoditi da ta memorija bude "izgubljena" ako je se ne dealocira, ili ako se iz funkcije ne vrati pokazivač na tu memoriju (kao u slučaju funkcije NumToString). Primjer: Funkciji char * strdup(char *s) namjena je da stvori kopiju nekog stringa s, i vrati pokazivač na novoformirani string. char *strdup( char * s) { char *buf, *ptr; int n = strlen(s); /* alociraj memoriju n+1 bajta za kopiju stringa s*/ buf = malloc(n* sizeof(char)+1); /* kopiraj string u tu memoriju */ if(buf != NULL) strcpy( buf, s); /* vrati pokazivač na string */ return buf; } Ova funkcija nije specificirana u standardnoj biblioteci C jezika, ali je implementirana u biblioteci Visual C i gcc kompilatora. 12.2 Kako se vrši alociranje memorije Da bi se bolje shvatio proces alociranja memorije, bit će ilustrirano stanje slobodnje memorije u slučaju kada se vrši višestruko alociranje/dealociranje memorije. Sivom bojom označeno je područje slobodne memorije: 163
  • 164.
    Neka najprije trebaalocirati memoriju za string "Ivo". To se vrši iskazom: char *str1 = malloc(4); Alocirana su 4 bajta, jer pored znakova 'I', 'v' i 'o', treba spremiti i znak '0'. Ako se uspješno izvrši alokacija memorije, vrijedit će prikaz: ? ? ? ? Upitnici označavaju da je sadržaj memorije, na koji str1 pokazuje, nedefiniran. Alocirana će memorija biti inicijalizirana tek kada se izvrši funkcija: strcpy(str1, "Ivo"); Sada alocirana memorija ima definirani sadržaj: I v o - (crtica označava nul-znak) Ako se nadalje izvrši alociranje memorije za string "Ivona", str2 = malloc(6); strcpy(str2, "Ivona"); vrijedi prikaz: I v o - I v o n a - Između dva alocirana bloka nacrtano je manje područje slobodne memorije kako bi se istaklo da memorijski alokator ne slaže alocirane blokove neposredno jedan do drugog. Zapravo, memorijski alokator u alocirani sadržaj ispred svakog bloka upisuje i informacije o tom bloku, primjerice veličinu samog bloka. Realnu situaciju je bolje prikazati na sljedeći način: 4 I v o - 6 I v o n a - str1 str2 Kada korisnik oslobodi memoriju na koju pokazuje str1, naredbom free(str1); stanje slobodne memorije je takovo da postoji dio slobodne memorije ispred stringa "Ivona". 6 I v O n a - 164
  • 165.
    Kaže se daje slobodna memorija fragmentirana. Pri višestrukim pozivima malloc/free može doći do višestruke fragmentacije, čime se "gubi" jedan dio slobodne memorije. Zapamtite: • O stanju slobodne memorije vodi računa posebni proces – memorijski alokator. • Nakon što se alocira memorija pomoću funkcije malloc(), treba je inicijalizirati. • Ako se želi inicijalizirati sadržaj memorije na vrijednost nula, koristi se funkcija calloc(). • Ne smije se pretpostaviti da se sukcesivnim pozivima funkcije malloc() dobiva kontinuirani memorijski blok. 12.3 Alociranje višedimenzionalnih nizova Korištenje pokazivača i funkcija za dinamičko alociranja memorije omogućuje stvaranje dinamičkih struktura podataka koje mogu imati promjenljivu veličinu tijekom izvršenja programa. Nema posebnih pravila za stvaranje dinamičkih struktura podataka. U ovom i u narednim poglavljima bit će pokazani različiti primjeri koji su korisni u programiranju i koji se mogu koristiti kao obrazac za "dinamičko struktuiranje podataka" u C jeziku. Najprije će biti opisano kako se dinamički alociraju višedimenzionalni nizovi i to nizovi stringova i matrice promjenljivih dimenzija. Dinamički nizovi stringova Prije je pokazano da se u C jeziku pokazivači tipa char * mogu tretirati kao stringovi, uz uvjet da pokazuju na niz znakova koji završava s nultim znakom. To omogućuje da se niz stringova deklarira iskazom: #define N 100 char *txt[N]; Ovaj niz stringova je statičan jer se njime može registrirati konačan broj stringova txt[i]. Uočite da je N konstanta. Niz stringova se može realizirati i dinamičkim alociranjem niza koji će sadržavati N pokazivača tipa char *. To se postiže iskazima: int N = 100; /* veličina niza je varijabla */ char **txt; /* txt pokazuje na niz pokazivača */ txt = calloc(N, sizeof(char*)); /* alociraj za N pokazivača */ U oba slučaja txt[i] predstavlja string. Razlika je pak od prethodne definicije niza stringova u tome što se sada veličina niza može mijenjati tijekom izvršenja programa. Početno su svi elementi niza jednaki NULL, što znači da niz nije inicijaliziran. Inicijalizacija niza se postiže tako da se pokazivačima txt[i] pridijeli adresa nekog stringa. Primjerice, nakon sljedećih operacija: txt[0]= strdup("Hello") txt[1]= strdup("World!") txt[2]= strdup("je prvi C program") prva tri elementa niza txt[i] predstavljaju stringove, a ostali elementi niza su i dalje neinicijalizirani. Stanje u memoriji se može ilustrirati slikom 12.2. 165
  • 166.
    Slika 12.2 Prikazkorištenja memorije za dinamički niz stringova Nakon upotrebe, potrebno je osloboditi memoriju koju zauzima niz stringova. To se postiže tako da se najprije dealocira memorija koju zauzimaju stringovi. for(i=0; i<N; i++) { if(txt[i] != NULL) /* oslobodi samo alocirane stringove */ free(txt[i]); } a zatim se dealocira memorija koju zauzima niz pokazivača txt; free(txt); Prikazana struktura je zgodna za unošenje proizvoljnog broja linija teksta, i može biti temeljna struktura u editoru teksta. U ovu strukturu je lako unositi tekst , brisati tekst i povećavati broj linija teksta. Primjer: U programu alloctxt.c korisnik unosi proizvoljan broj linija teksta. Početno se alocira memorija za 5 linija teksta. Ako korisnik unese više linija, tada se pomoću funkcije realloc() povećava potrebna memorija za dodatnih 5 linija. Unos završava kada se otkuca prazna linija. Nakon toga se vrši sortiranje teksta pomoću funkcije qsort(). Na kraju programa ispisuje se sortirani tekst i dealocira memorija. /* Datoteka: alloctxt.c */ #include <stdio.h> #include <stdlib.h> #include <string.h> int CmpString( const void *pstr1, const void *pstr2 ); char *strdup( char * s); void memerror(); int main( void ) { char **txt; /* pokazivač na pokazivač stringa */ int maxsize = 5; /* početna maksimalna veličina niza */ int numstrings = 0; /* početno je u nizu 0 stringova */ char str[256]={"0"}; 166
  • 167.
    int i; /* * Početno alociraj memoriju za maxsize=5 , * Koristi malloc koji vraća pokazivač u txt. * Izvrši provjeru NULL vrijednosti pokazivača */ txt = malloc(maxsize * sizeof(char *)); if(txt == NULL ) memerror() ; /* * Učitaj proizvoljan broj stringova u niz txt[]: * Prethodno alociraj memoriju sa svaki uneseni string * Ako broj stringova premaši maxsize: * povećaj allocirani niz za 5 elemenata pomoću funkcije realloc * Kraj unosa je ako se unese prazna linija. */ while(gets(str) != NULL) { char *s = strdup( str) ; if(s== NULL) memerror(); if(strlen(s)==0) break; txt[numstrings++] = s; if(numstrings >= maxsize) { maxsize += 5; txt = realloc( txt, maxsize * sizeof( int )); if(txt== NULL) memerror(); } } /* sortiraj niz stringova leksikografski */ if(numstrings > 1) qsort((void *)txt, numstrings, sizeof(char*), CmpString); /* Ispiši podatke iz dinamički alociranog niza * i vrati memoriju na heap koristeći free() * za svaki pojedinačni string */ for(i = 0; i < numstrings; i ++ ) { puts(txt[i]) ; free( txt[i] ); } free(txt); return 0 ; } int CmpString( const void *pstr1, const void *pstr2 ) { /* Ova usporedna funkcija se koristi u funkciji qsort() * Prema dogovoru, u usporednu funkciju se šalju pokazivači * na element niza, u ovom slučaju * pokazivac na string, odnosno char **. 167
  • 168.
    * Za usporedbustringova koristi se funkcija strcmp(). * Pošto argument funkcije strcmp mora biti string (char *) * to znači da joj se mora poslati *pstr1 i *pstr2, * odnosno sadržaj argumenata pstr1 i pstr2, * koji su pokazivaci tipa char** */ return strcmp( *(char **) pstr1, *(char **)pstr2 ); } void memerror() { /* funkcija za dojavu pogreške */ printf("%s", "Greska pri alociranju memorije"); exit(1); } Dinamičke matrice Matrica je skup istovrsnih elemenata koji se obilježavaju s dva indeksa - mat[i][j]. Prvi indeks predstavlja redak, a drugi indeks predstavlja stupac matrice. Matrica se može programski realizirati kao dinamička struktura. Primjerice, matrica imena mat, koja će sadržavati elemente tipa double, se sljedećim postupkom: Slika 12.3 Prikaz korištenja memorije za dinamičke matrice 1. Identifikator matrice se deklarira kao pokazivač na pokazivač na double, a dvije varijable koje označavaju broj redaka i stupaca matrice se iniciraju na neku početnu vrijednost. int brojredaka = 5, brojstupaca=10; double **mat; 2. Alocira se memorija za niz pokazivača na retke matrice (mat[i]) mat = malloc( brojredaka * sizeof(double *)); 3. Zatim se alocira memorija za svaki pojedini redak. To zauzeće je određeno brojem stupaca matrice: for(k = 0; k < brojredaka; k++) mat[i] = malloc( brojstupaca * sizeof(double)); 4. Ovime je postupak formiranja matrice završen. Nakon toga se mogu raditi operacije s matricom. Primjerice, iskaz for(i = 0; i < brojredaka; i++) for(j = 0; j < brojstupaca; j++) mat[i][j] = 0; 168
  • 169.
    postavlja sve elementematrice na vrijednost nula. 5. Nakon završetka rada s matricom potrebno je osloboditi alociranu memoriju. To se postiže tako da se najprije dealocira memorija koju zauzimaju elementi matrice; for(k=0; k < brojredaka; k++) free(mat[k]); a zatim se dealocira memorija koju zauzima niz pokazivača retka; free(mat); Iako ovaj postupak izgleda dosta kompliciran, on se često koristi jer se njime omogućuje rad s matricama čije se dimenzije mogu mijenjati tijekom izvršenja programa. Također, pogodno je pisati funkcije s ovakvim matricama, jer se mogu primijeniti na matrice proizvoljnih dimenzija. Primjerice, funkcija: void NulMat(double **mat, int brojredaka, int brojstupaca) { for(int i = 0; i < brojredaka; i++) for(int j = 0; j < brojstupaca; j++) mat[i][j] = 0; } može poslužiti da se postavi vrijednost elemenata bilo koje matrice na vrijednost 0. Ovo nije moguće kod korištenja statičkih nizova je se tada u definiciji funkcije, u parametru koji označava dvodimenzionalni niz, uvijek mora označiti broj kolona. Funkcija PrintMat() može poslužiti za ispis matrice u proizvoljnom formatu: void PrintMat(double **mat, int brojredaka, int brojstupaca, char *format) { int i,j; for( i = 0; i < brojredaka; i++) { for(j = 0; j < brojstupaca; j++) printf (format, mat[i][j]); printf("n") ; } } Sam postupak alociranja i dealociranja matrice se može formalizirati s dvije funkcije double **AlocirajMatricu (int brojredaka,int brojstupaca) { int k; double **mat = malloc( brojredaka * sizeof(double *)); if(mat == NULL) return NULL; for(k = 0; k < brojredaka; k++) mat[k] = malloc( brojstupaca * sizeof(double)); return mat; } void DealocirajMatricu(double **mat, int brojredaka) 169
  • 170.
    { int k; for(k=0; k < brojredaka; k++) free(mat[k]); free(mat); } Primjer: U programu allocmat.c formira se matrica s 3x4 elementa, ispunja slučajnim brojem i ispisuje vrijednost elemenata matrice. Zatim se ta matrica dealocira i ponovo alocira s 2x2 elementa. /* Datoteka: allocmat.c */ #include <stdio.h> #include <stdlib.h> #include <math.h> double **AlocirajMatricu (int brojredaka,int brojstupaca); void DealocirajMatricu(double **mat, int brojredaka); void PrintMat(double **mat, int brojredaka, int brojstupaca, char *format); int main( void ) { int i,j; double **mat; /* matrica */ int rd= 3; /* broj redaka */ int st= 4; /* broj stupaca */ /* formiraj matricu sa 3x4 elementa*/ mat = AlocirajMatricu(rd, st); if(mat == NULL) exit(1); /* ispuni matricu sa slučajnim vrijednostima */ for(i = 0; i < rd; i++) for(j = 0; j < st; j++) mat[i][j] = (double)rand(); /* ispisi vrijednosti */ PrintMat(mat, rd, st, "%12.2lf"); DealocirajMatricu(mat, rd); /* sada formiraj drugu matricu sa 2x2 elementa*/ rd = 2; st = 2; mat = AlocirajMatricu(rd, st); if(mat == NULL) exit(1); for(i = 0; i < rd; i++) for(j = 0; j < st; j++) mat[i][j] = (double)rand(); /* ispisi vrijednosti */ PrintMat(mat, rd, st, "%12.2lf"); DealocirajMatricu(mat, rd); return 0 ; 170
  • 171.
    } /* definiraj potrebne funkcije */ Dobije se ispis: 41.00 18467.00 6334.00 26500.00 19169.00 15724.00 11478.00 29358.00 26962.00 24464.00 5705.00 28145.00 23281.00 16827.00 9961.00 491.00 12.4 Standardne funkcije za brzi pristup memoriji U standardnoj biblioteci je definirano nekoliko funkcija koje su optimirane za brzi pristup memoriji. void *memcpy(void *d, const void *s, size_t n) Kopira n znakova s adrese s na adresu d, i vraća d. void *memmove(void *d, const void *s, size_t n) Ima isto djelovanje kao memcpy, osim što vrijedi i za preklapajuća memorijska područja. int memcmp(const void *s, const void *t, size_t n) uspoređuje prvih n znakova s i t; vraća rezultat kao strcmp. void *memchr(const void *m, int c, size_t n) vraća pokazivač na prvu pojavu znaka c u memorijskom području m, ili NULL ako znak nije prisutan među prvih n znakova. void *memset(void *m, int c, size_t n) postavlja znak c u prvih n bajta od m , vraća m. Primjer: prethodnu funkciju NulMat() može se napisati pomoću memset() funkcije: void NulMat(double **mat, int brojredaka, int brojstupaca) { for(int i = 0; i < brojredaka; i++) memset(mat[i], 0, brojstupaca*sizeof(double) } 171
  • 172.
    13 Korisnički definiranestrukture podataka Naglasci: • korisnički definirane strukture podataka • članovi strukture • prijenos struktura u funkcije • strukture i funkcije za očitanje vremena • unija podataka • bit polja • pobrojani tipovi U C-jeziku je implementiran mehanizam za definiranje korisničkih tipova podataka. To su: 1. Struktura - tip koji označava uređen skup varijabli, koje mogu biti različitog tipa 2. Unija - tip kojim se jednom memorijskom objektu pridjeljuje skup tipova 3. Pobrojani tip (ili enumeracija) - tip definiran skupom imenovanih cjelobrojnih konstanti 13.1 Struktura (struct) Struktura je skup od jedne ili više varijabli, koje mogu biti različitog tipa, grupiranih zajedno pod jednim imenom radi lakšeg rukovanja. Varijable koje čine strukturu nazivaju se članovi ili polja strukture. Član strukture može biti bilo koji tip koji je dozvoljen u C jeziku (prosti tip, pokazivački tip, niz ili prethodno definirani korisnički tip). Za definiranje strukture koristi se ključna riječ struct. Tako nastaje složeni strukturni tip podataka - struktura - pomoću koje se mogu deklarirati varijable odnosno memorijski objekti strukturnog tipa. U radu sa strukturnim tipovima potrebno je znati: 1. Kako se deklarira struktura i njezini članovi, 2. Kako se deklariraju varijable strukturnog tipa, 3. Kako se operira sa strukturnim varijablama, 4. Kako se može izvršiti inicijaliziranje početne vrijednosti varijable strukturnog tipa. Strukturni tip se definira prema sljedećem sintaktičkom pravilu: struct oznaka_strukture { tip1 oznaka_člana1; tip2 oznaka_člana2; .... }; 172
  • 173.
    struct oznaka_strukture lista_varijabli; oznaka_varijable.oznaka_člana Primjerice, zapisom struct _tocka { int x; int y; }; deklarirana je struktura imena _tocka, koja ima dva člana: x i y. Pomoću ove strukture zgodno je deklarirati varijable koje će označavati položaj točke u pravokutnom koordinatnom sustavu. Na slici 13.1 prikazan je pravokutnik kojeg određuju dvije točke T1 i T2. Deklarira ih se iskazom struct _tocka T1, T2; a vrijednost koordinata se postavlja u sljedećoj sekvenci: T1.x = 2; T1.y = 1; T2.x = 6; T2.y = 4; Slika13. 1. Označavanje koordinata točke i pravokutnika Ukoliko se želi izračunati površinu pravokutnika može se pisati: int povrsina; ..... povrsina = (T2.x – T1.x)*(T2.y-T2.x) ....... Na ovaj način je dobiven programski zapis kojim se poboljšava apstrakcija. Dalje se razina apstrakcije može povećati na način da se deklarira struktura _pravokutnik koja ima dva člana koji predstavljaju točke dijagonala: struct _pravokutnik 173
  • 174.
    { struct _tocka p1; struct _tocka p2; } Dakle, članovi strukture mogu biti prethodno definirani strukturni tipovi. Sada se prijašnji programski segment može zapisati sljedećom sekvencom: int povrsina; struct _pravokutnik pravokutnik; ..... pravokutnik.p1.x = 2; pravokutnik.p1.y = 1; pravokutnik.p2.x = 6; pravokutnik.p2.y = 4; ...... povrsina = ( pravokutnik.p2.x – pravokutnik.p1.x) *( pravokutnik.p2.y - pravokutnik.p2.x) Uočite da se za pristup članovima strukture _tocka, koja je član strukture _pravokutnik, pristupa tako da se dva puta koristi točka-operator. Potrebno je navesti još neka sintaktička pravila za rad sa strukturama. 1. Može se istovremeno izvršiti deklariranje strukturnih varijabli i same strukture, primjerice struct _tocka {int x; int y;} T1,T2; Ako kasnije neće biti potrebno deklarirati nove varijable ovog tipa, može se izostaviti oznaka strukture, tj. struct {int x; int y;} T1,T2; 2. Pomoću typedef može se definirati sinonim za strukturu, primjerice typedef struct tocka _tocka; typedef struct _pravokutnik { _tocka p1; _tocka p2 } pravokutnik_t; Sada _tocka i pravokutnik_t predstavljaju oznake tipova pomoću kojih se može deklarirati varijable (bez navođenja ključne riječi struct), primjerice _tocka T1, T2; pravokutnik_t pravokutnik; 3. Inicijaliziranje početnih vrijednosti se može izvršiti na sličan način kao kod nizova, tako da se pripadajuća lista početnih vrijednosti navede unutar vitičastih zagrada. struct _tocka T1 = {2,1}, T2 = {6,4}; struct _pravokutnik pravokutnik = { {2,1}, {6,4} }; 174
  • 175.
    Ako se strukturainicijalizira djelomično, elementi koji nisu inicijalizirani se postavljaju na vrijednost nula. Strukture i nizovi Elementi strukture mogu biti nizovi. Također, može se formirati nizove struktura. Primjerice, struktura za vođenje evidencije o ocjenama pojedinog studenta može imati oblik: typedef struct _studentinfo { char ime[30]; int ocjena; } studentinfo_t; Uz pretpostavku da nastavu pohađa 30 studenata, za vođenje evidencije o studentima može se definirati niz: studentinfo_t student[30]; Pojedinom članu strukture, koji je element niza, pristupa se na način da se točka operator piše iza uglatih zagrada, primjerice: student[0].ocjena = 2; student[1].ocjena = 5; Struktura kao argument funkcije Strukture se, za razliku od nizova, tretiraju kao "tipovi prve klase". To znači da se može pridijeliti vrijednost jedne strukturne varijable drugoj, da se vrijednost strukturne varijable može prenositi kao argument funkcije i da funkcija može vratiti vrijednost strukturne varijable. Program struct.c demonstrira upotrebu strukturnih varijabli u argumenatima funkcije. /* Datoteka: struct.c */ /* Primjer definiraja strukture i koristenja */ /* strukture kao argumenta funkcije */ #include <stdio.h> #include <string.h> typedef struct _student { char ime[30]; int ocjena; }studentinfo_t; void display( studentinfo_t st ); int main() { studentinfo_t student[30]; int i=0; strcpy( student[0].ime, "Marko Matic" ); student[0].ocjena = 4; strcpy( student[1].ime, "Marko Katic" ); student[1].ocjena = 2; strcpy( student[2].ime, "Ivo Runjanin" ); student[2].ocjena = 1; 175
  • 176.
    strcpy( student[3].ime, ""); student[3].ocjena = 0; while (student[i].ocjena != 0 ) display( student[i++]); return 0; } void display(studentinfo_t st) { printf( "Ime: %s ", st.ime ); printf( "tocjena: %dn", st.ocjena ); } Rezultat izvršenja programa je: Ime: Marko Matic ocjena: 4 Ime: Marko Katic ocjena: 2 Ime: Ivo Runjanin ocjena: 1 U programu se prvo inicijalizira prva tri elementa niza koji su tipa studentinfo_t. Četvrtom elementu se vrijednost člana ocjena postavlja na vrijednost nula. Ova nulta ocjena će kasnije služiti kao oznaka elementa niza, do kojeg su uneseni potpuni podaci. Ispis se vrši pomoću funkcije display(studentinfo_t). U prethodnom programu strukturna varijabla se prenosi u funkciju po vrijednosti. Ovaj način prijenosa strukture u funkciju nije preporučljiv jer se za prijenos po vrijednosti na stog mora kopirati cijeli sadržaj strukture. To zahtijeva veliku količinu memorije i procesorskog vremena (u ovom slučaju veličina strukture je 34 bajta). Mnogo bolji način prijenosa strukture u funkciju je da se koristi pokazivač na strukturu, a da se zatim u funkciji elementima strukture pristupa indirekcijom. Pokazivači na strukturne tipove i operator -> Neka su deklarirani varijabla i pokazivač tipa studentinfo_t: studentinfo_t St, *pSt; i neka je varijabla St inicijalizirana slijedećim iskazom: St.ime = strcpy("Ivo Boban"); St.ocjena = 1; Ako se pokazivač pSt inicijalizira na adresu varijable St, tj. pSt = &St; tada se može pristupiti varijabli St i pomoću dereferenciranog pokazivača. Primjerice, ako se želi promijeniti ocjenu na vrijednost 2 i ispisati novo stanje, može se koristiti iskaze: (*pSt).ocjena = 2; printf("Ime: %s, ocjena: %d", (*pSt).ime, (*pSt).ocjena) Uočite da se zbog primjene točka operatora dereferencirani pokazivač mora napisati u zagradama, jer bi inače točka operator imao prioritet. Ovakav način označavanja indirekcije je dosta kompliciran, stoga je u C jeziku definiran operator ->, pomoću kojeg se prethodni iskazi zapisuju u obliku: 176
  • 177.
    pSt->ocjena = 2; printf("Ime: %s, ocjena: %d", pSt->ime, pSt->ocjena) Vrijedi ekvivalentni zapis (*pStudent).ocjena ⇔ pStudent->ocjena Operator -> označava pristup elementu strukture pomoću pokazivača i može ga se zvati operatorom indirekcije pokazivača strukture. Primjer: U programu structp.c pokazano je kako se prethodni program može napisati mnogo efikasnije korištenjem pokazivača. U ovom primjeru pokazivači će se koristiti i kao članove strukture i pomoću njih će se vršiti prijenos strukturne varijable u funkciju. /* Datoteka: structp.c * Primjer definiraja strukture pomocu pokazivačkih članova i * korištenje pokazivača strukture kao argumenta funkcije */ #include <stdio.h> #include <stdlib.h> /* def. NULL */ typedef struct _student { char *ime; int ocjena; }studentinfo_t; void display( studentinfo_t *pSt ); int main() { int i=0; studentinfo_t *p; studentinfo_t student[30] = { { "Marko Matic", 4 }, { "Marko Katic", 2 }, { "Ivo Runjanin", 1 }, { NULL, 0} }; p=&student[0]; while (p->ime != NULL ) display( p++); return 0; } void display(studentinfo_t *pS) { printf( "Ime: %s ", pS->ime ); printf( "tocjena: %dn", pS->ocjena ); } 177
  • 178.
    Prvo što setreba uočiti u ovom programu je način kako je deklarirana struktura _student . U njoj sada nije rezerviran fiksni broj mjesta za član ime, već ime sada predstavlja pokazivač na char (string). typedef struct _student { char *ime; int ocjena; }studentinfo_t; Niz student je inicijaliziran pri samoj deklaraciji sa: studentinfo_t student[30] = { { "Marko Matic", 4 }, { "Marko Katic", 2 }, { "Ivo Runjanin", 1 }, { NULL, 0} }; Ovime se postiže ušteda memorije jer se za string ime rezervira točno onoliko mjesta koliko je upisano inicijalizacijom (plus nula!). Pomoćnim pokazivačem p, kojeg se početno inicira na adresu nultog elementa niza student, pretražuje se niz sve dok se ne dođe do elementa kojem član ime ima vrijednost NULL pokazivača. Dok to nije ispunjeno vrši se ispis sadržaja niza pomoću funkcije display(), kojoj se kao argument prenosi pokazivač na tip studentinfo_t. Unutar funkcije display() elementima strukture se pristupa indirekcijom pokazivača strukture (->). Na kraju, da se zaključiti, da prijenos strukture u funkciju u pravilu treba vršiti pomoću pokazivača. Isto vrijedi za slučaj kada funkcija vraća vrijednost strukture. I u tom slučaju je bolje raditi s pokazivačima na strukturu, jer se štedi memorija i vrijeme izvršenja programa. Prenošenje strukture po vrijednosti se može tolerirati samo u slučajevima kada struktura ima malo zauzeće memorije. Memorijska slika strukturnih tipova U tablici 13.1 dana je usporedba karakteristika nizova i struktura. Važno je uočiti da članovi struktura u memoriji nisu nužno poredani neposredno jedan do drugog. Razlog tome je činjenica da je dozvoljeno da se članovi strukture smještaju u memoriju na način za koji se ocjenjuje da će dati najbrže izvršenje programa (većina procesora brže pristupa parnim adresama, nego neparnim adresama). karakteristika niz struktura strukturalni sadržaj kolekcija elemenata istog tipa kolekcija imenovanih članova koji mogu biti različitog tipa pristup elementima elementima se pristupa članovima se pristupa pomoću imena složenog tipa pomoću indeksa niza raspored elemenata u u nizu jedan do drugog u nizu, ali ne nužno jedan do drugog memoriji računala Tablica 13.1. Usporedba nizova i struktura Kod kompilatora Microsoft Visual C može se posebnom direktivom (#pragma pack(1)) zadati da se članovi strukture "pakiraju" u memoriju jedan do drugog. To pokazuje program pack.c. 178
  • 179.
    /* Datoteka: pack.c***********************************/ /* Primjer memorijski pakirane i nepakirane strukture */ #include <stdio.h> struct s1 { char c; short i; double d; } v1; #pragma pack(1) /* forsiraj pakiranu strukturu */ struct s2 { char c; short i; double d; } v2; int main( void) { printf("Minimalno zauzece memorije: %d bajtan", sizeof(char)+sizeof(short)+ sizeof(double)); printf("Zauzece memorije nepakiranom strukturom: %d bajtan", sizeof(struct s1)); printf("Zauzece memorije pakiranom strukturom: %d bajtan", sizeof(struct s2)); printf("nAdresa elementa : nepakirano pakiranon"); printf("Adresa od c : %p %pn", &v1.c, &v2.c); printf("Adresa od i : %p %pn", &v1.i, &v2.i); printf("Adresa od d : %p %pn", &v1.d, &v2.d); return 0; } Nakon izvršenja programa dobije se poruka: Minimalno zauzece memorije: 11 bajta Zauzece memorije nepakiranom strukturom: 16 bajta Zauzece memorije pakiranom strukturom: 11 bajta Adresa elementa : nepakirano pakirano Adresa od c : 00406B90 00406BA0 Adresa od i : 00406B92 00406BA1 Adresa od d : 00406B98 00406BA3 Uočite da je zauzeće memorije pakirane strukture minimalno i jednako zbroju veličine zauzeća memorije pojedinog člana, dok kod nepakirane strukture ostaje neiskorišten jedan bajt između char i short, te dva bajta između short i double. Ne smije se pretpostaviti veličina zauzeća memorije na temelju zauzeća memorije pojedinog člana strukture. Za određivanje veličine memorije koju zauzima strukturni tip uvijek treba koristiti operator sizeof. 179
  • 180.
    13.2 Union –zajednički memorijski objekt za različite tipova podataka S ključnom riječi union definira se korisnički tip podataka pomoću kojeg se nekom memorijskom objektu može pristupiti s različitim tipskim pristupom. Deklaracija unije sliči deklaraciji strukture, primjerice, union skalar { char c; int i; double d; }; međutim, njeno značenje je bitno drukčije. Ako se s unijom skalar deklarira objekt imena obj, tj. union skalar obj; tom se objektu može pristupiti na više načina, jer obj može sadržavati vrijednost tipa char, int ili double. Dozvoljeno je pisati: obj.c = 'A'; obj.i = 1237; obj.d = 457.87 Sve ove vrijednosti se upisuju u istu memorijsku lokaciju. Veličina zauzeća memorije je određena elementom unije koji zauzima najveći prostor memorije. U programiranju se često unija koristi unutar neke strukture kojoj posebni element služi za označavanje tipa vrijednosti koji trenutno sadrži unija. Primjerice, #define char_type 0 #define int_type 1 #define double_type 2 struct variant { int var_type; /* oznaka tipa vrijednosti unije skalar */ union skalar var; }; struct variant obj; /* pored vrijednosti, zabilježimo i tip vrijednosti*/ obj.var.c = 'A'; obj.var_type = char_type; obj.var.i =1237; obj.var_type = int_type; obj.var.d =457.87 obj.var_type = double_type; .......... /* uniju koristimo tako da se najprije provjeri tip vrijednosti */ switch(obj.val_type) { case char_type: printf("%c", obj.var.c); break; case int_type: printf("%d", obj.var.i); break; case double_type: printf("%f", obj.var.d); break; 180
  • 181.
    } 13.3 Bit-polja Unutar strukture ili unije može se specificirati veličinu cjelobrojnih članova u bitovima . To se izvodi tako da se iza člana strukture navede dvotočka i broj bitova koliko taj član zauzima. Primjerice struct bitfield1 { int i3b : 3; unsigned int i1b : 1; signed int i7b : 7; }; U ovoj deklaraciji je definirano da u strukturi bitfield1 član i3b je cijeli broj od 3-bita, član i1b je 1-bitni kardinalni broj , a član i7b je 7-bitni cijeli broj. (uočite da 1-bitni član može imati samo dvije vrijednosti 0 i 1) Ovakve strukture se koriste u cilju uštede memorijskog prostora, posebno u slučaju kad se većina članova tretira kao logička vrijednost. Članovima strukture se pristupa kao da su normalni cijeli brojevi, a kompilator vodi računa o tome da se maskiraju nepostojeći bitovi. struct bitfield1 a,b; a.i3b=3; b.i7b=65; Manipuliranje s ovakvim strukturama je potpuno pod kontrolom kompilatora. Zbog toga se ne može koristiti pokazivače na članove strukture jer oni nisu direktno adresibilni. Iz istog razloga nije moguće koristiti nizove bitnih-polja. Kada se želi kontrolirati kako se slažu bit-polja unutar jedne riječi, na raspolaganju su dva mehanizma. Prvi je da se umetnu bezimeni članovi koji će predstavljati "prazne bitove". Drugi je mehanizam da veličina bezimenog polja može biti 0. To je poruka kompilatoru da se na tom mjestu završi pakiranje u jednu riječ, i da se od tog mjesta članovi strukture pakiraju u novu riječ. Primjerice struct bitfield2 { int i3b : 3; unsigned int i1b : 1; signed int i7b : 7; int : 2; int i2b: 2; int : 0; int i4b : 4, i5b : 5; }; opisuje strukturu koja se pakira u dvije riječi. Prva sadrži redom 3-, 1-, i 7-bitna polja, zatim 2- bitnu prazninu, te 2-bitno polje i2b, a druga riječ sadrži 4-bitna i 5-bitna polja i4b i i5b. 181
  • 182.
    13.4 Pobrojanji tip(enum) Treća klasa korisnički definiranih tipova C jezika je tzv. pobrojani tip (eng. enumeration). Deklarira se pomoću ključne riječi enum. Služi definiranju integralnog cjelobrojnog tipa kojemu se skup vrijednosti označava simboličkim (imenovanim ) konstantama u sljedećoj notaciji: pobrojani_tip: enum ime_tipa { lista_definicije_ konstanti }; definicija_ konstanti: ime ime = konstanta . Primjerice, deklaracijom enum dani_t {Nedjelja, Ponedjeljak, Utorak, Srijeda, Cetvrtak, Petak, Subota}; definira se korisnički pobrojani tip dani_t kojem se vrijednost označava simboličkim konstantama: Nedjelja, Ponedjeljak, Utorak, Srijeda, Cetvrtak, Petak, Subota. Pri kompiliranju programa ovim se konstantama pridjeljuje numerička vrijednost prema pravilu da prvo ime u listi ima numeričku vrijednost 0, drugo ime ima vrijednost 1, treće ime ima vrijednost 2 itd. Pomoću pobrojanog tipa mogu se deklarirati varijable, prema pravilu deklaracija_varijable: enum ime_tipa lista_varijabli; primjerice, enum dani_t danas, sutra; ....... danas = Srijeda; sutra = Cetvrtak; ........ Vrijednost se neke ili svih simboličkih konstanti može inicijalizirati već pri samoj deklaraciji pobrojanog tipa. Primjerice, enum karte_t {AS = 1, JACK = 11, DAMA, KRALJ}; Neinicijalizirane konstante imaju vrijednost za jedan veću od vrijednosti prethodne konstante, tako DAMA ima vrijednost 12, a KRALJ ima vrijednost 13. Pobrojane tipove ne treba smatrati posebno “čvrstim” tipovima jer ih se može tretirati ravnopravno s cjelobrojnim tipom. Primjerice, dozvoljeno je pisati: int i = Subota; enum karte_t c = 10; c++; /* c postaje jack (11) */ Ponekad se enum koristi samo za definiranje simboličkih konstanti (poput #define direktive). Umjesto korištenja leksičkih direktiva: #define ONE 1 #define TWO 2 #define THREE 3 može se koristiti enum deklaracija: 182
  • 183.
    enum threenum {ONE=1,TWO, THREE}; 13.5 Strukture i funkcije za očitanje vremena U datoteci <time.h> definirano je nekoliko funkcija i struktura imena tm, za očitanje i manipuliranje vremena i datuma. Dosada je za očitanje vremena korištena funkcija time_t time(time_t *tp); koja vraća vrijednost time_t tipa, tj. kardinalni broj koji predstavlja trenutno vrijeme (obično je to broj sekundi od 1.1.1970.). Parametar tp, ako nije NULL, također prihvaća trenutno vrijeme u *tp. Da bi se olakšalo pretvorbu ovog vremena u stvarno vrijeme i datum, definirana je struktura tm sa sljedećim članovima: struct tm /* opisuje vrijeme i datum */ { int tm_sec; /* sekunde 0..61 */ int tm_min; /* minute 0..59 */ int tm_hour; /* sat 0..23 */ int tm_mday; /* dan 1..31 */ int tm_mon; /* mjesec 0..11 */ int tm_year; /* broj godina nakon 1900 */ int tm_wday; /* dan u sedmici 0..6 */ int tm_yday; /* dan u godini 0..365 */ int tm_isdst; /* da li je dan promjene sata 0..1 */ }; Napomena: ako dan promjene sata nije implementiran tada tm_isdst ima negativnu vrijednost. Broj sekundi može biti veći od 59 u slučaju prestupnog vremena. Mjeseci su kodiranu tako da 0 označava siječanj, 1 veljaču itd. Dani u sedmici su kodirani tako da 0 označava nedjelju, 1 ponedjeljak itd. Stvarna godina se dobije tako da se članu tm_year doda vrijednost 1900 (primjerice u godini 2002. godini član tm_year sadrži vrijednost 102). localtime, gmtime Pretvorbu vremena iz formata time_t u struct tm vrši se funkcijom localtime(), kada se želi dobiti lokalno vrijeme, ili funkcijom gmtime() za dobiti univerzalno vrijeme u nultom meridijanu. struct tm *localtime(const time_t *t); struct tm *gmtime(const time_t *t); Obje funkcije primaju adresu varijable koja sadrži vrijeme u formatu time_t, a vraćaju pokazivač na statičku strukturu tipa tm (sadržaj se obnavlja pri svakom pozivu ovih funkcija) . ctime, asctime Ako se želi dobiti zapis vremena u obliku stringa, mogu se koristiti funkcije char *ctime(const time_t *t); char *asctime(const struct tm *tp); 183
  • 184.
    Funkcija ctime() zaargument koristi adresu varijable koja sadrži vrijeme u formatu time_t, a funkcija asctime()za argument koristi pokazivač na strukturu tm. Obje funkcije vraćaju pokazivač statičkog stringa koji sadrži zapis vremena u standardnom formatu. Primjerice, sekvenca naredbi time_t t = time(NULL); char *s = ctime(&t); puts(s); generira ispis: Sat May 11 14:21:20 2002 Uočite da je rezultat poziva ctime(&t) ekvivalentan pozivu asctime(localtime(&t)) . Standardna verzija je prilagođena američkim standardima. Ako se želi napisati vrijeme u formatu 11.05.2002 14:21 tada se može koristiti sljedeće iskaze: /* ispisuje datum i vrijeme u formatu 11.05.2002 14:21 */ time_t t = time(NULL); struct tm *p = localtime(&t); printf("%.2d.%.2d.%.2d %2d:%.2dn", p->tm_mday, p->tm_mon + 1, p->tm_year +1900, p->tm_hour, p->tm_min); strftime Funkcija strftime() se koristi za formatirani ispis vremena. Format se zadaje kao kod printf() funkcije. Prototip funkcije strftime()glasi: size_t strftime(char *buf, size_t bufsize, const char *fmt, const struct tm *tp); Prvi argument je string str u koji se vrši formatirani zapis. Drugi argument (bufsize) ograničava broj znakova stringa. Treći parametar je string u kojem se zapisuje format ispisa nizom specifikatora oblika %x (kao kod printf() funkcije). Posljednji argument je pokazivač strukture tm. Funkcija vraća broj znakova u stringu ili 0 ako nije moguće generirati formatirani string. Specifikatori formata su %a kratica od tri slova za ime dana u sedmici (eng. Sun, Mon, Tue,..) %A puno ime dana u sedmici (eng...) %b kratica od tri slova za ime mjeseca (eng. Jan, Feb, Mar,...) %B puno ime mjeseca (eng....) %c kompletni zapis vremena i datuma %d dan u mjesecu (1..31) %H sat u formatu (1..24) %I sat u formatu (1..12) %j dan u godini (1..365) %m mjesec u godini (1..12) %M minute %p AM/PM (eng.) string koji označava jutro ili popodne %S sekunde %U broj za sedmicu u godini (1..52) - 1 određen prvom nedjeljom 184
  • 185.
    %w broj za dan u sedmici (0-nedjelja) %W broj za sedmicu u godini (1..52) - 1 određen prvim ponedjeljkom %x kompletni zapis datuma %X kompletni zapis vremena %y zadnje dvije znamenke godine %Y godina u formatu s 4 znamenke %Z ime vremenske zone (ako postoji ) %% znak % Primjer: U programu vrijeme.c demonstrira se korištenje funkcija za datum i vrijeme. /* Datoteka: vrijeme.c */ /* Prikazuje datum i vrijeme u tri formata*/ #include <stdio.h> #include <time.h> int main() { time_t vrijeme = time(NULL); struct tm *ptr; char datum_str[20]; /* ispisuje datum i vrijeme u standardnom formatu */ puts(ctime(&vrijeme)); /* ispisuje datum i vrijeme pomoću strftime funkcije */ strftime(datum_str, sizeof(datum_str), "%d.%m.%y %H:%Mn", localtime(&vrijeme)); puts(datum_str); /* ispisuje datum i vrijeme u proizvoljnom formatu */ ptr = localtime(&vrijeme); printf("%.2d.%.2d.%.2d %2d:%.2dn", ptr->tm_mday, ptr->tm_mon+1, ptr->tm_year +1900, ptr->tm_hour, ptr->tm_min); return 0; } Dobije se ispis: Mon May 13 20:13:06 2002 13.05.02 20:13 13.05.2002 20:13 Za obradu vremena se koriste i sljedeće funkcije: mktime time_t mktime(struct tm *tp) Funkcija mktime() pretvara zapisa iz strukture tm u time_t format. Korisna je u tzv. kalendarskim proračunima. Kada je potrebno dodati nekom datumu n dana, tada se može upisati 185
  • 186.
    datum u tmstrukturu, povećati član tm_mday za n, zatim pozivom mktime() se dobije time_t vrijednost koja odgovara novom datumu. difftime double difftime(time_t t1, time_t t2) Funkcija difftime() vraća realnu vrijednost koja je jednaka razlici vremena t1 i t1 u sekundama. clock clock_t clock(void); Funkcija clock() služi za preciznije mjerenje vremena nego je to moguće sa prethodnim funkcijama. Ona vraća vrijednost procesorskog mjerača vremena, koji starta na početku programa, u jedinicama koje su znatno manje od sekunde (nekoliko milisekundi). Koliko je tih jedinica u jednoj sekundi određeno je konstantom CLOCKS_PER_SEC. To znači da izraz: (double)clock()/CLOCKS_PER_SEC daje vrijednost koja je jednaka vremenu (u sekundama) od startanja programa. Primjer: U programu brzina.c ispituje se vremenska rezolucija funkcije clock(), tj. minimalno vrijeme koje se njome može mjeriti. Također, pomoću funkcije clock() mjeri se koliko je potrebno vremena za izvršenje sinusne funkcije. /* Datoteka: brzina.c * Program određuje vremensku rezoluciju funkcije clock() * i mjeri brinu izvršenja sinus funkcije */ #include <stdio.h> #include <math.h> #include <time.h> int main() { double start, stop; double n ; double rezolucija; start =(double)clock()/CLOCKS_PER_SEC; do { stop=(double)clock()/CLOCKS_PER_SEC; } while (stop == start); rezolucija = stop-start; printf("Rezolucija CLOCK-a je %g sekundin" , rezolucija); start =(double)clock()/CLOCKS_PER_SEC; stop = start + 10*rezolucija; do { n += 1.0; sin(n); 186
  • 187.
    } while (stop > (double)clock()/CLOCKS_PER_SEC); printf("Funkcija sin se izvršava %g sekundin" , 10*rezolucija/n); return 0; } Dobije se ispis: Rezolucija CLOCK-a je 0.015 sekundi Funkcija sin se izvrsava u 2.3543e-007 sekundi Rezolucija funkcije clock() je 15 ms. U drugoj petlji, u kojoj se računa funkcija sin(), odabrano je da se ona ponavlja za vrijeme koje je 10 puta veće od rezolucije clock()-a. Na taj način mjerenje vremena izvršenja sinus funkcije vrši se s greškom koja je manja ili jednaka 10%. 187
  • 188.
    14 Leksički pretprocesor Naglasci: • leksičke direktive • makro-supstitucije • leksički string-operatori • direktive za uvjetno kompiliranje Pojam "pretprocesor'' se koristi za oznaku prve faze obrade izvornog koda. Kod nekih C kompilatora pretprocesor je izveden kao zasebni program. Pretprocesor ne analizira sintaksu zapisanog koda, već koristeći leksičke direktive, vrši leksičku obradu izvornog kôda na tri načina: 1. Umeće u izvorni kôd datoteke koje su navedene u direktivi #include 2. Vrši supstituciju teksta prema direktivi #define 3. Vrši selekciju kôda, koji će se kompilirati, pomoću direktiva: #if, #elif, #else i #endif 14.1 Direktiva #include Koriste se dvije varijante direktive #include: #include <ime_datoteke> #include "ime_datoteke" pomoću kojih se vrši umetanje neke datoteke u izvorni kôd, i to na mjestu gdje ja zapisana direktiva. Ako je ime_datoteke zapisano unutar navodnih znakova, traženje datoteke počinje u direktoriju gdje se nalazi izvorni kod, a ako nije tamo pronađena, ili ako je ime zapisano unutar < >, traženje datoteke se vrši u direktoriju u kojem su smještene datoteke s deklaracijama standardnih funkcija. Ime datoteke može sadržavati potpuni ili djelomični opis staze do datoteke. Zapis ovisi o pravilima operativnog sustava. Primjerice, na Unix-u može biti oblik #include "/usr/src/include/classZ.h" #include "../include/classZ.h" ili na Windowsima #include "C:srcincludeclassZ.h" #include "..includeclassZ.h" 14.2 Direktiva #define za makro-supstitucije Leksička supstitucija teksta se definira pomoću direktive #define na dva načina. Prvi način zapisa, koji se vrši prema pravilu: 188
  • 189.
    #define identifikator supstitucijski-tekst jeveć korišten za definiranje simboličkih konstante ili dijelova izvornog koda, primjerice #define EPSILON 1.0e-6 #define PRINT_LINE printf("----------n"); #define LONG_SUBSTITION if(a>b){ printf("a>b"); } else { prinf("a<=b"); } Značenje ove direktive je da se u izvornom kodu, na mjestima gdje se je identifikator, umetne supstitucijski-tekst. Supstitucijski tekst se zapisuje u jednoj ili više linija. Znak nove linije označava kraj zapisa, ali ako je posljednji znak obrnuta kosa crta tada supstitucijskom tekstu pripada i zapis u narednoj liniji. Direktiva za supstituciju teksta se često naziva makro supstitucija ili još kraće – makro. Drugi način zapisa direktive #define omogućuje da se uz identifikator navede jedan ili više makro argumenata, prema pravilu: #define identifikator(argument, ... , argument) supstitucijski-tekst-s-argumentima tako da supstitucijski tekst može biti različit za različita pozivanja makroa. Primjerice, makroi #define KVADRAT(x) x*x #define POSTOTAK(x,y) x*100.0/y se u izvornom kodu, ovisno o argumentima, supstituiraju na sljedeći način: poziv makroa rezultira kodom y = KVADRAT(z); y = z*z; y = POSTOTAK(a,b); y = a*100.0/b; y = KVADRAT(a+b); y = a+b*a+b; y = POSTOTAK(a+b,c+d); y = a+b*100.0/c+d; Dva zadnja primjera pokazuju najčešću grešku kod primjene makroa. Dobiveni kod ne odgovara namjeri da se dobije kvadrat vrijednosti (a/b) jer se zbog prioriteta operatora izraz (a+b*a+b) tretira kao (a+(b*a)+b), a ne kako je bila intencija programera, tj. kao ((a+b)*(a+b)). Zbog toga se preporučuje da se argumenti makroa u supstitucijskom tekstu uvijek pišu u zagradama, primjerice: #define KVADRAT(x) ((x)*(x)) #define POSTOTAK(x,y) ((x)*100.0/(y)) Tada se ekspanzija makroa uvijek vrši s istim značajem, primjerice poziv makroa rezultira kodom Y = KVADRAT(a+b) y = ((a+b)*(a+b)) Y = POSTOTAK(a+b,c+d) y = ((a+b)*100.0/(c+d)) 189
  • 190.
    U definiciji makronaredbe prvi argument mora biti napisan neposredno iza zagrada, bez razmaka. Makro argumente u supstitucijskom tekstu treba uvijek pisati unutar zagrada. Time se osigurava da će se ekspanzija makroa izvršiti s istim efektom za bilo koji oblik argumenata. Također, poželjno je i da se cijeli supstitucijski tekst zapiše u zagradama. Poziv makroa izgleda kao poziv funkcije, ali nema značaj poziva funkcije (ne vrši se prijenos parametara funkcije), već se radi o supstituciji teksta prije procesa kompiliranja. Simboli unutar supstitucijskog teksta mogu biti prethodno definirani makroi, primjerice #define JEDAN 1 #define DVA (JEDAN +1) #define TRI (DVA +1) .... Ekspanzija koju pretprocesor radi s izrazom JEDAN+DVA+TRI je 1+(1+1)+((1+1)+1), što daje vrijednost 6. Ovaj proračun se provodi tijekom kompiliranja. 14.3 String operatori # i ## Ekspanzija makro argumenata se ne vrši ako je argument u supstitucijskom tekstu zapisan u navodnim znakovima, odnosno unutar stringa. Ponekad je potrebno da se neki argument u ekspanziji makroa pojavljuje literalno kao string. To se postiže tako da se u supstitucujskom tekstu nepesredno ispred imena argumenta napiše znak # (tzv. string operator). U ekspanziji makroa se tada taj argument pojavljuje zapisan unutar navodnih znakova. Primjerice, za definiciju #define PRINT(x) printf(#x "=%g", x) vrši se ekspanzija PRINT(max) u printf("max" "=%g", max), a pošto se dva uzastopna navodnika poništavaju, efekt je da se kompilira naredba printf("max=%g", max). Zadatak: Provjerite ispis sljedećeg programa: /* program fun.c */ #include <stdio.h> #include <math.h> #define PRINT_FUN(fun, x) printf(#fun " = %gn", fun ((x))) int main() { PRINT_FUN(sin, 3); PRINT_FUN(cos, 3); return 0; } Ispis je: 190
  • 191.
    sin = 0.14112 cos = -0.989992 Operator ## djeluje na dva susjedna simbola na način da preprocesor izbaci taj operator i sva prazna mjesta, pa se dobije jedan novi simbol s identifikatorom sastavljenim od ta dva susjedna simbola. Primjerice, za definiciju #define NIZ(ime, tip, n) tip ime ## _array_ ## tip[N] vrši se ekspanzija NIZ(x, int, 100) int x_array_int [100] NIZ(vektor, double, N) double vektor_array_double [N] Zadatak: Provjerite ispis sljedećeg programa: /* program strop.c */ #include <stdio.h> #define N 10 #define PRINT(x) printf(#x "=%dn", x) #define NIZ(ime, tip, n) tip ime ## _array [n] int main() { int i, y=7; NIZ(x, int, N); for(i=0; i<N;i++) x_array[i] = 10; PRINT(y); PRINT(x_array[y]); return 0; } Ispis je: y=7 x_array[y]=10 14.4 Direktiva #undef Ponekad je potrebno redefinirati značaj nekog identifikatora. U tu svrhu koristi se direktiva #undef: #undef identifikator Ovom se direktivom poništava prethodni značaj identifikatora. Primjerice, #define SIMBOL_X VALUE_FOR_X .... .... /* područje gdje se koristi SIMBOL_X */ .... 191
  • 192.
    #undef SIMBOL_X .... .... /* ovdje se ne može koristiti SIMBOL_X */ .... #define SIMBOL_X NEW_VALUE_FOR_X .... .... /* ovdje ponovo može koristiti SIMBOL_X */ .... /* ali s nekim drugim značenjem */ Ako se pokuša redefinirati neki simbol bez prethodne #undef direktive, C kompilator će dojaviti grešku poput: "Attempt to redefine macro SIMBOL_X" 14.5 Direktive za uvjetno kompiliranje Moguće je pomoću posebnih direktiva kontrolirati i sam proces pretprocesiranja. U tu svrhu se koriste direktive: #if, #ifdef, #ifndef, #elif, #else i #endif. Iza direktiva #if i #elif (else-if) mogu se koristiti prosti izrazi koji rezultiraju cjelobrojnom vrijednošću (ne smiju se koristiti sizeof i cast operatori te enum konstante). Ako je vrijednost tih izraza različita od nule, tada se u proces kompiliranja uključuje tekst koji slijedi iza ovih direktiva sve do slijedeće direktive (#elif, #else ili #endif). U sljedećem primjeru pokazano je kako se pomoću ovih direktiva određuje koja će datoteka biti uključena u proces kompiliranja, ovisno o tipu operativnog sustava: #if SISTEM == LINUX #define HDR "linx.h" #elif SISTEM == MSDOS #define HDR "msdos.h" #elif SISTEM == WIN32 #define HDR "win32.h" #else #define HDR "default.h" #endif #include HDR Pretpostavljeno je da je prethodno definirana vrijednost simbola: LINUX, WIN32, MSDOS i SISTEM. Direktiva #elif ima značenje else if. Iza #if i #elif može se koristiti izraz defined(identifikator) koji daje 1 ako je identifikator prethodno registriran kao neki simbol (uključujući i makro simbole), inače je 0. Primjerice, da bi se osiguralo da se sadržaj neke datoteke može uključiti u izvorni kod samo jedan put, često se koriste direktive po sljedećem obrascu : /* datoteka header.h */ #if !defined(HEADER_H) #define HEADER_H 192
  • 193.
    /* ovdje sezapisuje sadržaj od header.h */ #endif Pri prvom uključenju ove datoteke definira se simbol HEADER_H i sadržaj datoteke se uljučuje u izvorni kod. Ako se ova datoteka uključi po drugi put, neće se koristiti njen sadržaj jer tada postoji definiran simbol HEADER_H. Direktive #ifdef i #ifndef su specijalizirane za ispitivanje da li je neki identifikator definiran (ili nije definiran). Prethodni se primjer može zapisati i u obliku: #ifndef _HEADER_H #define _HEADER_H /* ovdje se zapisuje sadržaj od header.h */ #endif #ifdef identifikator je ekvivalentno #if defined(identifikator). #ifndef identifikator je ekvivalentno #if !defined(identifikator). Iza svake #if, #ifdef ili #ifndef direktive mora biti #endif direktiva. 193
  • 194.
    15 Rad sdatotekama i tokovima Naglasci: • ulazno-izlazni tokovi • standardni tokovi • tekstualne i binarne datoteke • otvaranje i zatvaranje datotečnih tokova • formatirani tokovi • binarni tokovi • sekvencijalni i proizvoljni pristup datotekama • kopiranje, brisanje i promjena imena datoteka 15.1 Ulazno-izlazni tokovi U standardnoj je biblioteci implementiran niz funkcija koje na jedinstven način tretiraju sve ulazno izlazne operacije: unos s tipkovnice, ispis na ekran te čitanje i pisanje informacija koje se pohranjuju na magnetskim i optičkim medijima. Komuniciranje s uređajima koji obavljaju ove operacije vrši se sekvencijalno bajt po bajt, a programski mehanizam kojim se vrši ovakvi prijenos informacije naziva se tok (eng. stream). U jednom se programu može raditi s više tokova. Svakom toku se pridjeljuje jedna struktura podataka imena FILE, koja je definirana u <stdio.h>. Temeljna namjena te strukture je da služi kao memorijski ulazno/izlazni međuspremnik (eng. I/O buffer) pri prijenosu podataka. Važno je znati da se pri izlaznim operacijama podaci ne šalju direktno vanjskim uređajima, već se najprije upisuju o ovaj međuspremnik. Kada se on ispuni, tada se sadržaj cijelog međuspremnika šalje vanjskom uređaju. Na ovaj način se smanjuje broj pristupa disku i znatno ubrzava rad s datotekama. Sličnu namjenu ovaj međuspremnik ima i pri ulaznim operacijama. Tokovi se dijele u četiri grupe: • standardni ulaz (vrši dobavu znakove tipkovnice) • standardni izlaz (vrši ispis na ekran) • standardna dojava greške (obično se vrši ispis na ekran) • datotečni tok (vrši čitanje ili pisanje podataka u datoteku) Standardni ulaz, standardni izlaz i standardni tok dojave greške se samoinicijaliziraju pri pokretanju programa, a njihov pokazivač na strukturu FILE je u globalnim varijablama: FILE *stdin; /* pokazivač toka standardnog ulaza */ FILE *stdout; /* pokazivač toka standardnog izlaza */ FILE *stderr; /* pokazivač toka dojave greške */ Ovi pokazivači su deklarirani u datoteci <stdio.h>. Iniciranje pokazivača datotečnih tokova mora obaviti sam programer. Kako se to radi bit će objašnjeno kasnije. 194
  • 195.
    Programer ne moravoditi računa o detaljima kako se izvršava ulazno/izlazni prijenos podataka. Ono što on mora znati je pokazivač toka, s kojim se komunicira, i funkcije pomoću kojih se ta komunikacija realizira. Pokazivača toka (FILE *) se mora navesti kao argument svake funkcije s kojom se vrše ulazno/izlazne operacije. Primjerice, za formatirani ispis podataka koristi se funkcija fprintf kojoj prototip glasi: int fprintf(FILE *pTok, const char *format, ...); gdje je pTok pokazivač toka, a tri točkice označavaju da se funkcija može koristiti s promjenjljivim brojem argumenata. Formatirani ispis se vrši prema obrascu koji se zapisuje u stringu format, na isti način kako se zapisuje format ispisa u printf()funkciji. Primjerice, za ispis stringa "Hello World" na standardnom izlazu može se koristiti naredba: fprintf(stdout, "Hello World"); koja ima isti učinak kao naredba: printf("Hello World"); Zapravo, printf() funkcija je interno realizirana kao fprintf() funkcija kojoj je pokazivač toka jednak stdout. 15.2 Binarne i tekstualne datoteke Informacije se u datoteke zapisuju u kodiranom obliku. Temeljna su dva načina kodiranog zapisa: binarni i tekstualni (ili formatirani). Kaže se da je zapis izvršen u binarnu datoteku kada se informacije na disk zapisuju u istom binarnom obliku kako su kodirane u memoriji računala. U tekstualne datoteke se zapis vrši formatirano pomoću slijeda ASCII znakova, na isti način kako se vrši tekstualni ispis na video monitoru, primjerice printf() funkcijom. Sadržaj tekstualnih datoteka se može pregledati bilo kojim editorom teksta, dok sadržaj binarnih datoteka obično može razumjeti samo program koji ih je formirao. Treba imati na umu da kada se u tekstualnu datoteku formatirano upisuje C-string, tada se ne zapisuje završni znak '0'. Uobičajeno se zapis u tekstualne datoteke vrši u redovima teksta, na način da se za oznaku kraja linije koristi znak 'n'. Takovi zapis zovemo linija. Poželjno je da se ne unose linije koje sadrže više od 256 znakova, jer se time osigurava da će tekstualna datoteka biti ispravno očitana s gotovo svim programima koji manipuliraju s tekstualnim zapisom. Potrebno je napomenuti da se na MS-DOS računalima tekstualne datoteke zapisuju tako da se svaki znak 'n' (CR) pretvara u dva znaka "rn" (CR-LF), a kada se vrši očitavanje s diska tada se ""rn" prevodi u jedan znak 'n'. Ova se operacija obavlja na razini operativnog sustava. Programer o njoj ne mora voditi računa ukoliko koristi funkcije koje su u C jeziku predviđene za rad s tekstualnim datotekama.. Iznimka je slučaj kada se datoteka, koja je zapisana u tekst modu, tretira kao binarna datoteka. Na UNIX sustavima se ne vrši ova pretvorba. Prema ANSI/ISO standardu u datoteka <stdio.h> sadrži prototipove funkcija za rad s datotekama. Neke od ovih funkcija predviđene su za rad s binarnim datotekama, a neke za rad s tekstualnim datotekama. Prije nego se opiše te funkcije najprije će biti pokazano kako se pristupa datotekama. 15.3 Pristup datotekama Svaka datoteka ima ime. Ime datoteke je spremljeno na disku kao tekstualni zapis u posebnoj sekciji kataloga diska. Uz ime su zabilježeni i podaci o datoteci: vrijeme kada je 195
  • 196.
    spremljena, broj bajtakoje datoteka zauzima, mjesto na disku gdje je spremljen sadržaj datoteke i atributi pristupa datoteci (read, write, hidden). Da bi se moglo koristiti neku datoteku potrebno je od operativnog sustava zatražiti dozvolu pristupa toj datoteci. Taj proces se zove otvaranje datoteke. Isto tako se za kreiranje nove datoteke mora zatražiti dozvola od operativnog sustava. Tu funkciju obavlja standardna funkcija fopen(). Ona pored komunikacije s operativnim sustavom kreira datotečni tok koji sadrži memorijski međuspremnik za efikasno čitanje ili spremanje podataka na disk. Prototip funkcije fopen() je: FILE *fopen(const char *staza, const char *mod); staza je string koji sadrži ime datoteke, s potpunim opisom staze direktorija, primjerice string : char *filename = "c:datalist.txt"; bi koristili na Windows računalima za otvoriti datoteku imena list.txt koja se nalazi na disku c: u direktoriju imena data. Napomenimo da se u imenu datoteke ne smiju koristiti znakovi : /, , :, *, ?, ", <, > i |. Ako se zapiše samo ime datoteke, podrazumijeva se da se datoteka nalazi u tekućem direktoriju. mod je string koji se opisuje način otvaranja datoteke. Zapisuje s jednim ili više znakova: r, w, a i +, čije značenje je dano u tablici 15.1. mod značenje "r" Otvori datoteku za čitanje (eng. read). Ako datoteka ne postoji fopen() vraća NULL. Otvori datoteku za pisanje (eng. write). Ako ne postoji datoteka zadanog imena kreira se nova datoteka. "w" Ako postoji datoteka zadanog imena njen se sadržaj briše i kreira prazna datoteka (novi podaci će se zapisivati počevši od početka datoteke). Otvori datoteku za dopunu sadržaja (eng. append) . Ako ne postoji datoteka zadanog imena kreira se "a" nova datoteka. Ako postoji datoteka zadanog imena, novi podaci će se dodavati na kraj datoteke. Otvori datoteku za čitanje i pisanje . Ako ne postoji datoteka zadanog imena kreira se nova datoteka. "r+" Ako postoji datoteka zadanog imena njen se sadržaj briše i kreira prazna datoteka (novi podaci će se zapisivati od početka datoteke).. "w+" Isto kao r+ Otvori datoteku za čitanje i dopunu . Ako ne postoji datoteka zadanog imena kreira se nova datoteka. "a+" Ako postoji datoteka zadanog imena u nju se vrši upis na kraju datoteke.. Ako se iza slova w, r ili a još zapiše slovo 'b' to označava da se datoteku treba otvoriti u binarnom "b" modu, inače se datoteka otvara u tekstualnom modu. Tablica 15.1. Značaj znakova u stringu mod funkcije fopen() Funkcija fopen() vraća pokazivač toka (FILE *). U slučaju greške vrijednost toga pokazivača je NULL. Najčešći uzrok greške pri otvaranju datoteke je: • Neispravan zapis imena datoteke. • Neispravan zapis direktorija ili oznake diska. • Ne postoji direktorij zadana imena • Zahtjev da se otvori nepostojeća datoteka u modu čitanja – "r". 196
  • 197.
    Dobra je praksada se uvijek provjeri da li je datoteka otvorena bez greške. Primjerice, za otvoriti datoteku imena "hello.txt" u modu pisanja, pogodan je slijed iskaza FILE * fp; fp = fopen("hello.txt", "w"); if( fp == NULL) printf("Greska pri otvaranju datoteke"); Kada je datoteka otvorena, koristi je se kao tok. Primjerice, iskazima fprintf( fp, "Hello World!n"); fprintf( fp, "%sn" "Hello World drugi put!"); u prethodno otvorenoj datoteci "hello.txt" biti će zapisane dvije linije teksta: Hello World! Hello World drugi put; Kada se završi rad s datotekom, treba zatvoriti datoteku. Što je to zatvaranje datoteke? Zatvaranje datoteke je postupak koji je nužno provesti kako bi svi podaci iz računala, koji se jednim dijelom nalaze u međuspremniku strukture FILE, bili spremljeni na disk, te da bi se ispravno zapisao podatak o veličini datoteke. Zatvaranje datoteke se vrši funkcijom fclose() čiji je prototip: int fclose(FILE *fp); Funkcija fclose() prima argument koji je pokazivač toka prethodno otvorene datoteke, a vraća vrijednost nula ako je proces zatvaranja uspješan ili EOF ako pri zatvaranju nastane pogreška. Ukoliko se ne zatvori datoteka pomoću ove funkcije, ona će biti prisilno zatvorena po završetku programa. Ipak se preporučuje da se uvijek zatvori datoteka čim se s njome završi rad, jer se time štede resursi operativnog sustava i osigurava od mogućeg gubitka podataka (primjerice, pri resetiranju računala dok je program aktivan, pri nestanku električnog napajanja, ili ako nastupi blokada programa). U nekim će programima biti potrebno da datoteke budu otvorene cijelo vrijeme. U tom slučaju je zgodno koristiti funkciju fflush() kojom se forsira pražnjenje datotečnog međuspremnika i ažurira stanje datoteke na disku, bez zatvaranja datoteke. Prototip funkcije fflush() je int fflush(FILE *fp); funkcija prima argument koji je pokazivač toka prethodno otvorene datoteke, a vraća vrijednost nula ako je proces pražnjenja međuspremnika uspješan ili EOF ako pri zapisu podataka iz međuspremnika nastane greška. Funkcija fflush(stdin) s također često koristi za odstranjivanje viška znakova iz standardnog ulaza. 15.4 Formatirano pisanje podataka u datoteku Formatirano pisanje se vrši pomoći fprintf() funkcije. Pokažimo to sljedećim programom: /* Datoteka: txtfile-write.c */ /* Demonstrira se upis u tekstualnu datotke */ 197
  • 198.
    /* 5 realnihbrojeva, koje unosi korisnik */ #include <stdlib.h> #include <stdio.h> int main() { FILE *fp; float data[5]; int i; char filename[20]; puts("Otipkaj 5 realnih brojeva"); for (i = 0; i < 5; i++) scanf("%f", &data[i]); /* Dobavi ime datoteke, ali prethodno */ /* isprazni moguci višak znakova iz međuspremnika ulaza */ fflush(stdin); puts("Otipkaj ime datoteka:"); gets(filename); if ( (fp = fopen(filename, "w")) == NULL) { fprintf(stderr, "Greska pri otvaranju datoteke %s.", filename); exit(1); } /*Ispisi vrijednosti u datoteku i na standardni izlaz */ for (i = 0; i < 5; i++) { fprintf(fp, "ndata[%d] = %f", i, data[i]); fprintf(stdout, "ndata[%d] = %f", i, data[i]); } fclose(fp); printf("nSada procitaj datoteku: %s, nekim editorom", filename); return(0); } Izlaz iz programa je: Otipkaj 5 realnih brojeva 3.14159 9.99 1.50 3. 1000.01 Otipkaj ime datoteke brojevi.txt data[0] = 3.141590 data[1] = 9.990000 data[2] = 1.500000 data[3] = 3.000000 198
  • 199.
    data[4] = 1000.010 Sada procitaj datoteku: brojevi.txt, nekim editorom 15.5 Formatirano čitanje podataka iz datoteke Za formatirano čitanje sadržaja datoteke koristi se fscanf() funkcija, koja je poopćeni oblik scanf() funkcije za dobavu podataka iz ulaznih tokova. Prototip fscanf() funkcije je: int fscanf(FILE *fp, const char *fmt, ...); Parametar fp je pokazivač ulaznog toka, koji može biti stdout ili datotečni tok koji se dobije kada se datoteka otvori s atributom "r", "r+" ili "w+". String fmt služi za specifikaciju formata po kojem se učitava vrijednost varijabli, čije adrese se koriste kao argumenti funkcije. Tri točke označavaju proizvoljan broj argumenata, uz uvjet da svakom argumentu mora pripadati po jedan specifikator formata u stringu fmt. Primjer: Program txtfile-read.c čita sadržaj datoteke "input.txt", a zatim ga ispisuje na standardni izlaz. Prethodno je potrebno nekim editorom teksta formirati datoteka imena "input.txt", sa sljedećim sadržajem: 6.8 5.89 67 1.099010 67.001 /* Datoteka: txtfile-read.c */ /* Demonstrira se upis u tekstualnu datotke */ /* 5 realnih brojeva, koje unosu korisnik */ #include <stdio.h> int main() { float f1, f2, f3, f4, f5; FILE *fp; if ( (fp = fopen("INPUT.TXT", "r")) == NULL) { fprintf(stderr, "Greska pri otvaranju datoteke.n"); exit(1); } fscanf(fp, "%f %f %f %f %f", &f1, &f2, &f3, &f4, &f5); fprintf(stdout, "Vrijednosti u datoteci su:n"); fprintf(stdout, "%f, %f, %f, %f, %fn.", f1, f2, f3, f4, f5); fclose(fp); return(0); } Ispis je: Vrijednosti u datoteci su: 6.800000, 5.890000, 67.000000, 1.099010, 67.000999 199
  • 200.
    Uočite da jeposljednji broj u datoteci 67.0001 pročitan kao 67.000999. Očito je da se u pretvorbi koja se vrši pri formatiranom unosu gubi na točnosti. Funkcija fscanf() je pogodna za formatirani unos brojeva, ali nije pogodna za unos stringova i znakova. To je pokazano u prethodnim poglavljima, kada je analizirana upotreba funkcije scanf(). Dalje će biti opisane funkcije koje omogućuju dobavu znakova i linija teksta. 15.6 Znakovni ulaz/izlaz U pristupu datotekama pojam znakovni ulaz/izlaz se koristi za transfer pojedinačnog znaka ili jedne linije (linija je nula ili više znakova zaključenih s znakom nove linije). Znakovni ulaz/izlaz se uglavnom koristi s tekstualnim datotekama. Znakovni ulaz – getc, ungetc, fgetc, fgets Za dobavu znakova koriste se funkcije getc() i fgetc(), a za dobavu linije koristi se funkcija fgets(). Deklarirani su u <stdio.h>. Prototip funkcija za dobavu znaka je: int getc(FILE *fp); int fgetc(FILE *fp); Obje funkcije obavljaju s tokom fp iste operaciju kao funkcija fgetchar() sa standardnim ulazom, na način da vraćaju trenutni ulazni znak, ili EOF ako je greška ili kraj datoteke. Prototip funkcije za dobavu linije je: char *fgets(char *str, int n, FILE *fp); Parametar str je pokazivač gdje će biti prihvaćen string, n je maksimalni broj znakova koji string prihvaća (uključujući i nul znak), a fp je pokazivač toka. Znakovi se uzimaju iz toka sve do pojave znaka nove linije ili dok se ne prenese n-1 znakova (fgets() postavlja n-ti znak '0'). Ako je transfer uspješan fgets() vraća pokazivač str, a ako je transfer neuspješan ili ako je detektiran EOF vraća NULL. Ukoliko greška ili EOF nastupi nakon transfera prvog znaka, str više nije pogodan za upotrebu jer nije ispravno zaključen s nul znaka. Obično se ova funkcija koristi za dobavu linije teksta. Preporuka je da se uvijek alocira dovoljno memorije za string, kako be se učitala čitava linija. Ponekad je pri dobavi znaka potrebno vratiti taj znak u tok. U tu svrhu se može koristiti funkcija int ungetc(int c , FILE *fp); ungetc() je korisna pri leksičkoj analizi. Primjerice, ako se iz toka dobavlja niz znamenki može ih se pridijeliti nekom broju sve dok se ne pojavi znak koji nije znamenka. Tada je zgodno vratiti taj znak u tok kako bi bio na raspolaganju u nekoj drugoj operaciji. Primjerice, sljedeći iskazi rezultiraju dobavom cijelog broja iz tekstualnog ulaznog toka: int n = 0; int c; while((c = getc(fp)) >= '0' && c <= '9') n = 10 * n + (c - '0'); ungetc(c, fp); /* nije znamenka – vrati znak u ulazni tok */ 200
  • 201.
    printf("%d", n); Ovim semehanizmom ne smije vraćati više znakova u tok. Garantira se uspješno vraćanje samo jednog znaka. Ukoliko se ne može izvršiti ova operacija funkcija ungetc() vraća EOF, a ako je operacija uspješna funkcija ungetc() vraća znak c. Znakovni izlaz – putc, fputs Za znakovni izlaz se mogu koristiti dvije funkcije; putc() i fputs(). Funkcija putc() je ekvivalentna funkciju putchar() kada se komunicira sa standardnim izlazom. Prototip te funkcije je: int putc(int ch, FILE *fp); Funkcija koristi dva argumenta; ch je znak koji se zapisuje, a fp je pokazivač izlaznog toka. Iako je ch deklariran kao int uzima se samo donji bajt. Funkcija vraća znak koji je zapisan u tok ili EOF ako nastupi pogreška. Za zapis stringa u izlazni tok koristi se funkcija fputs(), kojoj prototip glasi: int fputs(char *str, FILE *fp); Koriste se dva argumenta: str je pokazivač na string, a fp je pokazivač izlaznog toka. Ova funkcija zapisuje string bez zaključnog nul znaka. Ako je transfer uspješan vraća pozitivnu vrijednost, a ako je transfer neuspješan vraća EOF. Za ispis stringa na standardni izlaz do sada je korištena funkcija int puts(char *str); koja je uvije dodavala znak nove linije. To je različito od djelovanja fputs(str, stdout), koja ne dodaje znak nove linije. Znakovni prijenos kod binarnih datoteka Za direktan upis u binarne datoteke, bez ikakvog posrednog formatiranja, također se mogu koristiti funkcije putc() i fgetc() ali se tada ne vrijedi da se vrijednost EOF (-1) koristi za dojavu greške, jer je kod binarnih datoteka to regularni simbol. Za detektiranje kraja datoteke predviđena je posebna funkcija int feof(FILE *fp); koja vraća vrijednost različitu od nule ako je dosegnut kraj datoteke. Nakon toga više nije moguće čitanje iz datoteke. Primjer: kopiranje datoteka Kopiranje sadržaja jedne datoteke u drugu datoteku provodi se prema sljedećem algoritmu: 1. Otvori izvornu datoteku u binarnom modu (tj. datoteku iz koje se kopira) 2. Otvori odredišnu datoteku u binarnom modu (tj. datoteku u koju se kopira) 3. Učitaj znak iz izvorne datoteke 4. Ako funkcija feof() vrati vrijednost različitu od nule (nije dosegnut kraj datoteke) tada upiši znak u odredišnu datoteku i vrati se na korak 3. 5. Ako funkcija feof() vrati 0 to znači da je dosegnut kraj datoteke. U tom slučaju zatvori obje datoteke. Ovaj algoritam je implementiran u funkciji 201
  • 202.
    int kopiraj_datoteke( char*ime_izvora, char *ime_odredista ); Funkcija prima dva argumenta koji označavaju imena izvorne i odredišne datoteke. Vraća 0 ako nije izvršeno kopiranje ili 1 ako je kopiranje uspješno. Implementacija funkcije je: int kopiraj_datoteke( char *ime_izvora, char *ime_odredista ) { FILE *fpi, *fpo; /* Otvori datoteku za čitanje u binarnom modu */ if (fpi = fopen( ime_izvora, "rb" ) == NULL ) return 0; /* Otvori datoteku za pisanje u binarnom modu */ if ( fpo = fopen( ime_odredista, "wb" ) == NULL ) { fclose ( fpi ); return 0; } /* Učitaj 1 bajt iz fpi. Ako nije eof, upiši bajt u fpo */ while (1) { char c = fgetc( fpi ); if ( !feof( fpi ) ) fputc( c, fpo ); else break; } fclose ( fpi); fclose ( fpo); return 1; } Testiranje ove funkcije se provodi programom kopiraj.c u kojem se ime izvorne i odredišne datoteke dobavlja s komandne linije. /* Datoteka: kopiraj.c * kopira datoteku ime1 u datoteku ime2, komandom * c:> kopiraj ime1 ime2 */ #include <stdio.h> int kopiraj_datoteke( char *ime_izvora, char *ime_odredista ); int main(int argc, char **argv) { char *ime1, *ime2; if (argc <3) /* moraju biti dva argumenta komandne linije */ { printf("Uputstvo: kopiraj ime1 ime2n"); return 1; } ime1 = argv[1]; ime1 = argv[2]; if( kopiraj_datoteke(ime1, ime1 ) ) 202
  • 203.
    puts("Kopiranje zavrseno uspjesnon"); else puts("Kopiranje neuspjesno!n"); return(0); } U radu s binarnim datotekama za detekciju kraja datoteke isključivo se koristi funkcija feof(), dok se u radu s tekstualnim datotekama kraj datoteke može detektirati i kada funkcije vrate EOF (obično je to vrijednost -1). 15.7 Direktni ulaz/izlaz za memorijske objekte Najefikasniji i najbrži način pohrane bilo kojeg složenog memorijskog objekta je da se zapisuje u binarne datoteke. Na taj način datoteka sadrži sliku memorijskog objekta, pa se isti može na najbrži mogući način prenijeti iz datoteke u memoriju. Za ovakvi tip ulazno izlaznih operacija predviđene su dvije funkcije: fwrite() i fread(). fwrite Funkcija fwrite() služi za zapis u datoteku proizvoljnog broja bajta s neke memorijske lokacije. Prototip funkcije je: int fwrite(void *buf, int size, int count, FILE *fp); Argument buf je pokazivač na memorijsku lokaciju s koje se podaci zapisuju u tok fp. Argument size označava veličinu u bajtima pojedinog elementa koji se upisuje, a argument count označava ukupni broj takovih elemenata koji se zapisuju. Primjerice, ako je potrebno zapisati 100 elemenata niza cijelih brojeva, tada je size jednak 4 (sizeof int), a count je jednako 100. Dakle, ukupno se zapisuje 400 bajta. Funkcija vraća vrijednost koja je jednaka broju elemenata koji su uspješno zapisani. Ako je ta vrijednost različita od count, to znači da je nastala pogreška. Uobičajeno je da se pri svakom transferu vrši ispitivanje ispravnog transfera iskazom: if( (fwrite(buf, size, count, fp)) != count) fprintf(stderr, "Error writing to file."); Primjeri korištenja funkcije fwrite(): 1. zapis skalarne varijable x, koja je tipa float, vrši se sa: fwrite(&x, sizeof(float), 1, fp); 2. zapis niza od 50 elemenata strukturnog tipa, primjerice struct tocka {int x, int y;} niz[50]; vrši se iskazom: fwrite(niz, sizeof(struct tocka), 50, fp); ili fwrite(niz, sizeof(niz), 1, fp); U drugom slučaju čitav se niz tretira kao jedan element, a učinak je isti kao u prvom iskazu. 203
  • 204.
    fread Funkcija fread() služi za učitavanje proizvoljnog broja bajta na neku memorijsku lokaciju. Prototip funkcije je: int fread(void *buf, int size, int count, FILE *fp); Argument buf je pokazivač na memorijsku lokaciju u koju se upisuju podaci iz toka fp. Argument size označava veličinu u bajtima pojedinog elementa koji se učitava, a argument count označava ukupni broj takovih elemenata koji se učitavaju u memoriju. Funkcija vraća vrijednost koja je jednaka broju elemenata koji su uspješno učitani. Ako je ta vrijednost različita od count, to znači da je nastala greška ili je dosegnut kraj datoteke. Primjer: U programu binio.c niz od 10 cijelih brojeva prvo se zapisuje u binarnu datoteku imena "podaci", a zatim se taj niz ponovo učitava u binarnom obliku. /*Datoteka: binio.c */ #include <stdlib.h> #include <stdio.h> #define SIZE 10 void prekini(char *s) { fprintf(stderr, s); exit(1); } int main() { int i, niz1[SIZE], niz2[SIZE]; FILE *fp; for (i = 0; i < SIZE; i++) niz1[i] = 7 * i; /* otvori datoteku za pisanje u binarnom modu*/ if ( (fp = fopen("podaci", "wb")) == NULL) prekini("Greška pri otvaranju datoteke"); /* Spremi niz1 u datoteku */ if (fwrite(niz1, sizeof(int), SIZE, fp) != SIZE) prekini("Greška pri pisanju u datoteku"); /* Zatvori datoteku */ fclose(fp); /* Ponovo otvori datoteku za čitanje*/ if ( (fp = fopen("podaci", "rb")) == NULL) prekini("Greška pri otvaranju datoteke"); /* Čitaj iz datoteke u niz2 */ if (fread(niz2, sizeof(int), SIZE, fp) != SIZE) prekini("Greška pri citanju datoteke"); fclose(fp); /* Sada ispisi oba niza i provjeri da li su jednaka*/ 204
  • 205.
    for (i =0; i < SIZE; i++) printf("%dt%dn", niz1[i], niz2[i]); return(0); } Dobije se ispis: 0 0 7 7 14 14 21 21 28 28 35 35 42 42 49 49 56 56 63 63 Prednost zapisa u binarnom modu u odnosu na formatirani zapis nije samu u efikasnom korištenju resursa i brzini rada. U binarnom modu nema gubitka informacije (smanjenja točnosti numeričkih zapisa) koje se javljaju u formatiranom zapisu. Nije preporučljivo direktno zapisivanje u datoteku struktura koji sadrže pokazivačke članove, jer pri ponovnom učitavanju takovih struktura pokazivači neće sadržavati ispravne adrese. Funkcije rewind() i ftell() U prethodnom programu bilo je potrebno dva puta otvoriti i zatvoriti datoteku istog imena. U oba slučaja se čitanje/pisanje vršilo od početka datoteke. U slučaju kada se s nekom datotekom vrši čitanje i pisanje ona se može otvoriti u modu "w+b". Da bi ovi procesi startali od početka datoteke tada je potrebno koristiti funkciju rewind() kojom se mjesto pristupa datoteci postavlja na početak datoteke. Prototip ove funkcije glasi: void rewind(FILE *fp); Uvijek se može odrediti mjesto na kojem će biti izvršen slijedeći pristup datoteci. To se postiže funkcijom ftell() kojoj je prototip: long ftell(FILE *fp); Funkcija ftell() vraća cjelobrojnu vrijednost koja odgovara poziciji (u bajtima) slijedećeg pristupa datoteci. U slučaju pogreške funkcija vraća vrijednost -1L. Primjer: Program binio1.c ima isti učinak kao i program binio.c, ali se koristi mod "w+b" i funkcija rewind(). Također, demonstrira se upotreba funkcije ftell(). /*Datoteka: binio1.c */ #include <stdlib.h> #include <stdio.h> #define SIZE 10 205
  • 206.
    void prekini(char *s) { fprintf(stderr, s); exit(1); } int main() { int i, niz1[SIZE], niz2[SIZE]; FILE *fp; for (i = 0; i < SIZE; i++) niz1[i] = 7 * i; /* Otvori datoteku za čitanje i pisanje*/ if ( (fp = fopen("podaci", "w+b")) == NULL) prekini("Greška pri otvaranju datoteke"); /* Spremi niz1 u datoteku */ if (fwrite(niz1, sizeof(int), SIZE, fp) != SIZE) prekini("Greška pri pisanju u datoteku"); /* pomoću ftell() izvijesti o broju bajta u datoteci */ printf("U datoteci je zapisano %d bajtan", ftell(fp)); /* vrati poziciju pristupa datoteci na pocetak */ rewind(fp); /* Čitaj iz datoteke u niz2 */ if (fread(niz2, sizeof(int), SIZE, fp) != SIZE) prekini("Greška pri citanju datoteke"); fclose(fp); /* Sada ispisi oba niza i provjeri da li su jednaka*/ for (i = 0; i < SIZE; i++) printf("%dt%dn", niz1[i], niz2[i]); return(0); } 15.8 Sekvencijani i proizvoljni pristup datotekama Sekvencijalni pristup datoteci označava operacije s datotekama u kojima se čitanje ili pisanje uvijek vrši na kraju datoteke. Proizvoljni ili slučajni pristup datoteci (random access) označava operacije s datotekama u kojima se čitanje ili pisanje može usmjeriti na proizvoljno mjesto u datoteci. Svakoj se otvorenoj datoteci dodjeljuje jedan pozicijski indikator koji označava poziciju (u bajtima) od početka datoteke na kojoj će biti izvršeno čitanje ili pisanje. U svim dosadašnjim primjerima korišten je sekvencijalni pristup datoteci. U tom slučaju, nakon otvaranja datoteke pozicijski indikator ima vrijednost 0, a kada se datoteka zatvori pozicijski indikator ima vrijednost koja je jednaka broju bajta koji su zapisani u datoteci, ukoliko je posljednja operacija bila pisanje u datoteku. Proizvoljni pristup datoteci ima smisla samo kod binarnih datoteka, kod kojih se čitanje i pisanje vrši kontrolirano bajt po bajt. On se ostvaruje pomoću funkcije fseek(). fseek Funkcija fseek() služi za proizvoljno postavljanje pozicijskog indikatora datoteke. Deklarirana je u datoteci <stdio.h> prototipom: 206
  • 207.
    int fseek(FILE *fp,long pomak, int seek_start); Prvi argument je pokazivač toka. Drugi argument određuje pomak pozicijskog indikatora, a treći argument određuje od koje startne pozicije se vrši pomak pozicijskog indikatora. Ova startna pozicija se određuje pomoću tri konstante koje su definirane u <stdio.h>, a njihov značaj je opisan u tablici: Konstanta Vrijednost Značaj vrijednosti seek_start SEEK_SET 0 Pomak se vrši od početka datoteke prema kraju datoteke. SEEK_CUR 1 Pomak se vrši od trenutne pozicije prema kraju datoteke. SEEK_END 2 Pomak se vrši od kraja datoteke prema početku datoteke. Funkcija fseek() vraća vrijednost 0 ako je operacija uspješna, a ako je operacija neuspješna vraća vrijednost različitu od nule. Uočite: fseek(fp,0, SEEK_SET)) je jednako rewind(fp). Veličinu datoteke u bajtima se može dobiti naredbama: fseek(fp, 0, SEEK_END); size = ftell(fp); Primjer: U programu seek.c generira se datoteka "random.dat" s 50 slučajnih cijelih brojeva. Zatim se po proizvoljnom redoslijedu čita vrijednosti iz te datoteke. Redoslijed bira korisnik tako da unosi indeks elementa. Program završava kada korisnik unese negativnu vrijednost. /* Program fseek.c */ #include <stdlib.h> #include <stdio.h> #define MAX 50 int main() { FILE *fp; int data, i, niz[MAX]; long pomak; /* Inicijaliziraj niz od 50 elemenata po slucajnom uzorku */ for (i = 0; i < MAX; i++) niz[i] = rand(); /* Otvori binarnu datoteku RANDOM.DAT za čitanje i pisanje. */ if ( (fp = fopen("RANDOM.DAT", "w+b")) == NULL) { fprintf(stderr, "nGreska pri otvaranje datoteke"); exit(1); } /* upiši niz */ if ( (fwrite(niz, sizeof(int), MAX, fp)) != MAX) { fprintf(stderr, "nGreska pisanja u datoteku"); exit(1); 207
  • 208.
    } /* Pitaj korisnika koji element zeli ucitati, */ /* zavrsi ako se unese negativna vrijednost */ while (1) { printf("nIzaberi element datoteke: 0-%d ili -1 za kraj:", MAX-1); scanf("%ld", &pomak); if (pomak < 0) break; else if (pomak > MAX-1) continue; /* Postavi pozicijski indikator datoteke */ if ( (fseek(fp, (pomak*sizeof(int)), SEEK_SET)) != 0) { fprintf(stderr, "nGreska fseek()."); exit(1); } /* Zatim učitaj element i prikazi njegovu vrijednost. */ fread(&data, sizeof(int), 1, fp); printf("nElement %ld ima vrijednost %d.", pomak, data); } /* zatvori datoteku */ fclose(fp); return(0); } 15.9 Funkcije za održavanje datoteka Temeljne operacije za održavanje datoteka su brisanje datoteka, promjena imena datoteka i kopiranje datoteka. Prije je definirana funkciju za kopiranje datoteke. Za brisanje datoteke koristi se funkcija remove(), a za promjenu imena datoteke koristi se funkcija rename(). Ove funkcije su deklarirane u datoteci <stdio.h>. remove Prototipe funkcije remove(), kojom se briše datoteka glasi: int remove( const char *imedatoteke); Funkcija kao argument prima pokazivač stringa koji sadrži ime datoteke (uključujući i stazu) koju treba izbrisati. Operacija se može izvesti samo ako ta datoteka nije otvorena. Funkcija vraća vrijednost 0 ako je operacija brisanja uspješna, a ako je operacija neuspješna vraća vrijednost -1. Razlog neuspješnog brisanja može biti kada datoteka ne postoji ili kada je spremljena s atributom read-only. rename Prototipe funkcije rename(), kojom se mijenja ime datoteke glasi: int rename( const char *ime, const char *novo_ime ); Funkcija prima dva argumenta – pokazivače na string - prvi je ime datoteke, a drugi novo ime za datoteku. Operacija se može izvesti samo ako ta datoteka nije otvorena. Funkcija vraća 208
  • 209.
    vrijednost 0 akoje operacija uspješna, a ako je operacija neuspješna vraća vrijednost -1. Razlog neuspješne operacije može biti: • ne postoji datoteka ime • već postoji datoteka s imenom novo_ime • zadano je novo_ime s drugom oznakom diska Privremene datoteke Ponekad je potrebno formirati tzv. privremenu datoteku koja će poslužiti za smještaj podataka koji se vrši samo za vrijeme izvršenja programa. Tada nije bitno kako se datoteka zove, jer se nju treba izbrisati prije nego završi program. Za formiranje imena privremene datoteke može se koristiti funkcija tmpname(), koja je deklarirana u <stdio.h>. Prototip joj je: char *tmpnam(char *s); Argument funkcije je pokazivač stringa koji pokazuje na memoriju koja je dovoljna za smještaj imena datoteke. Ako je taj pokazivač jednak NULL tada funkcija tmpname() koristi vlastiti statički spremnik u kojem generira neko ime. Funkcija tada vraća pokazivač na taj spremnik. Način formiranja imena privremene datoteke je određen na način koji osigurava da se u jednom programu ne mogu pojaviti dva ista imena za privremenu datoteku. Primjer: u programu tmpname.c demonstrira se korištenje tzv. privremenih datoteka /* Datoteka: tmpname.c * Formiranje privremenih datoteka */ #include <stdio.h> int main() { char ime[80]; FILE *tmp; /* Formiraj privremenu datorteku */ tmpnam(ime); tmp = fopen(ime, "w"); if(tmp != NULL) { /* koristi datoteku */ printf("Formirana datoteka imena: %sn", ime); /* nakon koristenja zatvori datoteku **/ fclose(tmp); /* i izbrisi je s diska */ remove(ime); } } Dobije se ispis: Formirana datoteka imena: s3a4. 209
  • 210.
    16 Apstraktni tipovipodataka - ADT Naglasci: • Apstraktni dinamički tip podataka -ADT • Model, specifikacija, implementacija i aplikacija ADT-a • ADT STACK za rad sa stogom podataka • Proračun aritmetičkog izraza postfiksne notacije • ADT QUEUE za rad s redom a čekanje 16.1 Koncept apstraktnog dinamičkog tipa podataka Pomoću struktura, pokazivača i funkcija mogu se realizirati apstraktni dinamički tipovi podataka (ADT – eng. abstract data type). Mi smo, na neki način, već do sada koristili apstraktne tipove. Primjerice, int, char ili float apstraktno označavaju karakteristike nekog memorijskog objekta i operacije koje se mogu izvršavati s tim objektom. Pošto smo se na te tipove navikli, oni su u našem mentalnom sklopu postali "konkretni" primitivni tipovi C jezika. Sada će ideja tipa biti proširena i na druge objekte apstrakcije, tako da apstraktni tip predstavlja oznaku za skup objekata koji se ponašaju u skladu s definiranim operacijama. Na slici 16.1 prikazan je konceptualni model za rad s apstraktnim tipom podataka. Čini ga: 1. Model Prvi stupanj definiranja ADT-a je izrada modela podataka i operacija kojima se opisuje objekt apstrakcije, neovisno o načinu kako će biti implementiran u C jeziku. Model se opisuje algoritamskim zapisima i matematičkom aksiomatikom operacija. 2. Specifikacija Na temelju tog modela izrađuje se specifikacija u C jeziku koja mora sadržavati: • identifikator tipa kojim se označava ADT, • prototip funkcija kojima se realizira model operacija s apstraktnim objektom, • uz funkcije treba jasno dokumentirati koji su uvjeti za primjenu funkcije (eng. precondition) i stanje objekta nakon djelovanja funkcije (eng. postcondition). Specifikacija se zapisuje u "*.h" datoteci. Ona predstavlja sučelje prema aplikaciji. 3. Implementacija Na temelju specifikacije vrši se implementacija modela, odnosno definiranje potrebnih C funkcija i struktura podataka. Implementacija se zapisuje u jednoj ili više datoteka kao samostalni modul koji se može kompilirati neovisno od programa u kojem se koristi. 4. Aplikacija Korisnik upotrebljava ADT modul na temelju opisa koji je dan specifikacijom, i ne zanima ga kako je izvršena programska implementacija modela. 210
  • 211.
    Objekt apstrakcije ADT - model podataka i operacija kojima se opisuje objekt apstrakcije Izrada programa Sučelje s aplikacijom: Implementacija - oznaka tipa ADT-a funkcija i struktura - specifikacija funkcija i podataka potrebnih Izvršni program uvjeta za primjenu ADT-a za realizaciju ADT-a Slika 16.1. Konceptualni model ADT-a Kako se realizira apstraktni tip bit će najprije pokazano na primjeru apstraktnog objekta brojača. Primjer programske realizacije objekta brojača već je prije opisan u lekciji o modularnom programiranju. Tada je model rada brojača bio sljedeći - stanje objekta brojača opisivale su dvije statičke varijable: count (koja pokazuje izbroj) i mod (koja određuje modul brojača), a operacije s brojačem bile su reset_counter(), incr_count(), get_count(), get_modul(). Nedostatak te - statičke - realizacije brojača je u tome što u jednom programu omogućuje postojanje samo jednog apstraktnog objekta brojača. Sada će biti pokazano kako se programski može omogućiti višestruka pojavnost objekta brojača. Stanje apstraktnog objekta brojača bit će opisano strukturom _counter, pomoću koje se definira i tip COUNTER, koji označava pokazivač na ovu strukturu; struct _counter { int count; int mod; }; typedef struct _counter *COUNTER; Zapis velikim slovima je izvršen iz razloga da podsjeti kako se radi o pokazivačkom tipu. Ime COUNTER se dalje koristi kao oznaku tipa ADT-a brojača. Pojavnost (instancu) objekta tipa COUNTER, određuju dvije funkcije: new_counter(), koja vrši dinamičko alociranje objekta brojača i inicira varijable tipa COUNTER, te delete_counter(), koja dealocira objekt brojača. Pošto je model brojača poznat, sada slijedi opis specifikacije ADT-a COUNTER (u datoteci "counter-adt.h"). Uz svaki prototip funkcije u komentaru su opisani: namjena funkcije, argumenti i vrijednost koju funkcija vraća, uvjeti koji moraju biti zadovoljeni za primjenu funkcije (PRE) i stanje nakon primjene funkcije (POST). 211
  • 212.
    /* Datoteka: counter-adt.h Specifikacija ADT brojača po modulu: mod */ typedef struct _counter *COUNTER; COUNTER new_counter(int mod); /* Funkcija: alocira i inicijalizira novi objekt tipa COUNTER * vraca pokazivač tipa COUNTER * Argumenti: mod je modul brojača * POST: brojač na nuli, a modul brojaca na vrijednost mod. * Ako je mod<=1, modul se postavlja na vrijednost INT_MAX */ void delete_counter(COUNTER pc); /* Funkcija: dealocira objekt ADT brojaca * PRE: pc != NULL * POST: pc==NULL */ void reset_counter(COUNTER pc, int mod); /* Funkcija: resetira stanje brojača * PRE: pc != NULL * POST: brojač na nuli, a modul brojača ima vrijednost mod. * Ako je mod<=1, modul se postavlja na vrijednost INT_MAX */ int incr_count(COUNTER pc); /* Funkcija: inkrementira brojac u intervalu 0..mod-1 * PRE: pc != NULL * POST: vrijednost brojača inkrementirana (u intervalu 0..mod-1) */ int get_count(COUNTER pc); /* Funkcija: vraca trenutnu vrijednost brojača * PRE: pc != NULL */ int get_modul(COUNTER pc); /* Funkcija: vraca vrijednost modula brojača * PRE: pc != NULL */ Na temelju specifikacije vrši se implementacija modula ADT-a. Uočimo da se u specifikaciji funkcija pojavljuje preduvjet PRE: pc != NULL. Ovaj će preduvjet biti uvijek ispunjen ako se pri inicijalizaciji objekta brojača ispita vrijednost pokazivača na objekt, primjerice COUNTER pc = new_counter(0); /* inicijalizirara brojač pc s mod=INT_MAX*/ if(pc == NULL) exit(1); /* ako je p==NULL prekini program */ Tjekom razvoja modula poželjno je provjeravati ovaj preduvjet u svakoj funkciji. U C jeziku, prema ANSI standardu, u datoteci <assert.h> definirana je makro naredba: assert( uvjet ) 212
  • 213.
    kojom se možeprovjeravati da li je neki uvjet ispunjen. Ako uvjet nije ispunjen, prekida se program i izvještava u kojoj datoteci i u kojem retku izvornog koda je došlo do greške. Pošto ovo ispitivanje usporava program, predviđeno je da se ova provjera može isključiti. Ako ne želimo da se vrši ova provjera tada se u komandnoj liniji kompilatora treba definirati simbol NDEBUG, primjerice: c:> cl /D"NDEBUG" ime_datoteke.c ili u izvornom kodu ispred direktive #include <assert.h> treba definirati simbol NDEBUG, tj. #define NDEBUG 1 #include<assert.h> U implementaciji brojača koristit će se makro naredba assert(pc != NULL), i to u svim funkcijama koje kao argument imaju pokazivač pc. Slijedi opis implementacije: /* Datoteka: counter-adt.c * Implementacija ADT brojača po modulu mod */ #include <limits.h> /* zbog definicija INT_MAX*/ #include <stdlib.h> /* zbog definicija malloc i free*/ #include <assert.h> #include "counter-adt.h" typedef struct _counter { int count; int mod; } counter; /* typedef struct _counter *COUNTER; definiran u counter-adt.h */ COUNTER new_counter(int mod) { COUNTER pc = malloc(sizeof(counter)); if(pc != NULL) { if(mod <= 1) mod = INT_MAX; pc->mod = mod; pc->count=0; } return pc; } void delete_counter(COUNTER pc) { assert(pc != NULL); free(pc); } void reset_counter(COUNTER pc, int mod) { assert(pc != NULL); if(mod <= 1) mod = INT_MAX; pc->mod = mod; pc->count=0; } int incr_count(COUNTER pc) { assert(pc != NULL); pc->count++; if(pc->count >= pc->mod) pc->count = 0; return pc->count; 213
  • 214.
    } int get_count(COUNTER pc) { assert(pc != NULL); return pc->count; } int get_modul(COUNTER pc) { assert(pc != NULL); return pc->mod; } Modul ADT-a se može testirati programom testcount-adt.c u kojem se inicijaliziraju dva neovisna objekta brojača pc1 i pc2. Prvi na mod=5, a drugi na mod=INT_MAX. /* Datoteka: testcount-adt.c */ #include <stdio.h> #include "counter-adt.h" int main(void) { int i; COUNTER pc1 = new_counter(5); COUNTER pc2 = new_counter(0); if(pc1 == NULL || pc2 == NULL) exit(1); printf("brojac(mod=%d), brojac(mod=%d)n", get_modul(pc1),get_modul(pc2)); for(i=0; i<=10; i++) { incr_count(pc1); incr_count(pc2); printf("%dtt %dn", get_count(pc1),get_count(pc2)); } printf("itd........n"); delete_counter(pc1); delete_counter(pc2); return 0; } Nakon izvršenja ovog programa dobije se ispis: brojac(mod=5), brojac(mod=2147483647) 1 1 2 2 3 3 4 4 0 5 1 6 2 7 3 8 4 9 0 10 1 11 itd........ 214
  • 215.
    Na kraju razmatranjavažno je uočiti da u specifikaciji (counter-adt.h) nije navedena struktura podataka koja služi za implementaciji objekta brojača. Deklariran je samo pokazivač na "neku" strukturu - COUNTER. Kako izgleda ta struktura važno je implementatoru ADT-a, a ne onome tko ga koristi. Na ovaj način se ostvaruje princip enkapsulacije – skrivanja detalja implementacije od korisnika modula. Početnicima ovaj princip nema posebno značenje, ali iskusnim programerima on znači jednu od temeljnih paradigmi modernog programiranja. Enkapsulacija olakšava timski rad i doradu modula ADT-a, bez obzira u kojoj će aplikaciji biti primijenjen. 16.2 Stog i STACK ADT Stog je naziv za kolekciju podataka kojoj se pristupa po principu LIFO – last in first out. Primjerice, kada slažemo tanjure tada stavljamo jedan tanjur poviše drugog – tu operaciju zovemo push(), a kada uzimamo tanjur tada uvijek uzimamo onaj tanjur kojeg smo posljednjeg stavili u stog – tu operaciju nazivamo pop(). Pored ove dvije temeljne operacije, obično se u pristupu stogu koriste još dvije operacije: top() – vraća vrijednost elementa koji je na vrhu stoga i empty() – vraća 1 ako je stog prazan, inače vraća 0. Stog se može realizirati pomoću ADT STACK, koji ima sljedeću specifikaciju: #ifndef STACK_ADT #define STACK_ADT typedef int stackElemT; typedef struct stack *STACK; STACK stack_new(void); /* alocira memoriju za stog */ /* vraća pokazivač na stog */ void stack_free(STACK S); /* dealocira memoriju koju zauzima stog */ int stack_empty(STACK S); /* vraca 1 ako je stog prazan */ unsigned stack_count(STACK S); /* vraca broj elemenata na stogu */ stackElemT Top(STACK S); /* dobavlja vrijednost elementa na vrha stoga */ stackElemT Pop(STACK S); /* dobavlja vrijednost elementa na vrha stoga */ /* i odstranjuje ga sa stoga */ /* PRE: stog postoji */ /* POST: na stogu je jedan element manje */ void Push(STACK S, stackElemT x); /* postavlja element na vrh stoga */ Slika 16.2. Stog - operacije /* PRE: stog postoji */ /* POST: na stogu je jedan element vise */ #endif 215
  • 216.
    Implementacija se možeizvršiti na više načina. Sada će biti opisana implementacija u kojoj se za spremanje elemenata stoga koristi podatkovna struktura tipa dinamičkog niza, a u poglavlju 18 bit će pokazana implementacija pomoću strukture podataka tipa vezane liste. Implementacija ADT STACK pomoću dinamičkog niza je opisana u datoteci "stack-arr.c". /* Datoteka: stack-arr.c: * Implementacija ADT STACK pomoću niza */ #include <stdlib.h> #include "stack.h" #define STACK_GROW 10U #define STACK_SIZE 100U /* typedef int stackElemT; definirano in stack.h*/ /* typedef struct stack *STACK; definirano in stack.h*/ struct stack { stackElemT *A; unsigned top; /* indeks poviše stvarnog vrha stoga*/ unsigned size; /* veličina niza*/ }; static void stack_error(char *s) { printf("nGreska: %sn", s); exit(1); } STACK stack_new(void) { STACK S = malloc(sizeof(struct stack)); if(S != NULL) { S->size = STACK_SIZE; S -> top = 0; S->A = malloc(sizeof(stackElemT) * S->size); if(S->A == NULL) {free(S); S=NULL;} } return S; } void stack_free(STACK S) { if(S->A != NULL) free(S->A); if(S != NULL) free(S); } int stack_empty(STACK S) { return (S->top <= 0); } stackElemT stack_pop(STACK S) { if(stack_empty(S)) stack_error("Stog prazan"); return S->A[--(S->top)]; } void stack_push(STACK S, stackElemT x) { if (S->top >= S->size) { S->size += STACK_GROW; 216
  • 217.
    S->A = realloc(S->A,sizeof(stackElemT) * S->size); if(S->A == NULL) stack_error("Nema slobodne memorije"); } S->A[(S->top)++] = x; } stackElemT stack_top(STACK S) { if(stack_empty(S)) stack_error("Stog prazan"); return S->A[S->top-1]; } Testiranje ADT STACK provodi se programom stack-test.c. /* Datoteka: stack-test.c */ #include <stdio.h> #include <stdlib.h> #include "stack-arr.c" void upute(void) /* Upute za korisnika */ { printf("Otipkaj:n" "1 - push - gurni vrijednost na stogn" "2 - pop - skini vrijednost sa stogan" "0 - kraj programan"); } int main(void) { int izbor, val; STACK stog = stack_new(); upute(); printf("? "); scanf("%d", &izbor); while (izbor != 0) { switch (izbor) { case 1: /* push */ printf("Unesi integer: "); scanf("%d", &val); stack_push(stog, val); break; case 2: /* pop */ if (!stack_empty(stog)) printf("Podignuta je vrijednost %d.n", stack_pop(stog)); else printf("Stog je prazan.n"); break; default: printf("Pogresan odabir opcije. Ponovi!nn"); upute(); break; } printf("? "); scanf("%d", &izbor); } 217
  • 218.
    printf("nStog:"); while (!stack_empty(stog)) printf(" %d", stack_pop(stog)); stack_free(stog); return 0; } 16.3 Primjena stoga za proračun izraza postfiksne notacije Korištenjem programski simuliranog stoga jednostavno se provodi računanje matematičkih izraza u postfiksnoj notaciji. Postfiksna notacija izraza se piše tako da se najprije napišu operandi, a iza njih operator koji na njih djeluje, primjerice infiksna notacija izraza postfiksna notacija izraza A + B * C A B C * + (A + B) * C A B + C * (a + b)/(c – d) a b + c d - / a * b / c a b * C / Ovaj tip notacije se naziva i obrnuta poljska notacije, prema autoru Lukasiewiczu. Svojstva postfiksne notacije su: 1. Svaka formula se može napisati bez zagrada. 2. Infiksni operatori moraju uvažavati pravila prioriteta što nije potrebno kod postfiksne notacije. 3. Za proračun postfiksne notacije prikladna je upotreba stoga. Pretvorba infiksne u postfiksnu notaciju se izvodi slijedećim algoritmom: 1. Kompletno ispiši zagrade između svih operanada, tako da zagrade potpuno odrede redoslijed izvršenja operacija. 2. Pomakni svaki operator na mjesto desne zagrade 3. Odstrani zagrade Pretvorba izraza (8+2*5)/(1+3*2-4), prema gornjem pravilu, je sljedeća: prema 1. ( ( 8 + ( 2 * 5 ) ) / ( 1 + ( ( 3 * 2 ) - 4 ) ) ) prema 2. i 3. ( ( 8 + ( 2 * 5 ) ) ( 1 + ( ( 3 * 2 ) - 4 ) ) / ( 8 ( 2 * 5 ) + 1 ( ( 3 * 2 ) - 4 ) + / 8 2 5 * + 1 ( ( 3 * 2 ) 4 - + / 8 2 5 * + 1 3 2 * 4 - + / daje notaciju: 8 2 5 * + 1 3 2 * + 4 - / 218
  • 219.
    Za izračun postfiksnogizraza, koji ima n simbola, vrijedi algoritam: 1. Neka je k =1 indeks prvog simbola 2. Dok je k <= n ponavljaj Ako je k-ti simbol operand, stavi ga na stog. Ako je k-ti simbol operator, dobavi dvije vrijednosti sa stoga (najprije drugi, pa prvi operand), izvrši naznačenu operaciju i rezultat vrati na stog. Uvećaj k za 1 3. Algoritam završava s rezultatom na stogu. Primjer: Izraz zapisan infiksnoj notaciji: (8+2*5) / (1+3*2-4). ima postfiks notaciju: 8 2 5 * + 1 3 2 * + 4 - / Proračun ovog izraza pomoću stoga ilustriran je na slici 16.3: Neobrađeni ulazni niz Operacija Sadržaj stoga 1 8 2 5 * + 1 3 2 * + 4 - / push 8 8 2 2 5 * + 1 3 2 * + 4 - / push 2 8 2 3 5 * + 1 3 2 * + 4 - / push 5 8 2 5 4 * + 1 3 2 * + 4 - / pop(b), pop(a),push (a*b) 8 10 5 + 1 3 2 * + 4 - / pop(b), pop(a),push(a+b) 18 6 1 3 2 * + 4 - / push 1 18 1 7 3 2 * + 4 - / push 3 18 1 3 8 2 * + 4 - / push 2 18 1 3 2 9 * + 4 - / pop(b), pop(a),push (a*b) 18 1 6 10 + 4 - / pop(b), pop(a),push(a+b) 18 7 11 4 - / push 4 18 7 4 12 - / pop(b), pop(a),push(a-b) 18 3 13 / pop(b), pop(a),push(a/b) 6 Slika 16.3. Korištenje stoga za proračun izraza koji je zapisan u postfiksnoj notaciji Proračun izraza pomoću postfiksne notacije je jedan od uobičajenih načina kako interpreteri izračunavaju izraze zapisane u višim programskim jezicima - najprije se vrši pretvorba infiksnog zapisa izraza u postfiksni zapis, a zatim se proračun izraza vrši pomoću stoga. U ovom slučaju ne koristi se stog kojim upravlja procesor već se rad stoga simulira programski. Primjer: Datoteka "polish.c" sadrži jednostavni interpreter aritmetičkih izraza. Izraz treba zapisati u komandnoj liniji unutar navodnika, primjerice c:> polish "8 2 5 * + 1 3 2 * + 4 - /" U izrazu se smiju koristiti cijeli brojevi i operatori zbrajanja, oduzimanja, množenja i dijeljenja. Kada se program izvrši dobije se ispis: Rezultat: 8 2 5 * + 1 3 2 * + 4 - / = 6 219
  • 220.
    /* Datoteka polish.c: * Proracun izraza postfiksne notacije */ #include <stdio.h> #include <string.h> #include <stdlib.h> #include <ctype.h> #include "stack.h" #include "stack-arr.c" #define Pop() stack_pop(stack) #define Top() stack_top(stack) #define Push(x) stack_push(stack, (x)) main(int argc, char *argv[]) { char *str; int i, len, tmp; STACK stack; if (argc < 2) { printf("Za proracun (30/(5-2))*10 otkucaj:n"); printf("c:> polish "30 5 2 - / 10 *"n"); exit(1); } str = argv[1]; len = strlen(str); stack = stack_new(); for (i = 0; i < len; i++) { if (str[i] == '+') Push(Pop()+ Pop()); if (str[i] == '*') Push(Pop()* Pop()); if (str[i] == '-') { tmp = Pop(); Push(Pop()- tmp); } if (str[i] == '/') { int tmp = Pop(); if (tmp==0) {printf("Djeljenje s nulomn"); exit(1);} Push(Pop() / tmp); } if (isdigit(str[i])) /* konverzija niza znamenki u broj */ { Push(0); do { Push(10*Pop() + (int)str[i]-'0'); i++; } while (isdigit(str[i])); i--; } } printf("Rezultat: %s = %d n", str, Pop( )); stack_free(stack); } 220
  • 221.
    16.4 Red iQUEUE ADT Red (eng. queue) je struktura koja podsjeća na red za čekanje. Iz reda izlazi onaj koji je prvi u red ušao. Ovaj princip pristupa podacima se naziva FIFO – first in first out. Temeljne su operacije: ADT QUEUE get(Q ) - dobavi element iz reda Q. put( Q , el) - stavi element el u red Q. empty( Q ) - vraća 1 ako je red Q prazan, inače vraća 0. full( Q ) - vraća 1 ako je red Q popunjen, inače vraća 0. Ovakvi se redovi mogu realizirati kao ADT QUEUE prema sljedećoj specifikaciji: /* Datoteka: queue.h */ #ifndef _QUEUE_ADT #define _QUEUE_ADT typedef int queueElemT; typedef struct queue *QUEUE; QUEUE queue_new(void); /* formira novi objekt tipa QUEUE */ void queue_free(QUEUE Q); /* dealocira objekt tipa QUEUE */ int queue_empty(QUEUE Q); /* vraća 1 ako je red prazan */ int queue_full(QUEUE Q); /* vraća 1 ako je red popunjen */ void queue_put(QUEUE Q, queueElemT el); /* stavlja element u red */ queueElemT queue_get(QUEUE Q); /* vraća element iz reda */ void queue_print(QUEUE Q); #endif Implementacija se može provesti na više načina. Za smještaj elemenata reda najčešće se koristi niz ili linearna lista. Najprije ćemo upoznati implementaciju pomoću niza, i to implementaciju koja koristi tzv. cirkularni spremnik. On se realizira pomoću niza, kojem dva indeksa: back i front, označavaju mjesta unosa (back) i dobave (front) iz reda. Niz je veličine QUEUEARRAYSIZE. Operacije put() i get() se mogu ilustrirati na sljedeći način: 221
  • 222.
    Početno je redprazan - - - - - - Front=back (red prazan) Front = back; Nakon operacija put() povećava put('a'); put('b') se indeks back za 1 a b - - - - front back Operacija get() dobavlja element x = get() /* x sadrži vrijednost a */ kojem je indeks jednak front, a a b - - - - zatim se front povećava za front back jedan. Što napraviti kada, nakon put('c'); put('d'); put('e'); višestrukog unosa, back postane - b c d E - jednak krajnjem indeksu niza? front back Ideja je da se back ponovo put('f') postavi na početak niza (takovi - b C d e f spremnik se nazivaju cirkularni back front (red popunjen) spremnik). Time se maksimalno iskorištava prostor spremnika za Red popunjen ako je: smještaj elemenata reda. (back+1) % QUEUEARRAYSIZE == front U datoteci "queue_arr.c" realiziran je ADT QUEUE pomoću cirkularnog spremnika. /* Datoteka: queue-arr.c * QUEUE realiziran kao cirkularni spremnik */ #include <stdio.h> #include <stdlib.h> #include <assert.h> #include "queue.h" #define QUEUESIZE 100 /* maksimalni broj elemenata */ #define QUEUEARRAYSIZE (QUEUESIZE +1) /* veličina niza */ /* typedef int queueElemT; */ /* typedef struct queue *QUEUE; definirani u queue.h*/ struct queue { queueElemT A[QUEUEARRAYSIZE]; int front; int back; }; QUEUE queue_new(void) { QUEUE Q = malloc(sizeof(struct queue)); if(Q != NULL) Q->front = Q->back = 0; return Q; } void queue_free(QUEUE Q) { assert(Q != NULL); if(Q != NULL) free(Q); 222
  • 223.
    } int queue_empty(QUEUE Q) { assert(Q != NULL); return (Q->front == Q->back); } int queue_full(QUEUE Q) { assert(Q != NULL); return ((Q->back + 1) % QUEUEARRAYSIZE == Q->front); } void queue_put(QUEUE Q, queueElemT x) { assert(Q != NULL); Q->A[Q->back] = x; Q->back = (Q->back + 1) % QUEUEARRAYSIZE; } queueElemT queue_get(QUEUE Q) { queueElemT x; assert(Q != NULL); x = Q->A[Q->front]; Q->front = (Q->front +1) % QUEUEARRAYSIZE; return x; } void queue_print(QUEUE Q) { int i; assert(Q != NULL); printf("Red: "); for(i = Q->front % QUEUEARRAYSIZE; i < Q->back; i=(i+1)% QUEUEARRAYSIZE ) printf("%d, ", Q->A[i]); printf("n"); } Testiranje ADT QUEUE provodi se programom queue-test.c. /* Datoteka: queue-test.c */ #include <stdio.h> #include <stdlib.h> #include <assert.h> #include "queue.h" #include "queue-arr.c" void upute(void) { printf ("Izbornik:n" " 1 Umetni broj u Redn" " 2 Odstrani broj iz Redan" " 0 Krajn"); } int main() 223
  • 224.
    { int izbor, elem; QUEUE Q = queue_new(); upute(); printf("? "); scanf("%d", &izbor); while (izbor != 0) { switch(izbor) { case 1: printf("Otkucaj broj: "); scanf("n%d", &elem); if (!queue_full(Q)) { queue_put(Q, elem); printf("%d ubacen u red.n", elem); } queue_print(Q); break; case 2: if (!queue_empty(Q)) { elem = queue_get(Q); printf("%d odstranjen iz reda.n", elem); } queue_print(Q); break; default: printf("Pogresan Izbor.nn"); upute(); break; } printf("? "); scanf("%d", &izbor); } return 0; } 16.5 Zaključak Opisana je metoda programiranja, pomoću koje se sustavno analizira, specificira i implementira programske objekte kao apstraktne dinamičke tipove podataka – ADT. Izrada specifikacije operacija s apstraktnim objektima sve više postaje temeljni element programiranja. To osigurava da se maksimalna pažnja posveti onome što treba programirati. Neovisnost specifikacije od implementacije ADT, osigurava fleksibilan i pouzdan razvoj programa. U specifikaciji ADT-a dva su temeljna elementa: ime ADT-a i operacije koje se mogu izvršiti. To je karakteristka tipova, pa se s ADT-om kreiraju novi apstrakni tipovi podataka. Rad s ADT predstavlja objetno temeljeno programiranje. 224
  • 225.
    17 Rekurzija isloženost algoritama Naglasci: • rekurzija • podijeli pa vladaj • kompleksnost algoritama • binarno pretraživanje niza • sortiranje 17.1 Rekurzivne funkcije U programiranju i matematici često se koriste rekurzivne definicije funkcija. Direktna rekurzija nastaje kada se u definiciji funkcije poziva ta ista funkcija, a indirektna rekurzija nastaje kada jedna funkcija poziva drugu funkciju, a ova ponovo poziva funkciju iz koje je pozvana. Definicija rekurzivne funkcije u pravilu se sastoji od dva dijela: temeljnog slučaja i pravila rekurzije. Primjerice, u matematici se se može rekurzivno definirati funkcija n! (n-faktorijela) na sljedeći način: Definicija n! (n-faktorijela): 1. Temeljni slučaj: 0! = 1 za n=0 2. Pravilo rekurzije: n! = n * (n-1)! za n>0 Sve vrijednosti od n! se mogu izračunati pomoću gornjeg rekurzivnog pravila, tj. 1! = 1 * 0! = 1 * 1 = 1 2! = 2 * 1! = 2 * 1 * 0! = 2 * 1 * 1 = 2 3! = 3 * 2! = 3 * 2 * 1! = 3* 2 * 1 * 0! = 3 * 2 * 1 * 1 = 6 4! = .... Uočite da rekurzivno pravilo znači ponavljanje nekog odnosa, a temeljni slučaj označava prostu radnju nakon koje prestaje rekurzija. Programski se funkcija n! realizira kao funkcija fact(), koja prima argument tipa int i vraća vrijednost tipa int. Koristeći prethodnu matematičku definiciju , funkcija fact() se implementira na sljedeći način: int fact(int n) { if (n == 0) return 1; else return fact(n-1) * n; } Uočite da se u tijelu funkcije fact() poziva ta ista funkcija. Kako se izvršava ova funkcija? Da bi to shvatili, potrebno je znati kako se na razini strojnog koda vrši poziv funkcije. Većina kompilatora to vrši na sljedeći način: 225
  • 226.
    1. Pri pozivufunkcije najprije se argumenti funkcije postavljaju u dio memorije koji je predviđen za lokalne varijable. Ta memorija se naziva izvršni stog, jer se podacima pristupa s pus() i pop() opearacijama. Vrijednost, koja je posljednja stavljena na stog, predstavlja vrh stoga. 2. Zatim se izvršava kôd tijela pozvane funkcije. U njoj se argumenti funkcije tretiraju kao lokalne varijable čija je vrijednost na stogu. 3. Povrat iz funkcije se vrši tako da se stanje izvršnog stoga vrati na stanje prije poziva funkcije, a povratna se vrijednost postavlja u registar procesora, tzv. povratni registar. 4. Izvršenje programa se nastavlja naredbom u pozivnoj funkciji koja slijedi iza pozvane funkcije. Na slici 1 prikazano je izvršenje funkcije fact(4) i stanje izvršnog stoga. stanje stoga koji se izvršenje funkcije fact(4) koristi za prijenos argumenata funkcije fact(4)= ... 4 4 * fact(3)= ... 4 3 3 * fact(2) ... 4 3 2 2 * fact(1) ... 4 3 2 1 1 * fact(0) ... 4 3 2 1 0 return 1 ... 4 3 2 1 return 1*1 ... 4 3 2 return 2*1 ... 4 3 return 3*2 ... 4 return 4*6 ... => 24 Slika 17.1. Redoslijed izvršenja rekurzivne funkcije fact(4) Poziv funkcije fact(4) počinje tako da se argument vrijednosti 4 stavlja na vrh izvršnog stoga, a zatim se izvršavaju naredbe iz tijela funkcije. Pošto je argument različit od nule izvršava se naredba return fact(n-1) * n;. Da bi se ona mogla izvršiti, najprije se vrši poziv funkcije fact(n-1). Zbog toga se na izvršni stog stavlja vrijednost argumenta 3 i poziva funkcija fact(3). Ovaj proces se ponavlja sve dok argument funkcije ne postane jednak nuli. Nakon toga se u tijelu funkcije izvršava naredba return 1;. To znači da se odstranjuje vrijednost s vrha stoga (0), a u povratni registar se upisuje vrijednost 1. Program nastavlja izvršenje naredbom koja slijedi iza pozivne funkcije, a to je zapravo onaj dio naredbe return fact(n-1) * n; u kojoj se vrši množenje povratne vrijednosti od fact(n-1) i argumenta n, čija se vrijednost nalazi na vrhu stoga (u ovom slučaju to je vrijednost 1). Zatim se u povratni registar stavlja vrijednost 1*1 i skida argument s vrha stoga, pa na vrhu stoga ostaje vrijednost 2. Program se ponovo vraća na izvršenje u naredbu return 1 * 2;. U povratni registar se sada upisuje vrijednost 2, na vrhu stoga ostaje vrijednost 3 i izvršenje se vraća u naredbu return 2 * 3; jer je iz nje vršen poziv fact(3). Nakon toga se izvršava naredba return 6 * 4;. Ovo je posljednja naredba koja će se izvršiti. Nakon nje je vrh stoga prazan, a izvršenje programa se vraća na naredbu iz pozivne funkcije koja slijedi iza poziva fact(4). U povratnom registru je vrijednost 24, koja predstavlja vrijednost koju vraća fact(4). Moglo bi se slikovito reći da se rekurzivne funkcije izvršavaju tako da se najprije višestrukim pozivom funkcije vrši "traženje" temeljnog slučaja, pri čemu se pamte sva stanja procesa, a zatim se problem rješava "izvlačenjem" iz rekurzije. Kod funkcija koje imaju veliki broj rekurzivnih poziva može doći do značajnog ispunjenja izvršnog stoga. Kod MSDOS operativnog sustava može se maksimalno koristiti 64Kbajta za 226
  • 227.
    stog, pa trebabiti oprezan pri korištenju rekurzivnih funkcija. Kod WIN32 sustava za stog je predviđeno koristiti do 1Mbajta memorije. Zadatak: Napišite funkciju unsigned suma( unsigned n); kojoj je argument kardinalni broj n, a funkcija vraća vrijednost koja je jednaka sumi svih kardinalnih brojeva 0,1,2...,n. Koristite rekurzivnu definiciju: 1. trivijalni slučaj: ako je n=0, suma(n) = 0 2. rekurzivno pravilo: ako je n>0, suma(n) = suma(n-1)+n 17.2 Matematička indukcija Rekurzija se koristi i pri dokazivanju teorema indukcijom. Princip matematičke indukcija se koristi kod problema čija se zakonitost može označiti cijelim brojem n, kao S(n). Definira se na sljedeći način. Da bi dokazali da vrijedi zakonitost S(n), za bilo koju vrijednost n: 1. Dokaži da zakonitost S(n) vrijedi u trivijalnom slučaju za n=0 2. Zatim dokaži da vrijedi S(n), ako se pretpostavi da vrijedi S(n-1). Primjer: Suma od n prirodnih brojeva se može izračunati prema izrazu: 1 + 2 + 3 + … + n = n (n +1) / 2 Dokaz: 1. Trivijalni slučaj: za n = 1, suma je jednaka 1 Pošto je 1(1+1)/2) = 1 dokazano je da vrijedi trivijalni slučaj. 2. Pretpostavimo da vrijedi za 1 + … +( n -1), pa ispitajmo da li vrijedi za 1 + … +( n -1) + n ? Pošto je 1 + … +( n -1) + n = (n -1)( n -1+1) / 2 + n = n (n +1) / 2 dokaz je izvršen. Zadatak: Dokažite da ova formula vrijedi i za proračun sume svih kardinalnih brojeva (0,1,2,..) koji su manji ili jednaki n. Zadatak: Napišite funkciju za proračun sume svih kardinalnih brojeva koji su manji ili jednaki n, koristeći prethodno izvedenu formulu. 17.3 Kule Hanoja Čovjek nije sposoban razmišljati i rješavati probleme na rekurzivan način. U programiranju se pak rekurziju može koristiti u mnogo slučajeva, posebno kada je njome prirodno definiran 227
  • 228.
    neki problem. Jedanod najpoznatijih rekurzivnih problema u kompjuterskoj literaturi je bez sumnje rješenje inteligentne igre koja se naziva Kule Hanoja. Problem je predstavljen na slici 2. Slika 17.2. Kule Hanoia Postoje tri štapa označena s A, B i C. Na prvom štapu su nataknuti cilindrični diskovi promjenljive veličine, koji imaju rupu u sredini. Zadatak je premjestiti sve diskove s štapa A na štap B u redoslijedu kako se nalaze na štapu A. Pri prebacivanju diskova treba poštovati sljedeće pravila: Odjednom se smije pomicati samo jedan disk. Ne smije se stavljati veći disk povrh manjeg diska. Može se koristiti štap C za privremeni smještaj diskova, ali uz poštovanje prethodna dva pravila. Problem: pomakni N diskova sa štapa A na štap B, može se riješiti rekurzivnim postupkom. Temeljni slučaj i pravilo rekurzije su: Temeljni slučaj - Najjednostavniji slučaj kojeg svatko može riješiti je kada kula sadrži samo jedan disk. Tada je rješenje jednostavno; prebaci se taj disk na ciljni štap B. Rekurzivno pravilo - Ako kula sadrži N diskova, pomicanje diskova se može izvesti u tri koraka 1. Pomakni gornjih N-1 diskova sa štapa A na pomoćni štap C. 2. Preostali donji disk s štapa A pomakni na ciljni štap B. 3. Zatim pomakni kulu od N-1 diskova s pomoćnog štapa C na ciljni štap B. Teško je na prvi pogled prihvatiti da ovo rekurzivno pravilo poštuje pravilo da se uvijek pomiče samo jedan disk, ali ako se prisjetimo da se rekurzivni problemi počinju rješavati tek kad je pronađen temeljni slučaj, u kojem se pomiče samo jedan disk, i da su prije toga zapamćena sva moguća stanja procesa, onda je jasno da se uvijek pomiče samo jedan disk. Kako napisati funkciju pomakni_kulu() koja izvršava gornje pravilo. Potrebni argumente funkcije su: broj diskova koje treba pomaknuti, ime početnog štapa , ime ciljnog štapa, ime pomoćnog štapa. void pomakni_kulu(int n, char A, char B, char C); Također, potrebno je definirati funkciju kojom će se na prikladan način označiti prebacivanje jednog diska. Nju se može odmah definirati u obliku: 228
  • 229.
    void pomakni_disk(char sa_kule,char na_kulu) { printf("%c -> %cn", sa_kule, na_kulu); } Korištenjem ove funkcije i pravila rekurzije, funkciju pomakni_kulu() se može napisati na sljedeći način: void pomakni_kulu(int n, char A, char B, char C) { if (n == 1) { /* temeljni slučaj */ pomakni_disk(A, B); } else { pomakni_kulu (n - 1, A, C, B); /* 1. pravilo */ pomakni_disk (A, B); /* 2. pravilo */ pomakni_kulu (n - 1, C, B, A); /* 3. pravilo */ } } Ili još jednostavnije: void pomakni_kulu(int n, char A, char B, char C) { if (n > 0) { pomakni_kulu (n - 1, A, C, B); pomakni_disk (A, B); pomakni_kulu (n - 1, C, B, A); } } jer se u slučaju kada je n=1 u funkciji pomakni_kulu(0, ....) ne izvršava ništa, pa se u tom slučaju izvršava funkcija pomakni_disk(A,B), što je pravilo temeljnog slučaja. Za testiranje funkcije pomakni_kulu(), koristi se program hanoi.c: /* Datoteka: hanoi.c */ #include <stdio.h> void pomakni_kulu(int n, char A, char B, char C); void pomakni_disk(char sa_kule, char na_kulu) int main() { int n = 3; /* za slučaj 3 diska*/ pomakni_kulu(n, 'A','B','C'); return 0; } Nakon izvršenja ovog programa dobije se izvještaj o pomaku diskova oblika: A -> B A -> C B -> C A -> B C -> A 229
  • 230.
    C -> B A -> B U ovom primjeru je očito da se pomoću rekurzije dobije fascinantno jednostavno rješenje problema. Teško da postoji neka druga metoda kojom bi se ovaj problem mogao riješiti na jednako efikasan način. Zadatak: Provjerite izvršenje programa za slučaj da broj diskova iznosi: 2, 3, 4 i 5. Pokazat će se da broj operacija iznosi 2n-1, što se može i logično zaključiti, jer se povećanjem broja diskova za jedan udvostručuje broj rekurzivnih poziva funkcije pomakni_kulu(), a u temeljnom slučaju se vrši samo jedna operacija. Procijenite koliko bi trajalo izvršenje programa pod uvjetom da izvršenje jedne operacije traje 1us i da se koristi 64 diska. Da li izvršenje tog program traje dulje od životog vijeka čovjeka? 17.4 Metoda - podijeli pa vladaj (Divide and Conquer) U analizi programskih metoda često se spominje metoda "podijeli pa vladaj". Kod nje se rekurzija nameće kao prirodni način rješenja problema. Opći princip metode je da se problem logično podijeli u više manjih problema, tako da se rješenje dalje može odrediti rješavanjem jednog od tih "manjih" problema. 17.4.1 Drugi korijen broja Metodu podijeli pa vladaj primijenit ćemo za približan proračun drugog korijena broja n. Metoda: Numerički se proračuni mogu provesti samo s ograničenom točnošću. Zbog toga zadovoljava postupak u kojem se određuje da vrijednost x predstavlja drugi korijen od n, ako je razlika (n – x2) približno jednaka nuli, odnosno manja od po volji odabrane vrijednosti epsilon. Točno rješenje se nalazi unutar nekog intervala [d,g]. Primjerice, sigurno je da se rješenje nalazi u intervalu [0,n] ako je n>1, odnosno u intervalu [0,1] ako je n<1. Interval može biti i uži ako smo sigurni da obuhvaća točno rješenje. Do rješenja se dolazi rekurzivno: Temeljni slučaj: Ukoliko se uzme da je vrijednost od x u sredini intervala [d,g], tj. x=(d+g)/2, može se prihvatiti da je to zadovoljavajuće rješenje, ako je širina intervala manja od neke po volji odabrane vrijednosti epsilon (pr. 0,000001) , tj. ako je je g-d < epsilon. Ako je širina intervala veća od epsilon, do rješenje se dolazi koristeći rekurziju prema pravilu (2). Pravilo rekurzije: Ako je n < x2 rješenje se traži u intervalu [d, x], inače, rješenje se traži u intervalu [x, g]. Implementacija: U programu korijen.c implementirana je i testrirana funkcija DrugiKorijen(n), koja vraća drugi korijen od n. U toj funkciji se prvo određuje donja i gornja granica intervala unutar kojega se nalazi rješenje, a zatim se poziva funkcija korijen_rek(n, d, g) koja obavlja proračun prema prethodnom rekurzivnom algoritmu. Točnost proračuna se ispituje usporedbom s vrijednošću kojeg vraća standardna funkcija sqrt(n). 230
  • 231.
    /* Program korijen.c*/ #include <stdio.h> #include <math.h> #define EPSILON 0.000001 /* proizvoljni kriterij točnosti */ double korijen_rek(double n, double d, double g) { double x = (d + g)/ 2.0; if (g - d < EPSILON) /* temeljni slučaj */ return x; else if (n < x*x ) /* pravilo rekurzije */ return korijen_rek (n, d, x); else return korijen_rek (n, x, g); } double DrugiKorijen(double n) { double g, d=0.0; /* početne granice d=0.0, g=n ili 1 ako je n<1*/ if(n < 0) n= -n; /* samo za pozitivne vrijednosti */ if(n>1) g=n; else g=1.0; return korijen_rek(n, d, g); } int main( int argc, char *argv[]) { int i; double n; for (i = 1; i < argc; i++) { sscanf( argv[i], "%lf", &n); printf("Drugi korijen(%f) = %f (treba biti %f)n", n, DrugiKorijen(n), sqrt(n)); } return 0; } Nakon poziva programa: c:>korijen 5 7.6 3.14 Dobije se ispis: Drugi korijen (5.000000) = 2.236068 (treba biti 2.236068) Drugi korijen (7.600000) = 2.756810 (treba biti 2.756810) Drugi korijen (3.140000) = 1.772004 (treba biti 1.772005) 17.4.2 Binarno pretraživanje niza Drugi primjer primjene metode podijeli pa vladaj je traženje elementa sortiranog niza metodom koja se naziva binarno pretraživanje niza. Zadatak: Zadan je niz cijelih brojeva a[n] kojem su elementi sortirani od manje prema većoj vrijednosti, tj. a[i-1] < a[i], za i=1,..n-1 231
  • 232.
    Potrebno je odreditida li se u ovom nizu nalazi element vrijednosti x, i to pomoću funkcije int binSearch( int a[],int x, int d, int g); koja vraća indeks elementa a[i], kojem je vrijednost jednaka traženoj vrijednosti x. Ako vrijednost od x nije jednaka ni jednom elementu niza, funkcija binSearch() vraća negativnu vrijednost -1. Cjelobrojne vrijednosti d i g predstavljaju indekse niza koji određuju interval [d,g] unutar kojeg se vrši traženje. Metoda: Problem se može riješiti rekurzivno na sljedeći način: Ako u nizu a[i] postoji element jednak traženoj vrijednosti x, njegov indeks je iz intervala [d,g], gdje mora biti istinito g >= d. Trivijalni slučaj je za d=0, g=n-1, koji obuhvaća cijeli niz. Temeljni slučaj: Razmatra se element niza indeksa i = (g+d)/2 (dijelimo niz na dva podniza). Ako je a[i] jednak x, pronađen je traženi element niza, a funkcija vraća indeks i. Pravilo rekurzije: Ako je a[i] <x rješenje se traži u intervalu [i+1, g], inače je u intervalu [d, i-1]. Implementacija: int binSearch( int a[],int x, int d, int g) { int i; if (d > g) return –1; i = (d + g)/ 2; if (a[i] == x) return i; if (a[i] < x) return binSearch( a, x, i + 1, g); else return binSearch( a, x, d, i - 1); } Proces traženja vrijednosti x=23 u nizu od 14 elemenata, ilustriran je na slici 17.3. 0 1 2 3 4 5 6 7 8 9 10 11 12 13 1 2 3 5 6 8 9 12 23 26 27 31 34 42 D i g 1 2 3 5 6 8 9 12 23 26 27 31 34 42 d i g 1 2 3 5 6 8 9 12 23 26 27 31 34 42 d i g 1.korak: d=0, g=13, i=6, a[6]<23 2.korak: d=i+1=7,g=14, i=10, a[10]>23 3.korak: d=7, g=i-1=9, i=8, a[8]==23 Slika 17.3. Binarno pretraživanje niza 17.5 Pretvorba rekurzije u iteraciju Neke rekurzivne funkcije se mogu transformirati u funkcije koje umjesto rekurzije koriste iteraciju. To su funkcije u kojima je rekurzivni poziv funkcije posljednja naredba u tijelu funkcije, primjerice 232
  • 233.
    void petlja() { iskaz ... if (e) petlja(); } U ovom slučaju, rekurzivni poziv funkcije petlja(), znači da se izvršenje vraća na početak tijela funkcije, zbog toga se ekvivalentna verzija funkcije može napisati tako da se umjesto poziva funkcije koristi goto naredba s odredištem na prvu naredbu tijela funkcije, tj. void petlja() { start: iskaz ... if (e) goto start; } Rekurzivni poziv, koji se vrši na kraju (ili na repu) tijela funkcije, često se naziva "repna rekurzija". (eng. tail recursion). Neki optimizirajući kompilatori mogu prepoznati ovakav oblik rekurzivne funkcije i transformirati rekurzivno tijelo funkcije u iterativnu petlju. Na taj način se dobije efikasnija funkcija, jer se ne gubi vrijeme i prostor na izvršnom stogu koji su potrebni za poziv funkcije. Kod većine se rekurzivnih funkcija ne može izvršiti ova transformacije. Primjerice, funkcija fact() nije repno rekurzivna jer se u posljednjoj naredbi (return fact(n-1)*n;) najprije vrši poziv funkcije fact(), a zatim naredba množenja. Funkcija binSearch(), koja je opisana u prethodnom odjeljku, može se transformirati u funkciju s repnom rekurzijom, na sljedeći način: int binSearch( int a[],int x, int d, int g) { int i; if (d > g) return –1; i = (d + g)/ 2; if (a[i] == x) return i; if (a[i] < x) d=i+1; else g=i-1 return binSearch( a, x, d, g); } Dalje se može provesti transformacija u iterativnu funkciju int binSearch( int a[],int x, int d, int g) { int i; start: if (d > g) return –1; i = (d + g)/ 2; if (a[i] == x) return i; if (a[i] < x) d=i+1; else g=i-1 goto start; } Može se napisati iterativno tijelo funkcije i u obliku while petlje: int binSearch( int a[],int x, int d, int g) { int i; while (d <= g) { 233
  • 234.
    i = (d+ g)/2; if (a[i] == x) return i; if (a[i] < x) d=i+1; else g=i-1; } return –1; } Iterativna verzija se može pojednostaviti u slučaju kada se pretražuje cijeli niz. Tada kao argument funkcije nije potrebna donja granica indeksa, jer je ona jednaka nuli, a umjesto gornje granice indeksa, argument je broj elemenata niza n. int binSearch( int a[],int x, int n) { int i, d=0, g=n-1; while (d <= g) { i = (d + g)/2; if (a[i] == x) return i; if (a[i] < x) d=i+1; else g=i-1; } return –1; } 17.6 Standardna bsearch() funkcija U standardnoj biblioteci implementirana je polimorfna funkcija bsearch(). Služi za binarno pretraživanje nizova s proizvoljnim tipom elemenata niza. Deklarirana je na sljedeći način: void *bsearch( const void *px, const void *niz, size_t n, size_t el_size, int (*pCmpFun)(const void *, const void *) ); Prvi parametar funkcije je pokazivač na objekt kojeg se traži. Drugi parametar je pokazivač na prvi element niza od n elemenata koji zauzimaju el_size bajta. Posljednji parametar je pokazivač na usporednu funkciju koja je ista kao kod qsort() funkcije. Funkcija bsearch() vraća pokazivač na element niza ili NULL ako niz ne sadrži traženi objekt. Realizacija te funkcije, ali pod imenom binsearch(), dana je i testirana programom binsearch.c, i to za slučaj da se vrši pretraživanje niza stringova. /* Datoteka: binsearch.c */ #include <stdio.h> #include <stdlib.h> #include <string.h> typedef char *string; void * binsearch( const void *px, const void *niz, size_t n, size_t el_size, int (*pCmpFun)(const void *, const void *) ) { int i, cmp, d=0, g=n-1; char * adr; 234
  • 235.
    while (d <=g) { i = (d + g)/2; /* adresa i-tog elementa niza*/ adr = (char *)niz + i*el_size; /*el. niza na toj adresi usporedi s objektom *px */ cmp = (*pCmpFun)((void *)adr, (void *)px); if (cmp == 0) return (void *) adr; /* objekt pronađen */ if (cmp < 0) d=i+1; else g=i-1; } return NULL; } int UsporediStringove( const void *pstr1, const void *pstr2 ) { /* Argumenti funkcije su pokazivači na objekte koje * usporedjujemo, u ovom slučaju objekt je string (char *). * Funkcija strcmp() prima argumente tipa string, stoga * treba izvršiti pretvorbu tipa i indirekciju da bi dobili * string,kao argument za strcmp() * ( str = *(string *) pstr)) */ return strcmp( *(string*) pstr1, *(string*)pstr2 ); } int main( void ) { string txt[] = {"Ante", "Ivo", "Marko", "Jure", "Bozo"}; int numstrings = sizeof(txt)/sizeof(txt[0]); string key="Ivo"; int i, idx; string * rez; /* sortiraj niz stringova leksikografski */ qsort((void *)txt, numstrings, sizeof(char*), UsporediStringove); for(i = 0; i < numstrings; i++ ) puts(txt[i]) ; /* pronađi string key */ rez =(string*) binsearch(&key, txt, numstrings, sizeof(char*), UsporediStringove); if(rez != NULL) printf("Pronadjen string "%s" na adresi %Fpn", *pstr, pstr); else printf("Nije pronadjen string "%s" n", key); return 0 ; } 235
  • 236.
    Dobije se ispis: Ante Ivo Marko Jure Bozo Nakon sortiranja: Ante Bozo Ivo Jure Marko Pronadjen string "Ivo" na adresi 0022FF48 17.7 Složenost algoritama - "Veliki - O" notacija Pri analizi složenosti algoritama uvodi se mjera "dimenzije problema". Primjerice, kada se obrađuju nizovi, onda dimenziju problema predstavlja duljina niza n, a kada su u pitanju kvadratne matrice, onda je dimenzija matrice istovremeno i dimenzija problema. Ako je n dimenzija problema, onda se efikasnost korištenja memorije može opisati funkcijom M(n), a vrijeme izvršenja algoritma funkcijom T(n). Funkcija T(n) se često naziva vremenska složenost algoritma (engl. time complexity), dok je M(n) prostorna složenost. Funkciji T(n) se pridaje znatno više značaja nego veličini M(n), pa se pod skraćenim pojmom "složenost" (eng. complexity) obično podrazumijeva vremenska složenost. Razlog tome je iskustvo u razvoju algoritama, koje je pokazalo da je zauzeće memorije znatno manji problem od postizanja prihvatljivog vremena obrade. Analiza brzine izvršenja programa se radi tako da se odredi ukupan broj operacija koje dominantno troše procesorsko vrijeme. Primjerice, u programu za zbrajanje elemenata kvadratne matrice, dimenzija nxn, označen je broj ponavljanja naredbi u kojima se vrši zbrajanje. NAREDBE PROGRAMA BROJ PONAVLJANJA S = 0 ; . for(i=0; i<n; i++) n for(j=0; j<n; j++) n*n S = S + M[i][j] n*n Ako se uzme da prosječno vrijeme izvršenja jedne naredbe iznosi t0, dobije se da ukupno vrijeme izvršenja algoritma iznosi: T(n) = (2n² + n)*t0 Veličina T(n) je posebno važna za velike vrijednosti od n. U tom slučaju je: T(n) ≤ konst * n² Ovaj se zaključak u računarskoj znanosti piše u obliku tzv. "veliki-O" notacije: T(n) = O(n²) i kaže se da je T(n) "veliki O od n²". Funkcija f(n) = n² predstavlja red složenosti algoritma. Uočite da vrijednost konstantnog faktora ne određuje složenost algoritma već samo faktor koji ovisi o veličini problema. Definicija: Složenost algoritma u "veliki-O" notaciji definira se na sljedeći način: T(n) = O( f(n) ), (čita se: T je veliki-O od f) 236
  • 237.
    ako postoje pozitivnekonstante C i n0, takove da je 0 ≤ T(n) ≤ C * f(n), za sve vrijednosti n ≥ n0. U prethodnom je primjeru funkcija složenosti određena razmatrajući broj operacija u kojima se vrši zbrajanje. To ne treba uzeti kao pravilo, već za pojedini problem treba sagledati koje su operacije dominantne po zauzeću procesorskog vremena. Primjerice, kod metoda pretraživanja niza to će biti naredbe usporedbe i pridjele vrijednosti. Klasifikacija algoritama prema redu funkcije složenosti za najpoznatije klase algoritama prikazana je u tablici 17.1. Ova je tablica uređena po kriteriju rastuće složenosti algoritama. Tip algoritma f(n) Konstantan const. Logaritamski log2n Linearan N Linearno-logaritamski nlog2n Kvadratni n2 k Stupanjski n (k>2) Eksponencijalni kn (k>1) Faktorijelni n! Tablica 17.1. Red složenosti algoritama Pri izradi algoritama cilj je nalaženje rješenja koje je manjeg reda složenosti, a posebno je značajan rezultat kada se pronađe polinomsko ili logaritamsko rješenje nekog od eksponencijalnih problema. U nastavku je opisana svaka od navedenih tipova algoritama. Konstantni algoritmi Konstantni algoritmi su klasa algoritama kod kojih je vrijeme rada približno konstantno i ne ovisi od veličine problema. Primjerice, program koji računa kvadratni korijen realnog broja do rezultata po pravilu dolazi poslije približno istog vremena rada bez obzira kolika je veličina broja koji je uzet za ulazni podatak. Logaritamski algoritmi Kod logaritamskog algoritma vrijeme izvršenja programa proporcionalno je (najčešće binarnom) logaritmu veličine problema. Imajući u vidu sporost porasta logaritamske funkcije vidi se da se ovdje radi o najefikasnijim i stoga najpopularnijim algoritmima. Tipičan predstavnik logaritamskih algoritama je binarno pretraživanje sortiranog niza prikazano u prethodnom odjeljku. Opća koncepcija logaritamskih algoritama je sljedeća: 1. Obaviti postupak kojim se veličina problema prepolovi. 2. Nastaviti razlaganje problema dok se ne dođe do veličine 1. 3. Obaviti završnu obradu s problemom jedinične veličine. Ukupan broj razlaganja k dobije se iz uvjeta: n / 2k = 1, odnosno k=log2 n. Kako je po pretpostavci broj operacija koji se obavlja pri svakom razlaganju približno isti, to je i vrijeme rada približno proporcionalno broju razlaganja, odnosno binarnom logaritmu od n. 237
  • 238.
    Linearni algoritmi Linearni algoritmi se javljaju u svim slučajevima gdje je obradom obuhvaćeno n istovjetnih podataka i gdje udvostručenje količine radnji ima za posljedicu udvostručenje vremena obrade. Opći oblik linearnog algoritma može se prikazati u vidu jedne for petlje: for (i=0; i < n; i++) {obrada koja traje vrijeme t} Zanemarujući vrijeme opsluživanja for petlje, u ovom slučaju funkcija složenosti je T(n)=nt, pa je T(n) = O(n). U slučajevima kada nije moguće deterministički odrediti vrijeme izvršavanja sadržaja petlje može se umjesto vremena rada T(n) statistički odrediti srednji broj naredbi I(n) koje program obavlja. Tada se podrazumijeva da je T(n)=t1*I(n), gdje je vrijeme izvršavanja prosječne naredbe t1 = konst. Budući da vrijedi T(n) = O( f(n) ) , I(n) = O( f(n) ), slijedi da se traženi red funkcije složenosti f(n) može odrediti kako iz T(n), tako i iz I(n). Primjerice, pronalaženje maksimalne vrijednosti elementa niza ima sljedeću analizu broja naredbi: NAREDBE PROGRAMA BROJ PONAVLJANJA max = a[0] 1 for(i=1; i<n; i++) (n-1) if (a[i] > max) (n-1) max = a[i] (n-1)p (0<=p<=1) Ovdje p označava vjerojatnost da dođe do ispunjenja uvjeta a[i]>max ( p ne ovisi od n). Sumirajući broj ponavljanja pojedinih naredbi dobije se da je ukupan broj izvršenih naredbi I(n) = 1 + (n-1)(3+p) = (3+ p) n - 1 - p. U ovom je izrazu dominantan samo prvi član polinoma, pa je f(n) = n, odnosno T(n) = O(n). Linearno-logaritamski algoritmi Linearno-logaritamski algoritmi, složenosti O(n log n) ), spadaju u klasu veoma efikasnih algoritama jer im složenost, za veliki n, raste sporije od kvadratne funkcije. Primjer za ovakav algoritam je Quicksort, koji će biti detaljno opisan kasnije. Bitna osobina ovih algoritama je sljedeća: (1) Obaviti pojedinačnu obradu kojom se veličina problema prepolovi. (2) Unutar svake polovine sekvencijalno obraditi sve postojeće podatke. (3) Nastaviti razlaganje problema dok se ne dođe do veličine 1. Slično kao i kod logaritamskih algoritama i ovdje je ukupan broj dijeljenja log2n, ali kako se nakon svakog dijeljenja sekvencijalno obrade svi podaci, to je ukupan broj elementarnih obrada jednak nlog2n, i to predstavlja red funkcije složenosti. 238
  • 239.
    Kvadratni i stupanjskialgoritmi Kvadratni algoritmi, složenosti O(n2), najčešće se dobivaju kada se koriste dvije for petlje jedna unutar druge. Primjer je dat na početku ovog poglavlja. Stupanjski algoritmi nastaju kod algoritama koji imaju k umetnutih petlji, pa je složenost O(nk). Eksponencijalni algoritmi Eksponencijalni algoritmi O(kn) spadaju u kategoriju problema za koje se suvremena računala ne mogu koristiti, izuzev u slučajevima kada su dimenzije takvog problema veoma male. Jedan od takovih primjera je algoritam koji rekurzivno rješava igru "Kule Hanoja", opisan u prethodnom poglavlju. Faktorijelni algoritmi Kao primjer faktorijelnih algoritama najčešće se uzima problem trgovačkog putnika. Problem je formuliran na sljedeći način: zadano je n+1 točaka u prostoru i poznata je udaljenost između svake dvije točke j i k. Polazeći od jedne točke potrebno je formirati putanju kojom se obilaze sve točke i vraća u polaznu točku, tako da je ukupni prijeđeni put minimalan. Trivijalni algoritam za rješavanje ovog problema mogao bi se temeljiti na uspoređivanju duljina svih mogućih putanja. Broj mogućih putanja iznosi n!. Polazeći iz početne točke postoji n putanja do n preostalih točaka. Kada odaberemo jednu od njih i dođemo u prvu točku onda preostaje n-1 mogućih putanja do druge točke, n-2 putanja do treće točke, itd., n+1-k putanja do k-te točke, i na kraju samo jedna putanja do n-te točke i natrag u polaznu točku. Naravno da postoje efikasnije varijante algoritma za rješavanje navedenog problema, ali u opisanom slučaju, s jednostavnim nabrajanjem i uspoređivanjem duljina n! različitih zatvorenih putanja, algoritam ima složenost O(n!). 17.8 Sortiranje Sortiranje je postupak kojim se neka kolekcija elemenata uređuje tako da se elementi poredaju po nekom kriteriju. Kod numeričkih nizova red elemenata se obično uređuje od manjeg prema većim elementima. Kod nizova stringova red se određuje prema leksikografskom rasporedu. Kod kolekcija strukturnog tipa obično se odabire jedan član strukture kao ključni element sortiranja. Sortiranje je važno u analizi algoritama, jer su analizom različitih metoda sortiranja postavljeni neki od temeljnih kompjuterskih algoritama. Za analizu metoda sortiranja postavlja se sljedeći problem: odredite algoritam pomoću kojeg se ulazni niz A[0..n-1], od n elemenata, transformira u niz kojem su elementi poredani u redu od manjeg prema većem elementu, tj. na izlazu treba biti ispunjeno: A[0] ≤ A[1] ≤ A[2] ≤ ... ≤ A[n-2] ≤ A[n-1]. Najprije će biti analizirane dvije jednostavne metode, selekcijsko sortiranje i sortiranje umetanjem, kojima je složenost O(n2). Zatim će biti analizirane dvije napredne metode, quicksort i mergesort, koje koriste metodu "podijeli pa vladaj". Njihova je složenost O(n log2 n). U analizi će biti korištena oznaka A[d..g] za označavanje da se analizira neki niz od indeksa d do indeksa g. Ovaj način označavanja ne vrijedi u C jeziku. 17.8.1 Selekcijsko sortiranje Ideja algoritma je: 1. Pronađi najmanji element i njegov indeks k. 2. Taj element postavi na početno mjesto u nizu (A[0]), a element koji je dotad postojao na indeksu 0 postavi na indeks k. 239
  • 240.
    3. Zatim pronađinajmanji element, počevši od indeksa 1. Kada ga pronađeš, zamijeni ga s elementom indeksa 1. 4. Ponovi postupak (3) za sve indekse (2..n-2). Primjerice za sortirati niz od 6 elemenata: 6, 4, 1, 5, 3 i 2 , treba izvršiti sljedeće operacije: 6 4 1 5 3 2 -> min od A[0..5] zamijeni sa A[0] 1 4 6 5 3 2 -> min od A[1..5] zamijeni sa A[1] 1 2 6 5 3 4 -> min od A[2..5] zamijeni sa A[2] 1 2 3 5 6 4 -> min od A[3..5] zamijeni sa A[3] 1 2 3 4 6 5 -> min od A[4..5] zamijeni sa A[4] 1 2 3 4 5 6 -> niz je sortiran Ovaj algoritam se može implementirati pomoću for petlje: for (i = 0; i < n-1; i++) { imin = indeks najmanjeg elementa u A[i..n-1]; Zamijeni A[i] sa A[imin]; } Uočite da se petlja izvršava dok je i < n-1, a ne za i < n, jer ako A[0..n-2] sadrži n-1 najmanjih elemenata, tada posljednji element mora biti najveći, i on se nalazi na ispravnom položaju , tj. A[n-1]. Indeks najmanjeg elementa u A[i..n-1] pronalazi se sa: imin = i; for (j = i+1; j < n; j++) if (A[j] < A[min]) imin=j; Zamjenu vrijednosti vrši funkcijom swap(); /* Zamjena vrijednosti dva int */ void swap(int *a, int *b) { int t = *a; *a = *b; *b = t; } Sada se može napisati funkcija za selekcijsko sortiranje, niza A koji ima n elemenata, u obliku: void selectionSort(int *A, int n) { int i, j, imin; /* indeks najmanjeg elementa u A[i..n-1] */ for (i = 0; i < n-1; i++) { /* Odredi najmanji element u A[i..n-1]. */ imin = i; /* pretpostavi da je to A[i] */ for (j = i+1; j < n; j++) if (A[j] < A[imin]) /* ako je A[j] najmanji */ imin = j; /* zapamti njegov indeks */ /* Sada je A[imin] najmanji element od A[i..n-1], */ /* njega zamjenjujemo sa A[i]. */ 240
  • 241.
    swap(&A[i], &A[imin]); } } Analiza selekcijskog sortiranja Svaka iteracija vanjske petlje (indeks i) traje konstantno vrijeme t1 plus vrijeme izvršenja unutarnje petlje (indeks j). Svaka iteracija u unutarnjoj petlji traje konstantno vrijeme t2 . Broj iteracija unutarnje petlje ovisi u kojoj se iteraciji nalazi vanjska petlja: Broj operacija u i unutarnjoj petlji 0 n-1 1 n-2 2 n-3 ... ... n-2 1 Ukupno vrijeme je: T(n) = [t1 + (n-1) t2] + [t1 + (n-2) t2] + [t1 + (n-3) t2] + ... + [t1 + (1) t2] odnosno, grupirajući članove u oblik t1 ( …) + (...) t2 dobije se T(n) = (n-1) t1 + [ (n-1) + (n-2) + (n-3) + ... + 1 ] t2 Izraz u uglatim zagradama predstavlja sumu aritmetičkog niza 1 + 2 + 3 + ... + (n-1) = (n-1)n/2 = (n2-n)/2, pa je ukupno vrijeme jednako: T(n) = (n-1) t1 + [(n2-n)/2] t2 = - t1 + t1 n - t2 n/2 + t2 n2/2 Očito je da dominira član sa n2 , pa je složenost selekcijskog sortiranja jednaka O(n2) . 17.8.2 Sortiranje umetanjem Algoritam sortiranja umetanjem (eng. insertion sort) se temelji na postupku koji je sličan načinu kako se slažu igraće karte. Algoritam se vrši u u n-1 korak. U svakom koraku se umeće i-ti element u dio niza koji mu prethodi (A[0..i-1]), tako taj niz bude sortiran. Primjerice, sortiranje niza od n=6 brojeva izgleda ovako: 6 4 1 5 3 2 -> ako je A[1]< A[0], umetni A[1] u A[0..0] 4 6 1 5 3 2 -> ako je A[2]< A[1], umetni A[2] u A[0..1] 1 4 6 5 3 2 -> ako je A[3]< A[2], umetni A[3] u A[0..2] 1 4 5 6 3 2 -> ako je A[4]< A[3], umetni A[4] u A[0..3] 1 3 4 5 6 2 -> ako je A[5]< A[4], umetni A[5] u A[0..4] 1 2 3 4 5 6 -> niz je sortiran Algoritam se može zapisati pseudokôdom: for (i = 1; i < n; i++) { 241
  • 242.
    x = A[i]; analiziraj elemente A[0 .. i-1] počevši od indeksa j=i-1, do indeka j=0 ako je x < A[j] tada pomakni element A[j] na mjesto A[j+1] inače prekini zatim umetni x na mjesto A[j+1] } pa se dobije funkcija: void insertionSort(int *A, int n) { int i, j, x; for (i = 1; i < n; i++) { x = A[i]; for(j = i-1; j >= 0; j--) { if(x < A[j]) A[j+1] = A[j]; else break; } A[j+1] = x; } } Analiza složenosti metode sortiranja umetanjem Sortiranje umetanjem je primjer algoritma u kojem prosječno vrijeme izvršenja nije puno kraće od vremena izvršenja koje se postiže u najgorem slučaju. Najgori slučaj Vanjska petlja se izvršava u n-1 iteracija, što daje O(n) iteracija. U unutarnjoj petlji se vrši od 0 do i<n iteracija, u najgorem slučaju vrši se također O(n) iteracija. To znači da se u najgorem slučaju vrši O(n) ⋅O(n) operacija, dakle složenost je O(n2). Najbolji slučaj U najboljem slučaju u unutarnjoj petlji se vrši 0 iteracija. To nastupa kada je niz sortiran. Tada je složenost O(n). Može se uzeti da to vrijedi i kada je niz "većim dijelom sortiran" jer se tada rijetko izvršava unutarnja petlja. Prosječni slučaj U prosječnom se slučaju vrši n/2 iteracija u unutarnjoj petlji. To također daje složenost unutarnje petlje O(n), pa je ukupna složenost: O(n2). 17.8.3 Sortiranje spajanjem sortiranih podnizova (merge sort) Sortiranje metodom spajanja sortiranih podnizova (eng. merge sort) temelji se na ideji da se niz rekurzivno dijeli na dva sortirana niza, te da se zatim izvrši spajanje tih sortiranih nizova. Problem će biti riješen za slučaj da se sortira niz A[d..g], tj. od donjeg indeksa d do gornjeg indeksa g, funkcijom void mergeSort(int *A, int d, int g); 242
  • 243.
    Rekurzivnom se podjelomniza u dva podniza, A[d..s] i A[s+1,g], koji su otprilike podjednake veličine (indeks s se odredi kao srednja vrijednost s = (d+g)/2), dolazi se do temeljnog slučaja kada u svakom nizu ima samo jedan element. Taj jedno-elementni niz je već sortiran, pa se pri "izvlačenju" iz rekurzije može vršiti spajanje sortiranih podnizova. Ovaj postupak je ilustriran na slici 4. Slika 17. 4. Prikaz sortiranja spajanjem sortiranih podnizova Za implementaciju ovog algoritma bitno je uočiti sljedeće: • Ulazni niz nije nužno "fizikalno" dijeliti na podnizove, jer se podnizovi ne preklapaju. Dovoljno je zapamtiti indekse ulaznog niza koji određuju neki podniz. • Spajanje podnizova se uvijek provodi s elementima koji su u ulaznom nizu poredani jedan do drugog; prvi podniz je A[d..s], a drugi podniz je A[s+1..g]. U tu svrhu koristit će se funkcija: void merge(int *A, int d, int s, int g) /* Ulaz: dva sortirana niza A[d..s] i A[s+1..g] */ /* Izlaz: sortirani niz A[d..g] */ Očito je da se radi o algoritmu tipa "podijeli pa vladaj": 1. Podijeli: podijeli niz A[d,g], na način da dva podniza A[d,s] i A[s+1,g] sadrže otprilike pojednak broj elemenata. To se postiže izborom: s=(d+g)/2. 2. Vladaj: rekurzivno nastavi dijeliti oba podniza sve dok njihova veličina ne postane manja od 2 elementa (niz koji sadrži nula ili jedan element je sortirani niz). 3. Spoji: Nakon toga, pri "izvlačenju" iz rekurzije, izvrši spajanje sortiranih nizova koristeći funkciju merge(A,d,s,g). Implementacija ovog algoritma je jednostavna; void mergeSort(int *A, int d, int g) { if (d < r ) { /* temeljni slucaj - 1 element */ int s = (d + g) / 2; /* s je indeks podjele niza */ mergeSort(A, d, s); /* rekurzivno podijeli A[d..s] */ mergeSort(A, s+1, g); /* rekurzivno podijeli A[s+1..g]*/ merge(A, d, s, g); /* zatim spoji sortirane nizove */ 243
  • 244.
    } } Još treba definirati funkciju merge(). Ona se može realizirati na način da se formiraju dva pomoćna niza, donji[] i gornji[], u koje se kopira sortirane nizove A[d..s] i A[s+1..g]. Zatim se iz tih pomoćnih sortiranih nizova formira jedan sortirani niz u području ulaznog niza A[d..g]. Postupak je ilustriran na slici 5. Slika 17.5. Spajanje sortiranih nizova Implementacija je sljedeća: /* Spajanje podnizove A[d..s] i A[s+1..g] u sortirani niz A[d..g]. */ void merge(int *A, int d, int s, int g) { int m = s - d + 1; /* broj elemenata u A[d..s] */ int n = g - s; /* broj elemenata u A[s+1..g] */ int i; /* indeks u donji niz*/ int j; /* indeks u gornji niz*/ int k; /* indeks u orig. niz A */ int *donji = malloc(sizeof(int) * m); /* niz A[d..s] */ int *gornji = malloc(sizeof(int) * n); /* niz A[s+1..g] */ /* Kopiraj A[d..s] u donji[0..m-1] i A[s+1..g] u gornji[0..n-1]. */ for (i = 0, k = d; i < m; i++, k++) donji[i] = A[k]; for (j = 0, k = s+1; j < n; j++, k++) gornji[j] = A[k]; /* Usporedbom donji[0..m-1] i gornji[0..n-1], */ /* pomakni manji element na sljedeću poziciju u A[d..g]. */ i = 0; j = 0; k = d; while(i < m && j < n; ) if (donji[i] < gornji[j]) A[k++] = donji[i++]; else A[k++] = gornji[j++]; /* Preostale elemente jednostavno kopiraj */ /* Jedna od ove dvije petlje će imati nula iteracija! */ while (i < m) A[k++] = donji[i++]; while (j < n) A[k++] = gornji[j++]; 244
  • 245.
    /* Dealociraj memorijukoju zauzimaju donji i gornji. */ free(donji); free(gornji); } Operacija kopiranja iz pomoćnih nizova u sortirani niz A[d..g] provodi se jednostavnom usporedbom sadržaja donji[i] i gornji[j]. Kopira se manji element u A i inkrementira pozicija u nizu. Na taj način, ova se operacija vrši u linearnom vremenu O(g-d+1). Pomoćni nizovi su formirani alociranjem memorije, stoga se na kraju funkcije vrši oslobađanje memorije. U realnoj se primjeni može koristiti brži postupak, bez alociranja memorije, na način da se pomoćni nizovi deklariraju kao globalne varijable. Dimenzija ovih globalnih nizova mora biti veća od polovine dimenzije niza koji se sortira. Na sličan način se može provesti i sortiranje datoteka. Složenost metode spajanja podnizova Može se na jednostavan način pokazati da je vremenska složenost ovog algoritma jednaka O(n log2 n), ukoliko se uzme da je veličina niza potencija broja 2, tj. da je n = 2m. Pošto se pri svakom rekurzivnom pozivu niz dijeli na dva podniza, sve dok duljina podniza ne postane jednaka 1, proizlazi da je broj razina podijele niza jednak log2 n. Na k-toj razini niz je podijeljen na 2k podnizova duljine n/2k. To znači da spajanje sortiranih nizova na k-toj razini ima složenost 2kxO(n/2k)= O(n), a pošto ima log2 n razina, proizlazi da je ukupna složenost jednaka O(n log2 n). Do istog rezultata se dolazi i uz znatno rigorozniju analizu složenosti. Može se pokazati da je ovo najbolji rezultat koji se može postići pri sortiranju nizova. Jedini problem ovog algoritma je što zahtijeva povećanu prostornu složenost. 17.8.1 Quicksort Quicksort je najčešće korištena metoda sortiranja. Njome se u prosječnom slučaju postiže složenost O(n log2 n), a u najgorem slučaju O(n2). To je lošiji rezultat nego kod mergesort metode, međutim prostorna složenost je manja nego kod mergesort metode, jer se sve operacije vrše na ulaznom nizu. Quicksort algoritam koristi metodu podijeli pa vladaj u sljedećem obliku: Algoritam: Sortiraj niz A[d..g] PODIJELI. Izaberi jedan element iz niza A[d..g] i zapamti njegovu vrijednost. Taj element se naziva pivot. Nakon toga podijeli A[d..g] u dva podniza A[d..p] i A[g+1..d] koji imaju slijedeća svojstva: Svaki element A[d..p] je manji ili jednak pivotu. Svaki element A[p+1..g] je veći ili jednak pivotu. Niti jedan podniz ne sadrži sve elemente (odnosno ne smije biti prazan). VLADAJ. Rekurzivno sortiraj oba podniza A[d..p] i A[p+1..g], i problem će biti riješen kada oba podniza budu imala manje od 2 elementa. Uvjet da nijedan podniz ne bude prazan je potreban jer, kada to ne bi bilo ispunjeno, rekurzivni problem bi bio isti kao originalni problem, pa bi nastala bekonačna rekurzija. Podjela na podnizove, tako da budu ispunjeni postavljeni uvjeti, vrši se funkcijom int podijeli(int *A, int d, int g) koja vraća indeks podjele na podnizove. 245
  • 246.
    Podjela se vršipomoću dva indeksa (i, j) i pivota koji se proizvoljno odabire, prema pravilu: • Pomakni i od početka prema kraju niza, dok se ne nađe element A[i] koji je veći ili jednak pivotu, • Pomakni j od kraja prema početku niza, dok se ne nađe element A[j] koji je manji ili jednak pivotu, • Zatim zamijeni vrijednosti A[i] i A[j], kako bi svaki svaki element A[d..i] bio manji ili jednak pivotu, a svaki element A[j..g] veći ili jednak pivotu. Ovaj se proces nastavlja dok se ne dobije da je i > j. Tada je podjela završena, a j označava indeks koji vraća funkcija. Ovaj uvjet ujedno osigurava da nijedan podniz neće biti prazan. Postupak je ilustriran na slici 17.6. Slika 17.6. Postupak podjele niza A[1..6]. Za pivot je odabran prvi element A[1] vrijednosti 6. Označeni su indeksi (i,j), a potamnjeni elementi pokazuju koje se elemente zamjenjuje. Linija podjela je prikazana u slučaju kada indeks i postane veći od indeksa j. /* Podijeli A[d..g] u podnizove A[d..p] i A[p+1..g], * gdje je d <= p < g, i * A[d..p] <= A[p+1..g]. * Početno se izabire A[d] kao pivot * Vraća indeks podjele. */ int podijeli(int *A, int d, int g) { int i = d - 1; /* index lijeve strane A[d..g] */ int j = g + 1; /* index desne strane A[d..g] */ int pivot = A[d]; /* izbor pivot-a */ while (1) { /* Nadji sljedeći manji indeks j za koji je A[j] <= pivot. */ while (A[--j] > pivot) {} /* Nadji sljedeći veći indeks i za koji je A[i] >= pivot. */ while (A[++i] < pivot) {} /* Ako je i >= j, raspored je OK. */ 246
  • 247.
    /* inače, zamijeniA[i] sa A[j] i nastavi. */ if (i < j) swap(&A[i], &A[j]); else return j; } } Pomoću ove funkcije se iskazuje kompletni quicksort algoritam: /* Sortiraj niz A[d..g] - quicksort algoritmom * 1.Podijeli A[d..g] u podnizove A[d..p] i A[p+1..g], * d <= p < g, * i svaki element A[d..p] je <= od elementa A[p+1..g]. * 2.Zatim rekurzivno sortiraj A[d..p] i A[p+1..g]. */ void quicksort(int *A, int d, int g) { if (d < g) /* završava kada podniz ima manje od 2 elementa */ { int p = podijeli(A, d, g); /* p je indeks podjele */ quicksort(A, d, p); /* rekurzivno sortiraj A[d..p]*/ quicksort(A, p+1, g); /* rekurzivno sortiraj A[p+1..g]*/ } } Analiza složenosti quicksort algoritma Prvo treba uočiti da je vrijeme za podjelu podnizova od n elemenata, proporcionalno broju elemenata cn (konstanta c > 0), tj. složenost je O(n). Analizirajmo zatim broj mogućih rekurzivnih poziva. Najbolji slučaj U najboljem slučaju nizovi se dijele na dvije jednake polovine. Pošto je tada ukupan broj razlaganja jednak log2 n dobije se da je složenost O(n)⋅O(log2 n) = O(n log2 n). Najgori slučaj U najgorem slučaju podjela na podnizove se vrši tako da je u jednom podnizu uvijek samo jedan element. To znači da bi tada nastaje n rekurzivnih poziva, pa je ukupna složenost O(n) ⋅O(n)= O(n2). Najgori slučaj nastupa kada je niz sortiran ili "skoro" sortiran. Prosječni slučaj Analiza prosječnog slučaja je dosta komplicirana. Ovdje se neće izvoditi. Važan je zaključak da se u prosječnom slučaju dobija složenost koja je bliska najboljem slučaju O(n log2 n). Postavlja se pitanje: Kako odabrati pivot element? U prethodnom algoritmu za pivota je odabran početni element A[d]. U praksi se pokazalo da je znatno povoljnije izbor pivota vršiti po nekom slučajnom uzorku. Recimo, ako se slučajno 247
  • 248.
    odabere indeks ixi element A[ix] kao pivot, tada se smanjuje mogućnost da se dobije nepovoljna podjela kod skoro sortiranih nizova. Sljedećim postupkom se dobiva vjerojatnost dobre podjele 50%. Odaberu se tri indeksa po slučajnom uzorku, a zatim se za pivot odabere jedan od ova tri elementa koji je najbliži njihovoj srednjoj vrijednosti (medijan). Dublje opravdanje ove metode ovdje neće biti dano. Pokazalo se, da uz primjenu takovog postupka, quicksort predstavlja najbrži poznati način sortiranja nizova, pa je implementiran u standardnoj biblioteci C jezika. 17.9 Zaključak Glavna korist od korištenja rekurzije kao programske tehnike je: • rekurzivne funkcije su jasnije, jednostavnije, kraće, i lakše ih je razumjeti od odgovarajućih ne-rekurzivnih funkcija, jer najčešće neposredno odgovaraju apstraktnom algoritmu rješenja problema. Glavni nedostatak korištenja rekurzije kao programske tehnike je: • izvršenje rekurzivnih funkcija troši više računarskih resursa od odgovarajućih ne- rekurzivnih funkcija. U mnogo slučajeva rekurzivne se funkcije mogu transformirati u ne-rekurzivne funkcije. To se obavlja jednostavno u slučaju korištenja “repne rekurzije”. Analiza složenosti algoritama se uglavnom provodi analizom vremena izvršenja programa. Složenost algoritma se iskazuje “veliki-O” notacijom. U analizi složenosti algoritama često se koristi princip matematičke indukcije. On se postavlja na sličan način kao i rekurzivna pravila: definira se temeljni slučaj i pravilo indukcije (rekurzije). Kao primjer analize algoritama, analizirano je nekoliko metoda za sortiranje nizova. Pokazano je da se korištenjem metode “podijeli pa vladaj” dobivaju efikasni algoritmi (quicksort i mergesort). 248
  • 249.
    18 Samoreferentne strukturei liste Naglasci: • samoreferentne strukture • jednostruko vezana lista • dvostruko vezana lista • operacije s listom • sortirane liste • implementacija ADT STACK pomoću jednostruko vezane liste • implementacija ADT QUEUE pomoću jednostruko vezane liste • implementacija ADT DEQUEUE pomoću dvostruko vezane liste 18.1 Samoreferentne strukture i lista Karakteristika je strukturnih tipova da članovi strukture mogu biti bilo kojeg prethodno definiranog tipa. To znači da struktura ne može sadržavati "samu sebe", jer se ona tek definira. Dozvoljeno je pak da struktura sadrži pokazivač na "sebe". Takve strukture se nazivaju samoreferentne ili rekurzivne strukture. Razmotrimo strukturu node koja pored nekog elementa (elem) sadrži pokazivač (next) koji se može inicirati da pokazuje na objekt istog strukturnog tipa. struct node { int elem; struct node * next; } Primjerice, neka su definirama tri objekta (N1, N2, N3) i pokazivač List, tipa structnode: struct node N1, N2, N3; struct node *List; tada je moguće inicirati pokazivač List tako da pokazuje na bilo koji od ova tri objekta. Uzet ćemo da pokazivač List pokazuje na objekt N1: List = &N1; Pošto objekti N1, N2 i N3 sadrže član next, koji je pokazivač tipa struct node, može se uspostaviti veza između objekata, tako da se pokazivaču next jednog objekta pridijeli adresa drugog objekta. Primjerice, sljedećim naredbama se definira sadržaj i veza sa drugim objektom. N1.elem = 1; N2.elem = 2; N3.elem = 3; N1.next = &N2; /* prvi povezujemo s drugim */ N2.next = &N3; /* drugi povezujemo s trećim */ N3.next = NULL; /* treći element ostavimo nepovezanim */ 249
  • 250.
    To stanje semože ilustrirati sljedećom slikom: Slika 18.1 Vezana lista Ovakova struktura se naziva vezana lista. Sastoji se od međusobno povezanih objekata koji se nazivaju čvorovi (eng. node), jer ilustracija prikazuje graf koji se sastoji od čvorova i usmjerenih veza. Prvi čvor vezane liste se obično naziva glava liste. Njegovu adresu se bilježi u pokazivaču List. Posljednji čvor se naziva kraj liste. Pokazivač next tog čvora se uvijek postavlja na vrijednost NULL, što služi kao oznaka kraja liste. Strukturalna definicija: Vezana lista skup elemenata, koji je realiziran na način da je svaki element dio čvora, koji sadrži i vezu sa sljedećim čvorom. Prazna lista ne sadrži ni jedan čvor. Do sada je pojam liste korišten za označavanje uređenog skupa elemenata A={a1,a2,...an}. Uređenost skupa je određena redom elemenata liste. Pojam liste se sada koristi i za označavanje strukture podataka koju se može definirati rekurzivno: Apstraktna definicija: Lista je linearna struktura podataka koja sadrži nula ili više elemenata, a ima dvije karakteristike: 1. Lista može biti prazna 2. Lista se sastoji od elementa liste iza kojeg slijedi lista elemenata Prema ovoj definiciji lista se može realizirati kao niz elemenata ili kao vezana lista. Listi, koja se formira kao niz elemenata, jednostavno se pristupa elementima liste pomoću indeksa niza. Znatno je kompliciraniji postupak ako treba umetnuti ili izbrisati element liste. Bit će pokazano da se te operacije, pri korištenju vezane liste, izvode brzo i jednostavno. Još je jedna bitna razlika između niza i vezane liste. Nizovi zauzimaju fiksnu veličinu memorije, a veličina vezane liste (i zauzeće memorije), je određena brojem elemenata liste. Ako se umetne element, vezana lista se smanjuje, a ako se izbriše element vezana lista se smanjuje. Zbog ovog svojstva vezana lista je dinamička struktura podataka, a zbog svojstva da služi prikupljanju podataka često se kaže da predstavlja kolekciju elemenata. 18.2 Operacije s vezanom listom Cilj je pokazati kako se vrše operacije s vezanom listom kao dinamičkom strukturom podataka. Najprije će biti pokazano kako se formira lista cijelih brojeva. Polazi se od definirane strukture “node”, koja sadrži jedan element kolekcije i vezu kojom se pristupa elementima kolekcije, u sljedećem obliku: 250
  • 251.
    typedef int elemT; typedef struct node Node; typedef Node *LIST; struct node { elemT elem; /* element liste */ Node *next; /* pokazuje na slijedeći čvor */ } Tip Node označava čvor liste, a tip LIST označava pokazivač na čvor liste. Deklaracija LIST List = NULL; inicijalizira varijablu List koja, prema apstraktnoj definiciji, predstavlja praznu listu, a programski predstavlja pokazivač koji na čvor koji sadrži glavu liste. Zvat ćemo ga pokazivač liste. Proces stvaranja vezane liste počinje stvaranjem čvora vezane liste. Formira se alociranjem memorije za strukturu node. List = malloc(sizeof (Node)); Nakon toga se upisuje vrijednost elementa liste, primjerice List->elem = 5; Upitnik označava da nije definiran sadržaj pokazivača List->next. Uobičajeno je da se on postavi na vrijednost NULL, jer pokazuje na “ništa”. Time se ujedno označava kraj liste (po analogiju sa stringovima, gdje znak '0' označava kraj stringa). To simbolički prikazujemo slikom: List->next = NULL; Sada se u ovu kolekciju može dodati još jedan element, sljedećim postupkom. Prvo, se formira čvor kojem pristupamo pomoću pokazivača q. Node *q=malloc(sizeof(Node)); q->elem =7; Zatim se ova dva čvora “povežu” sljedećim naredbama: q->next = List; 251
  • 252.
    List = q; Novi element je postavljen na početak liste. Pokazivač q više nije potreban jer on sada ima istu vrijednost kao pokazivač List (q se koristi samo kao pomoćni pokazivač za formiranje vezane liste). Na ovaj se način može formirati lista s proizvoljnim brojem članova. Početak liste (ili glava liste) je zabilježen u pokazivaču kojeg se tradicionalno naziva List. Kraj liste se je obilježen s NULL vrijednošću “next” pokazivača krajnjeg čvora liste. U ovom primjeru lista je formirana tako da se novi čvor postavlja na glavu liste. Kasnije će biti pokazano kako se novi čvor može dodati na kraj liste ili umetnuti između dva čvora liste. Sada će postupak formiranja i manipuliranja s listom biti opisan s nekoliko temeljnih funkcija, koje se mogu lako prilagoditi različitim tipovima elementa liste. Imena funkcija koje su neovisne o tipu elementa liste bit će zapisana malim slovima. Ostale funkcije, čija imena sadrže velika slova, ovise o tipu elementa liste. Kasnije će biti pokazano kako se te funkcije prilagođavaju tipu elementa liste. 18.2.1 Formiranje čvora liste U radu s listom, za formiranje novog čvora liste koristit će se funkcija newNode().Ona prima argument tipa elemT, a vraća pokazivač na alocirani čvor ili NULL ako se ne može izvršiti alociranje memorije. Node *newNode(elemT x) { Node *n = malloc(sizeof(Node)); if(n != NULL) n->elem = x; return n; } Također, potrebno je definirati funkciju freeNode(), kojom se oslobađa memorija koju zauzima čvor. void freeNode(Node *p) { /* ako se element liste formira alociranjem memorije * tada ovdje treba dodati naredbu za dealociranje * elementa liste. */ free(p); /* dealociraj čvor liste */ } Uočite da implementacija ove funkcije ovisi o definiciji elementa liste. Ako se element liste formira alociranjem memorije, primjerice, ako je element liste dinamički string, tada u ovu funkciju treba dodati naredbu za dealociranje elementa liste. 18.2.2 Dodavanje elementa na glavi liste Prethodno opisani postupak formiranja liste, na način da se novi čvor umeće na glavu liste, implementiran je u funkciji koja se naziva add_front_node(). void add_front_node(LIST *pList, Node *n) { if(n != NULL) /* izvrši samo ako je alociran element */ { 252
  • 253.
    n->next = *pList; *pList = n; } } Prvi argument funkcije je pokazivač na pokazivač liste. Ovakvi prijenos argumenta je nužan jer se u funkciji mijenja vrijednost pokazivača liste. Drugi argument je pokazivač čvora koji se unosi u listu. Dodavanje elementa x u listu List sada se vrši jednostavnim pozivom funkcije: add_front_node(&List, newNode(x)); Složenost ove operacije je O(1). 18.2.3 Brisanje čvora na glavi liste Brisanje čvora glave liste je jednostavna operacija. Njome čvor, koji slijedi iza glave liste, (List->next) postaje glava liste, a trenutni čvor glave liste (n) se dealocira iz memorije. To ilustrira slika 18.2. Node *n = List; List = List->next; freeNode(n); Slika 18.2 Brisanje s glave liste Postupak brisanja glave liste se formalizira funkcijom delete_front_node(). void delete_front_node(LIST *pList) { Node *n = *pList; if(n != NULL) { *pList = *pList->next; freeNode(n); } } Uočite da se i u ovoj funkciji mijenja vrijednost pokazivača glave liste, stoga se u funkciju prenosi pokazivač na pokazivač liste. Složenost ove operacije je O(1). 18.2.4 Brisanje cijele liste Brisanje cijele liste se vrši funkcijom delete_all_nodes(), na način da se brišu svi čvorovi sprijeda. void delete_all_nodes( LIST *pList ) { while (*pList != NULL) delete_front_node(pList); } 253
  • 254.
    18.2.5 Obilazak liste Ako je poznat pokazivač liste uvijek se može odrediti pokazivač na sljedeći element pomoću “next” pokazivača. Node *ptr = List->next; Dalje se sukcesivno može usmjeravati pokazivač ptr na sljedeći element liste naredbom ptr = ptr->next; Na taj se način može pristupiti svim elementima liste. Taj postupak se zove obilazak liste (eng. list traversing). Obilazak liste završava kada je ptr == NULL. Primjer: Pretpostavimo da želimo ispisati sadržaj liste redoslijedom od glave prema kraju liste. To možemo ostvariti sljedećim programom: Node *p = List; /* koristi pomoćni pokazivač p */ while (p != NULL) /* ponavljaj do kraja liste */ { printf("%dn", p->elem); /* ispiši sadržaj elementa */ p = p->next; /* i postavi pokazivač na */ /* sljedeći element liste */ } U ovom primjeru na sve elemente liste je primijenjena ista funkcija. Postupak kojim se na sve elemente liste djeluje nekom funkcijom može se poopćiti funkcijom list_for_each() u sljedećem obliku: void list_for_each(LIST L, void (*pfun)(Node *)) { while( L != NULL) { (*pfun)(L); L = L->next; } } Prvi argument ove funkcije je lista, a drugi argument ove funkcije je pokazivač na funkciju, koja se primijenjuje na sve elemente liste. To može biti bilo koja void funkcija kojoj je argument tipa Node *. Definiramo li funkciju: void printNode(Node *n) { printf("%dn", n->elem);} tada poziv funkcije: list_for each(L, printNode); ispisuje sadržaj cijele liste Kada je potrebno izvršiti obilazak liste od kraja prema glavi liste, ne može se primijeniti iterativni postupak. U tom slučaju se može koristiti rekurzivna funkcija reverse_list_for_each(). void reverse_list_for_each(LIST L, void (*pfun)(Node *)) { if (L == NULL) return; reverse_list_for_each(L->next, pfun); (*pfun)(L); 254
  • 255.
    } Obilazak liste jenužan i kada treba odrediti posljednji čvor liste. To vrši funkcija last_node(). Node *last_node(LIST L) {/*vraća pokazivač na krajnji čvor liste*/ if(L == NULL) return NULL; while ( L->next != NULL) L = L->next; return L; } Broj elemenata koji sadrži lista daje funkcija num_list_elements(). int num_list_elements(LIST L) { int num = 0; while ( L != NULL) { num++; L = L->next; } return num; /* broj elemenata liste */ } 18.2.6 Traženje elementa liste Često je razlog za obilazak liste traženje elementa liste. U slučaju kada je element liste prostog skalarnog tipa može se koristiti funkciju find_list_element(). /* funkcija: find_list_element * ispituje da li lista sadrži element x * Argumenti: * x – element kojeg se traži * List – pokazivač na glavu liste * Vraća: * pokazivač na čvor koji sadrži x, ili NULL ako x nije u listi */ Node *find_list_element(LIST L, elemT x) { while( L != NULL && p->elem != x ) L = L->next; return L; } Pretraživanje liste ima složenost O(n), gdje je n broj elemenata liste. 18.2.7 Dodavanje čvora na kraju liste Dodavanje čvora na kraju liste vrši se na sljedeći način: Ako lista još nije formirana, tj. ako je List==NULL, koristi se postupak opisan u funkciji add_front_node(). Ako je List != NULL, tada 1. Obilaskom liste odredi se pokazivač krajnjeg čvora liste. Taj pokazivač, nazovimo ga p, ima karakteristiku da je p->next == NULL. 255
  • 256.
    2. Zatim sep->next postavi na vrijednost pokazivača čvora kojeg se dodaje u listu. 3. Pošto dodani čvor predstavlja novi kraj liste njega se zaključuje s NULL . Ovaj postupak je realiziran funkcijom add_back_node(); /* funkcija: add_back_node * --------------------- * Dodaje čvor na kraj liste * Argumenti: * pList - pokazivač na pokazivač liste * n – pokazivač na čvor koji se dodaje u listu */ void add_back_node(LIST *pList, Node *n) { if(n == NULL) /* Izvršava se samo ako je */ return; /* alociran čvor. */ if(*pList == NULL) { /* Ako lista nije formirana */ *pList = n; /* iniciraj pokazivač */ n->next = NULL; } else { LIST p = *pList; while ( p->next != NULL) /* 1. odredi krajnji čvor */ p = p -> next; p ->next = n; /* 2. dodaj novi čvor */ n->next = NULL; /* 3. označi kraj liste */ } } Ovu funkciju se koristi po istom obrascu kao i funkciju add_front_node(), tj. novi element (x) se dodaje naredbom: add_back_node(&List, newNode(x)); 18.2.8 Umetanje i brisanje čvora unutar liste Postupak umetanja ili brisanja čvora n ilustrira slika 18.3 Slika 18.3 Umetanje i brisanje čvora unutar vezane liste Brisanje čvora koji slijedi iza čvora “p” ( na slici, to je čvor “n”) vrši se naredbama: 256
  • 257.
    n = p->next; /* odredi sljedeći */ p->next = n->next; freeNode(n); Ako treba izbrisati čvor “n”, potrebno je odrediti čvor “p”, koji mu prethodi. p = /* odredi čvor koji prethodi čvoru n*/ p->next = n->next; freeNode(n); Umetanje čvora “n” iza čvora “p” se provodi naredbama: n->next = p->next; p->next = n; Umetanje čvora “n” iza čvora “x” provodi se tako da se najprije odredi čvor “p” koji prethodi čvoru “x”, a zatim se provodi prethodni postupak. Operaciju kojom se određuje čvor koji prethodi čvoru “n” realizira se funkcijom get_previous_node(), koja vraća pokazivač na prethodni čvor, ili NULL ako ne postoji prethodni čvor. Node *get_previous_node(LIST List, Node *n ) { Node *t, *pre; t = pre = List; /* start od glave */ while( (t != n) /* dok ne pronađeš n */ && (t->next != NULL )) { pre = t; /* pamti prethodni */ t = t->next ; } return (pre); /* vrati prethodni */ } Sada se postupak brisanja čvora može realizirati funkcijom delete_node(). Njome se iz liste briše čvor “n”. void delete_node(LIST *pList, Node *n) { if(*pList == NULL || n == NULL) return; if(*pList == n) { /* ako n pokazuje glavu */ *pList = n->next; /* odstrani glavu */ } else { Node *pre = get_previous_node(*pList, n ); pre->next = n->next; } freeNode(n); /* dealociraj čvor */ } 18.2.9 Brisanje čvora na kraju liste Brisanje čvora na kraju liste je jednako komplicirana operacija kao i brisanje unutar liste, jer se i u ovom slučaju mora odrediti čvor koji mu prethodi. To je realizirano funkcijom delete_back_node(). /* Funkcija: delete_back_node 257
  • 258.
    * odstranjuje čvorna kraju liste * Argumenti: * pList - pokazivač na pokazivač liste. */ void delete_back_node(LIST *pList) { Node *pre, back; /* pre – prethodni */ if (*pList == NULL) /* back – krajnji */ return; back = pre = *pList; /* start od glave */ while(back->next != NULL ) { /* pronađi kraj liste*/ pre = back; /* zapamti prethodni */ back = back->next ; } if(back == *pList) /* ako je krajnji = glava */ *pList == NULL; /* napravi praznu listu */ else /* inače */ pre->next = NULL; /* prethodni postaje krajnji*/ freeNode(back); /* dealociraj čvor */ } Primjer: U program testlist.c testiraju se prije opisane funkcije. void printNode(Node *n) { if(n) printf( "%d ", n->elem ); } void printList(LIST List) { if( L == NULL ) printf( "Lista je praznan" ); else list_for_each(L, printNode); printf( "n" ); } int main( ) { LIST List; int i; /* obvezno iniciraj listu */ List = NULL ; /* formiraj listu s 10 cijelih brojeva */ for( i = 0; i < 10; i++ ) { add_front_node(&List, newNode(i)); printList(List); } /* izbriši prednji i stražni element */ delete_front_node(&List); delete_back_node(&List); printList(List); if(find_list_element(List, 5) != NULL) printf( "pronadjen element :%dn", 5 ); 258
  • 259.
    if(find_list_element(List, 9) ==NULL) printf( "Nije pronadjen element :%dn", 9 ) ; add_back_node(&List, newNode(9)); printList(List); delete_all_nodes(&List) ; printList(List); return 0; } Nakon izvršenja dobije se ispis: 0 1 0 2 1 0 3 2 1 0 4 3 2 1 0 5 4 3 2 1 0 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 8 7 6 5 4 3 2 1 pronadjen element :5 Nije pronadjen element :9 8 7 6 5 4 3 2 1 9 Lista je prazna 18.3 Što može biti element liste Funkcije koje su u prethodnom poglavlju korištene za rad s vezanom listom mogu se, u neizmijenjenom obliku, primijeniti jedino na listu cijelih brojeva. Sada će biti pokazano kako se te funkcije mogu prilagoditi za rad s listama koje sadrže proizvoljan tip podataka. Uzmimo za primjer da u listi treba zapisati podatke o studentima: 1) njihovo ime i 2) ocjenu. To se može realizirati na dva načina: Prvi je način da se čvor liste formira tako da sadrži više različitih tipova podataka, primjerice: typedef struct snode StudentNode; typedef StudentNode *STUDENTLIST; typedef struct snode { StudentNode *next; char *ime; int ocjena; } StudentNode; Drugi je način da se element liste definira strukturom koja ima više članova, primjerice: typedef struct student_info { char *ime; int ocjena; }Student; struct snode 259
  • 260.
    { StudentNode *next; /* pokazuje na slijedeći čvor */ Student elem; /* element liste tipa Student */ }; U oba slučaja lista se formira istim postupkom kao i lista stringova, jedino je potrebno modificirati funkcije newNode(), freeNode(), Find() i Print(). Primjerice, funkcija newNode() u ovom slučaju ima dva argumenta: ime i ocjenu, a realizira se na sljedeći način. Prvi način Drugi način StudentNode StudentNode *newNode(char *ime, int ocjena) *newNode(char *ime, int ocjena) { { StudentNode *novi = StudentNode *novi = malloc(sizeof(StudentNode)); malloc(sizeof(StudentNode)); if(novi != NULL) { if(novi != NULL) { novi->ime = strdup(ime); novi->elem.ime = strdup(ime); novi->ocjena = ocjena; novi->elem.ocjena = ocjena; } } return novi; return novi; } } 18.4 Lista sa sortiranim redoslijedom elemenata U dosadašnjim primjerima podaci su bili svrstani u listi redoslijedom kojim su i uneseni u listu. Često je pak potrebno da podaci u listi budu u sortiranom redoslijedu. To se može postići na dva načina. Prvi je način da se izvrši sortiranje liste, a drugi je način da se već pri samom unošenju podataka ostvari sortirani redoslijed elemenata. S obzirom da je namjena liste da služi kao kolekcija u koju se često unose i odstranjuju elementi, ovaj drugi pristup se češće koristi. Izrada liste s podacima o imenu i ocjeni studenta je tipičan primjer korištenja liste u kojem je poželjno imati sortiran redoslijed elemenata. Sada će biti prikazana izvedba modula za manipuliranje s listom studenata. Prema uputama za formiranje modula, iznesenim u poglavlju 9, modul za listu studenata će se formirati od sljedećih datoteka: 1. datoteka specifikacije modula ("studentlist.h") sadrži deklaracije funkcija i struktura koje se koriste za rad s listom. 2. datoteka implementacije ("studentlist.c") sadrži definicije varijabli i implementaciju potrebnih funkcija. 3. datoteka primitivnih funkcija za manipuliranje listom ("listprim.c") koje su definirane u prethodnom poglavlju. Ova datoteka će se koristiti isključivo kao #include datoteka u datoteci "studentlist.c". Sve funkcije iz ove datoteke su označene prefiksom static, što znači da će biti vidljive samo u datoteci "studentlist.c". 4. Testiranje modula se provodi programom studlist-test.c. Datoteka specifikacije –"studentlist.h" #ifndef _STUDENTLIST_H_ #define _STUDENTLIST_H_ /* sortirana lista za evidenciju studenata */ typedef struct stnode StudentNode; typedef StudentNode *STUDENTLIST; 260
  • 261.
    struct stnode { StudentNode *next; char *ime; int ocjena; }; void StudentL_insertSorted (STUDENTLIST *pList, char *ime, int ocjena); /* U listu umeće podatke o studentu: ime i ocjenu * Ako ime već postoji, zamjenuje se vrijednost ocjene. * Lista je uvijek sortirana prema imenu studenta */ StudentNode *StudentL_find(STUDENTLIST L, char *s); /* Traži čvor liste u kojem je ime jednako stringu s * Vraća pokazivač na čvor, ili NULL ako nije pronađen string */ void StudentL_deleteNode(STUDENTLIST *pList, StudentNode *n); /* Odstranjuje čvor liste na kojeg pokazuje n */ void StudentL_delete(STUDENTLIST *pList) ; /* Odstranjuje sve čvorove liste */ int StudentL_size(STUDENTLIST List); /* Vraća broj elemanate liste */ void StudentL_print(STUDENTLIST L ); /* Ispisuje sadržaj liste */ #endif Sve deklaracije su napisane unutar makro naredbi #ifndef _STUDENTLIST_H_ #define _STUDENTLIST_H_ ........ deklaracije ..... #endif To osigurava da se u jednoj izvornoj datoteci samo jedan put može umetnuti h-datoteka. Čvor liste je opisan strukturom StudentNode, a tip pokazivač na taj čvor je nazvan STUDENTLIST. Imena svih funkcija počinju prefiksom StudentL_ , a ostatak imena je u skladu s imenom ekvivalentnih primitivnih funkcija koje su definirane u prethodnom poglavlju. Datoteka implementacije – "studentlist.c" #include <stdio.h> #include <stdlib.h> #include "studentlist.h" static StudentNode *newNode(char *ime, int ocjena) { StudentNode *n = malloc(sizeof(StudentNode)); if(n != NULL) { n->ime=strdup(ime); 261
  • 262.
    n->ocjena = ocjena; } return n; } static void freeNode(StudentNode *n) { if(n) { free(n->ime); free(n); } } #define LIST STUDENTLIST #define Node StudentNode #include "listprim.c" #undef LIST #undef Node int StudentL_size(STUDENTLIST List) { return num_list_elements(List); } void StudentL_deleteNode(STUDENTLIST *pList, StudentNode *n) { delete_node(pList, n); } void StudentL_delete(STUDENTLIST *pList) { delete_all_nodes(pList); } void StudentL_insertSorted (STUDENTLIST *pList, char *ime, int ocjena) { StudentNode *tmp = *pList; StudentNode *pre = NULL; StudentNode *n; int cmp; if (!*pList) { /* ako je lista prazna, formiraj prvi čvor */ n= newNode (ime, ocjena); n->next=NULL; *pList = n; return; } /* nađi zadnji čvor (i njemu prethodni) u kojem je ime manje ili jednako zadanom imenu, ili kraj liste */ cmp = strcmp(ime, tmp->ime); while ((tmp->next) && (cmp > 0)) { pre = tmp; tmp = tmp->next; cmp = strcmp(ime, tmp->ime); } /* ako ime već postoji, zamijeni ocjenu */ if(cmp == 0) { tmp->ocjena = ocjena; return; } 262
  • 263.
    /*inače dodaj novičvor */ n= newNode (ime, ocjena); n->next=NULL; /* ako je dosegnut kraj liste, dodaj na kraj liste */ if ((tmp->next == NULL) && (cmp > 0)) { tmp->next = n; return; } /*ili umetni iza prethodnog*/ if (pre != NULL) { pre->next = n; n->next = tmp; } /*ili stavi na početak liste ako je prethodni == NULL*/ else { n->next = *pList; *pList = n; } } StudentNode *StudentL_find(STUDENTLIST L, char *ime) { while( L != NULL && strcmp(L->ime, ime) ) L = L->next; return L; } void PrintStInfo(StudentNode *n ) { printf( "Ime:%s, ocjena:%d n", n->ime, n->ocjena ); } void StudentL_print(STUDENTLIST L ) { if( L == NULL ) printf( "Lista je praznan" ); else list_for_each(L, PrintStInfo); printf( "n" ); } Izvedba svih funkcija je izvršena po poznatom obrascu, jedino je potrebno objasniti algoritam za sortirano umetanje čvorova liste. On glasi: Algoritam: Sortirano umetanje čvora liste (ime,ocjena) Ako je lista prazna, Formiraj čvor s zadanim imenom i ocjenom i stavi ga na početak liste inače Nađi zadnji čvor (i njemu prethodni) u kojem je ime leksikografski manje ili jednako zadanom imenu, ili kraj liste Ako čvor s zadanim imenom već postoji, Zamijeni ocjenu inače Formiraj čvor s zadanim imenom i ocjenom. Ako je dosegnut kraj liste Dodaj na kraj liste 263
  • 264.
    inače Umetni iza prethodnog ili stavi na početak liste ako je prethodni == NULL Testiranje modula se provodi programom studlist_test.c. /* Datoteka: studlist_test.c */ #include <stdio.h> #include <stdlib.h> #include "studentlist.h" int main( ) { STUDENTLIST List, n; int i; List = NULL ; StudentL_insertSorted (&List, "Bovan Marko", 5); StudentL_insertSorted (&List, "Radic Jure", 2); StudentL_insertSorted (&List, "Marin Ivo", 3); StudentL_insertSorted (&List, "Bovan Marko", 2); printf("Lista ima %d elemenatan", StudentL_size(List)); StudentL_print(List ); n = StudentL_find(List, "Bovan Marko"); if(n != NULL) StudentL_deleteNode(&List, n); StudentL_print(List); StudentL_delete(&List) ; return 0; } Program se kompilira komandom: c:> cl studlist_test.c studentlist.c Nakon izvršenja, dobije se sljedeći ispis: Lista ima 3 elemenata Ime:Bovan Marko, ocjena:2 Ime:Marin Ivo, ocjena:3 Ime:Radic Jure, ocjena:2 Ime:Marin Ivo, ocjena:3 Ime:Radic Jure, ocjena:2 Zadatak: Objasnite zašto u prethodnom ispisu lista početno ima tri elementa, iako je u programu četiri puta primijenjena funkcija StudentL_insertSorted(). Zadatak: Napišite program u kojem korisnik unosi rezultate ispita (ime i ocjenu studenta) u sortiranu listu. Unos završava kada se otkuca “prazno ime” ili ocjena 0. Nakon toga treba 264
  • 265.
    rezultate ispita upisatiu tekstualnu datoteku, na način da se u jednom retku ispiše redni broj, ime i ocjena studenta. 18.5 Implementacija ADT STACK pomoću linearne liste U poglavlju 16 izvršena je implementacija ADT STACK pomoću niza elemenata. Stog se može efikasno implementirati i pomoću linearne liste. Uzmemo li da je specifikacija ADT STACK ista kao i specifikacija ADT za implementaciju pomoću niza u datoteci "stack.h", koja je opisana u poglavlju 16, implementacija se može napraviti na način kako je opisano u datoteci "stack-list.c". Sada STACK predstavlja pokazivač na strukturu stack, koja ima samo jedan element, pokazivač na čvor linearne liste. Taj pokazivač je nazvan top, i on pokazuje na glavu liste. Operacija Push() je implementirana kao umetanje elementa na glavu liste, a pop() kao brisanje elementa s glave liste. /* Datoteka: stack-list.c */ /* Implementacija ADT STACK pomoću vezane liste */ #include <stdlib.h> #include <assert.h> #include "stack.h" /* typedef int stackElemT; definiran u stack.h*/ /* typedef struct stack *STACK; definiran u stack.h*/ struct node { stackElemT elem; struct node *next; }; struct stack { struct node *top; }; static void stack_error(char *s) { printf("nGreska: %sn", s); exit(1); } STACK stack_new(void) {/*alocira ADT STACK*/ STACK S = malloc(sizeof(struct stack)); if(S != NULL) S->top = NULL; return S; } void stack_free(STACK S) {/*dealocira ADT STACK*/ struct node *n; assert(S != NULL); if( !stack_empty(S)) for (n = S->top; n != NULL; n = n->next) free(n); } int stack_empty(STACK S) 265
  • 266.
    {/* vraća !=0 ako je stog prazan*/ assert(S != NULL); return ( S->top==NULL); } unsigned stack_count(STACK S) {/*vraća broj elemenata na stogu*/ unsigned num = 0; struct node *n; assert(S != NULL); if( !stack_empty(S)) for (n = S->top; n != NULL; n = n->next) num++; return num; } stackElemT stack_top(STACK S) {/*vraća vrijednost elementa na vrhu stoga*/ assert(S != NULL); return S->top->elem; } stackElemT stack_pop(STACK S) {/* Skida čvor s vrha stoga */ stackElemT el; struct node *n; assert(S != NULL); if (stack_empty(S)) stack_error("Stog je prazan"); n = S->top; el = n->elem; S->top = n->next; free(n); return el; } void stack_push(STACK S, stackElemT el) {/* Ubacuje čvor na vrh stoga*/ struct node *n; assert(S != NULL); n = malloc(sizeof(struct node)); if (n != NULL) { n->elem = el; n->next = S->top; S->top = n; } else printf(" Nema dovoljno memorije!n"); } Testiranje ove implementacije se provodi s programom stack-test.c, koji je opisan u poglavlju 16. Jedina razlika je da se u u tom programu umjesto uključenja datoteke "stack-arr.c" uključi datoteka "stack-list.c". Zadatak: Napišite ADT imena STRSTACK za stog na kojeg će se stavljati stringovi. Vodite računa da treba alocirati i dealocirati memoriju koju zauzima string. Napišite program za testiranje ADT STRSTACK. 266
  • 267.
    18.6 Implementacija ADTQUEUE pomoću vezane liste Red se može jednostavno implementirati i pomoću vezane liste. Za implementacija reda pomoću vezane liste koristit će se prethodna specifikaciju ADT-a QUEUE (danu u datoteci "queue.h"). U ovoj implementaciji QUEUE predstavlja pokazivač na strukturu queue, koja ima dva člana: front i back. To su pokazivači na prednji i stražnji element liste. Operacijom put() stavlja se element na kraj liste, a operacijom get() skida se element s glave liste. Slika 18.4 Operacije s listom koju se koristi kao red za čekanje /* Datoteka: queue-list.c */ /* Queue realiziran pomoću vezane liste*/ #include <stdio.h> #include <stdlib.h> #include <assert.h> #include "queue.h" /* typedef int queueElemT; */ /* typedef struct queue *QUEUE; */ struct node { queueElemT elem; struct node *next; }; struct queue { struct node * front; struct node * back; }; QUEUE queue_new(void) { /*alocira ADT QUEUE*/ QUEUE Q = malloc(sizeof(struct queue)); if(Q != NULL){ Q->front = Q->back = NULL; } return Q; } void queue_free(QUEUE Q) {/*dealocira ADT QUEUE*/ struct node *n; assert(Q != NULL); while ((n = Q->front) != NULL ) { Q->front = n ->next; free(n); } 267
  • 268.
    free(Q); } int queue_empty(QUEUE Q) {/*vraća != 0 ako je red prazan*/ assert(Q != NULL); return Q->front == NULL; } int queue_full(QUEUE Q) { /* vraća != 0 ako je red popunjen*/ /* uvijek možemo dodati element u listu */ return 0; } void queue_put(QUEUE Q, queueElemT el) {/* stavlja element el u red Q*/ struct node * n; assert(Q != NULL); n = malloc(sizeof(struct node)); if (n != NULL) { n->elem = el; n->next = NULL; if (queue_empty(Q)) Q->front = n; else Q->back->next = n; Q->back = n; } } queueElemT queue_get(QUEUE Q) {/* vraća element iz reda Q*/ queueElemT el; struct node * n; assert(Q != NULL); n = Q->front; el = n->elem; Q->front = n->next; if (Q->front == NULL) Q->back = NULL; free(n); return el; } unsigned queue_count(QUEUE Q) {/*vraća broj elemenata reda*/ unsigned num = 0; struct node *n; assert(Q != NULL); if( !queue_empty(Q)) for (n = Q->front; n != NULL; n = n->next) num++; return num; } void queue_print(QUEUE Q) { int i; struct node *n; assert(Q != NULL); n = Q->front; 268
  • 269.
    if( Q->front ==NULL ) printf( "Red je prazann" ); else do { printf( "%d, ", n->elem ); n = n->next; } while( n != NULL ); printf( "n" ); } Testiranje ove implementacije se provodi s programom queue-test.c, opisanim u pogavlju 16, tako da se u njemu umjeto uključenja datoteke "queue-arr.c" uključi datoteka "queue- list.c". 18.7 Dvostruko vezana lista Ukoliko se čvor liste formira tako da sadrži dva pokazivača, next - koji pokazuje na sljedeći čvor i prev - koji pokazuje na prethodni čvor, dobije se dvostruko vezana lista. Realizira se pomoću sljedeće strukture podataka: typedef int elemT; typedef struct node Node; typedef Node *DLIST; struct node { elemT elem; /* element liste */ Node *next; /* pokazuje na sljedeći čvor */ Node *prev; /* pokazuje na prethodni čvor */ } Slika 18.5 Dvostruko vezana lista Veze među čvorovima ilustrira slika 18.5. Karakteristike prikazane liste su: 1. Pokazivač next krajnjeg elementa i pokazivač prev glave liste su jednaki NULL. 2. Ovakvu listu se može iterativno obilaziti u oba smjera, od glave prema kraju liste (korištenjem pokazivača next) i od kraja prema početku liste (korištenjem pokazivača prev). 3. Umetanje unutar liste i brisanje iz liste se provodi jednostavnije i brže nego kod jednostruko vezane liste, jer je u svakom čvoru poznat pokazivač na prethodni čvor. 4. U odnosu na jednostruko vezanu listu, dvostruko vezana lista zauzima više memorije (zbog pokazivača prev) . 269
  • 270.
    5. Ako sevrši umetanje i brisanje čvorova samo na početku liste, tada je bolje rješenje koristiti jednostruko vezanu listu. Kroz dva primjera bit će pokazano kako se implementira dvostruko vezana lista. Primjer: Prikana je implementacija funkcija za umetanje čvora na glavi liste (dlist_add_front_node) i odstranjivanje čvora na glavi liste (dlist_delete_front_node). Uočite da je potrebno izvršiti nešto više operacija nego kod jednostruko vezane liste. void dlist_add_front_node(DLIST *pList, Node *n) { if(n != NULL) /* izvrši samo ako je alociran čvor */ { n->next = *pList; n->prev = NULL; if(*pList != NULL) (*pList)->prev = n; *pList = n; } } void dlist_delete_front_node(DLIST *pList) { Node *tmp = *pList; if(*pList != NULL) { *pList = (*pList)->next; if(*pList != NULL) (*pList)->prev = NULL; freeNode(tmp); } } Primjer: Prikazana je implementacija funkcija za brisanje čvora unutar liste (dlist_delete_node). Uočite da se ova operacija izvršava efikasnije nego kod primjene jednostruko vezane liste. void dlist_delete_node(DLIST *pList, Node *n) { if(*pList == NULL || n == NULL) return; if(*pList == n) { /* ako n pokazuje glavu */ *pList = n->next; /* odstrani glavu */ *plist->prev = NULL; } else { n->prev->next = n->next; if(n->next != NULL) n->next->prev = n->prev; } freeNode(n); /* dealociraj čvor */ } Zadatak: Napišite funkciju za brisanje i umetanje čvora na kraju liste. Za testiranje implementacije koristite isti program kao kod jednostruko vezane liste. 270
  • 271.
    Napomena: Funkcije zaformiranje čvora, brisanje liste i traženje elementa liste su iste kao kod jednostruko vezane liste. 18.8 Generički dvostrani red - ADT DEQUEUE Dvostruka se lista može iskoristiti za efikasnu implementaciju dvostranog reda (eng. double ended queue) u obliku ADT DEQUEUE. Temeljne operacije s dvostranim redom - DEQUEUE su: front() - vraća element na glavi liste push_front(el) - stavlja element el na glavu liste pop_front() - skida element s glave liste back() - vraća element s kraja liste push_back(el) - stavlja element el na kraj liste pop_back() - skida element s kraja liste find(el) - vraća pokazivač na traženi element ili null ako element nije u listi delete(el) - briše element el ako postoji u listi for_each(fun) - primjenjuje funkciju fun na sve elemente liste size() - vraća broj elemenata u listi empty() - vraća nenultu vrijednost ako je red prazan, inače vraća 0 Cilj je izvršiti generičku implementaciju ADT DQUEUE, tako da se on može primijeniti na bilo koji tip elemenata reda. Zbog toga će se u implementaciji ADT-a koristiti sljedeće strukture podataka: typedef struct dnode DNode; struct dnode { void *elem; DNode *prev; DNode *next; }; typedef struct dequeue Dequeue; typedef struct dequeue *DEQUEUE; typedef int (*CompareFuncT)(void *, void *); typedef void *(*CopyFuncT)(void *); typedef void (*FreeFuncT)(void *); struct dequeue { DNode *front; /*pokazivač prednjeg čvora liste */ DNode *back; /*pokazivač stražnjeg čvora liste */ int size; /*broj elemenata u listi */ CompareFuncT CompareElem; /*funkcija za usporedbu elemenata */ CopyFuncT CopyElem; /*funkcija za kopiranje i alociranje*/ FreeFuncT FreeElem; /*funkcija za dealociranje elementa */ }; Čvor liste je opisan strukturom DNode. Sadrži dva pokazivača koji pokazuju na prethodni i sljedeći čvor liste te pokazivač na element liste koji je deklariran kao void*. Tom pokazivaču se može pridijeliti adresa bilo koje strukture podataka, ali void pokazivači ne sadrže 271
  • 272.
    informaciju o operacijamakoje se mogu provoditi s podacima na koje oni pokazuju. Zbog toga će biti potrebno da korisnik definira tri pokazivača na funkcije; o CompareElem - pokazivač funkcije za usporedbu dva elementa (poput strcpy), o CopyElem - pokazivač funkcije za alociranje i kopiranje elementa (poput strdup) i o FreeElem - pokazivač funkcije za dealociranje memorije koju element liste zauzima (popot free). Ovi se pokazivači bilježei u strukturi Dequeue, koja je temelj za definiranje ADT DEQUEUE. Tip ovih funkcija je definiran s tri typedef definicije. U structuri Dequeue bilježe se i pokazivači na prednji (front) i stražnji element liste (back) te broj elemenata u listi (size). Sadržaje ove strukture se određuje pri inicijalizaciji ADT-a funkcijom dequeue_new(). Ta i ostale funkcije opisane su u datoteci "dequeue.h". Datoteka specifikacije - "dequeue.h": /* Datoteka: dequeue.h * Specifikacija ADT DEQUEUE */ #ifndef _DEQUEUE_H_ #define _DEQUEUE_H_ typedef struct dequeue *DEQUEUE; typedef int (*CompareFuncT)(void *, void *); typedef void *(*CopyFuncT)(void *); typedef void (*FreeFuncT)(void *); DEQUEUE dequeue_new(CompareFuncT Compare, CopyFuncT Copy, FreeFuncT Free); /* Konstruktor ADT DEQUEUE * Argumenti: * Free - pokazivač funkcije za dealociranje elementa liste * Copy - pokazivač funkcije za alociranje elementa liste * Compare - pokazivač funkcije za usporedbu elemenata liste * vraća 0 ako su elementi jednaki, * <0 ako je prvi element manji od drugoga * >0 ako je prvi element veći od drugoga * Ako su svi pokazivači funkcija jednaki nuli, * tada se podrazumijeva se da je element liste cijeli broj. * Primjer korištenja: * Za rad s listom stringova konstruktor je * DEQUEUE d = dequeue_new(strcmp,strdup, free); * Za rad s listom cijelih brojeva konstruktor je * DEQUEUE d = dequeue_new(0,0,0); * i tada se void pokazivači tretiraju kao cijeli brojevi */ void dequeue_free(DEQUEUE d); /* Destruktor DEQUEUE d */ int dequeue_size(DEQUEUE d); /* Vraća veličunu DEQUEUE d*/ int dequeue_empty(DEQUEUE d); /* Vraća 1 ako je DEQUEUE d prazan, inače vraća 0*/ 272
  • 273.
    void *dequeue_front(DEQUEUE d); /* Vraća pokazivač elementa na glavi DEQUEUE d * ili cijeli broj ako lista sadrži cijele brojeve */ void dequeue_push_front(DEQUEUE d, void *el); /* Stavlja element el na glavu DEQUEUE d*/ void dequeue_pop_front(DEQUEUE d); /* Skida element s glave DEQUEUE d*/ void *dequeue_back(DEQUEUE d); /* Vraća pokazivač elementa s kraja DEQUEUE d * ili cijeli broj ako lista sadrži cijele brojeve */ void dequeue_push_back(DEQUEUE d, void *el); /* Stavlja element el na kraj DEQUEUE d*/ void dequeue_pop_back(DEQUEUE d); /* Skida element s kraja DEQUEUE d*/ void *dequeue_find(DEQUEUE d, void *el); /* Vraća pokazivač elementa liste koji je jednak elementu el */ int dequeue_delete(DEQUEUE d, void *el); /* Briše element el. Ako postoji vraća 1, inače vraća 0 */ void dequeue_for_each(DEQUEUE d, void (*func)(void *)); /* Primjenjuje funkciju func na sve elemente DEQUEUE d*/ #endif Implementacija ADT DEQUEUE je opisana u datoteci "dequeue.c" . /* Datoteka: dequeue.c * Implementacija ADT DEQUEUE */ #include <stdlib.h> #include <string.h> #include <assert.h> #include "dequeue.h" typedef struct dnode DNode; struct dnode { void *elem; DNode *prev; DNode *next; }; typedef struct dequeue Dequeue; struct dequeue { DNode *front; DNode *back; int size; CompareFuncT CompareElem; CopyFuncT CopyElem; FreeFuncT FreeElem; 273
  • 274.
    }; static int CompareInternal(void*a, void *b) { if((int)a > (int)b) return 1; else if((int)a < (int)b) return -1; else return 0; } DEQUEUE dequeue_new( CompareFuncT Compare, CopyFuncT Copy, FreeFuncT Free) { DEQUEUE d=(DEQUEUE) malloc(sizeof(Dequeue)); if(d ) { d->front = d->back = NULL; d->size = 0; d->CopyElem = Copy; d->FreeElem = Free; if(Compare == NULL) d->CompareElem = CompareInternal; else d->CompareElem = Compare; } return d; } /* dvije pomoćne funkcije - make_node i free_node*/ static DNode *make_node(DEQUEUE d, void *el) {/* stvara čvor koristeći CopyElem funkciju */ DNode *n=(DNode *) malloc(sizeof(DNode)); if(n) { n->prev = n->next = NULL; if(d->CopyElem != NULL) n->elem = d->CopyElem(el); else n->elem = el; } return n; } static void free_node(DEQUEUE d, DNode *n) {/* dealocira čvor koristeći FreeElem funkciju */ if(n) { if(d->FreeElem != NULL) d->FreeElem(n->elem); free(n); } } void dequeue_free(DEQUEUE d) { DNode *tmp, *node; assert(d); node = d->front; while (node) { tmp = node; node = node->next; free_node(d, tmp); 274
  • 275.
    } free(d); } int dequeue_size(DEQUEUE d) { assert(d); return d->size; } int dequeue_empty(DEQUEUE d) { assert(d); return d->size==0; } void *dequeue_front(DEQUEUE d) { assert(d); if(d->front) return d->front->elem; else return NULL; } void dequeue_push_front(DEQUEUE d, void *elem) { DNode *new_node; assert(d); new_node = make_node(d, elem); if(new_node == NULL) return; if (d->front) { new_node->next = d->front; d->front->prev = new_node; d->front = new_node; } else { d->front = d->back = new_node; } d->size ++; } void dequeue_pop_front(DEQUEUE d) { DNode *old; assert(d); if (d->front) { old = d->front; d->front = d->front->next; if(d->front == NULL) d->back = NULL; else d->front->prev = NULL; free_node(d, old); d->size --; } } void* dequeue_back(DEQUEUE d) { assert(d); 275
  • 276.
    if(d->back) return d->back->elem; else return NULL; } void dequeue_push_back(DEQUEUE d, void * elem) { DNode *new_node; DNode *last; assert(d); new_node = make_node(d, elem); if(new_node == NULL) return; if (d->back) { last = d->back; last->next = new_node; new_node->prev = last; d->back = new_node; } else { d->front = d->back = new_node; } d->size++; } void dequeue_pop_back(DEQUEUE d) { DNode *old; assert(d); if (d->back) { old = d->back; d->back = d->back->prev; if(d->back == NULL) d->front = NULL; else d->back->next = NULL; free_node(d, old); d->size--; } } int dequeue_delete(DEQUEUE d, void * elem) { DNode *tmp; assert(d); tmp = d->front; while (tmp) { /*prvo pronađi element*/ if ((*d->CompareElem)(tmp->elem, elem) != 0) tmp = tmp->next; /*element u čvoru tmp*/ else if(tmp == d->front) { dequeue_pop_front(d); break; } else if(tmp == d->back){ dequeue_pop_back(d); break; } else { if (tmp->prev) tmp->prev->next = tmp->next; if (tmp->next) tmp->next->prev = tmp->prev; free_node(d, tmp); 276
  • 277.
    d->size--; break; } } return tmp != NULL; } void *dequeue_find(DEQUEUE d, void * elem) { DNode* node; assert(d); node = d->front; while (node){ if((*d->CompareElem)(node->elem, elem) == 0) return node->elem; node = node->next; } return NULL; } void dequeue_for_each(DEQUEUE d, void (*func)(void *)) { DNode* node; assert(d); node = d->front; while (node) { DNode *next = node->next; (*func) (node->elem); node = next; } } Testiranje DEQUEUE se provedi programom dequeue-teststr.c. U njemu korisnik proizvoljno umeće ili briše stringove u dvostranom redu. Datoteka: dequeue-teststr.c #include <stdio.h> #include <stdlib.h> #include <string.h> #include <assert.h> #include "dequeue.h" void upute(void) { printf ("Izbornik:n" " 1 Umetni ime sprijeda n" " 2 Umetni ime straga n" " 3 Odstrani sprijedan" " 4 Odstrani stragan" " 0 Krajn"); } void print_elem(void *el) {printf("%s ", (char*)el);} void dequeue_print(DEQUEUE D) { 277
  • 278.
    if(!dequeue_empty(D)) dequeue_for_each(D, print_elem); } int main() { int izbor; char elem[255]; DEQUEUE D = dequeue_new(strcmp, strdup, free); upute(); printf("? "); scanf("%d", &izbor); while (izbor != 0) { switch(izbor) { case 1: printf("Otkucaj ime: "); scanf("n%s", elem); dequeue_push_front(D, elem); printf("%s ubacen u red.n", elem); dequeue_print(D); break; case 2: printf("Otkucaj ime: "); scanf("n%s", elem); dequeue_push_back(D, elem); printf("%s ubacen u red.n", elem); dequeue_print(D); break; case 3: if (!dequeue_empty(D)) { printf("Bit ce %s odstranjen sprijeda.n", (char *) dequeue_front(D)); dequeue_pop_front(D); } dequeue_print(D); break; case 4: if (!dequeue_empty(D)) { printf("Bit ce %s odstranjen straga.n", (char *)dequeue_back(D)); dequeue_pop_back(D); } dequeue_print(D); break; default: printf("Pogresan Izbor.nn"); upute(); break; } printf("n? "); scanf("%d", &izbor); } if(dequeue_find(D, "ivo")) { 278
  • 279.
    dequeue_delete(D, "ivo"); dequeue_print(D); } dequeue_free(D); return 0; } Uočite da operacije traženja imaju složenost O(n), a sve ostale operacije imaju složenost O(1). Zadatak: Prethodni primjer testiranja dvostranog reda, koji sadrži stringove, promijenite tako da se njime testira dvostrani red koji sadrži cijele brojeve. Koristite konstruktor oblika: DEQUEUE d = dequeue_new(0,0,0); Zadatak: Prethodni primjer testiranja dvostranog reda, koji sadrži stringove, promijenite tako da se njime testira dvostrani red koji sadrži realne brojeve tipa double. U tom slučaju morate definirati funkcije void FreeFunc(double* pd); /*dealocira memoriju koju zauzima broj tipa double*/ double *CopyFunc(double* pd); /*vraća pokazivač na kopiju alociranog broja *pd */ int CompareDouble(double* pd1, double* pd1); /* Vraća 0 ako je *pd1 == *pd2 * Vraća 1 ako je *pd1 > *pd2 * Vraća -1 ako je *pd1 < *pd2 */ i koristiti konstruktor oblika; DEQUEUE d = dequeue_new(FreeFunc, Copy Func, CompareFunc); Zadatak: Generički pristup koji je korišten za definiranje ADT DEQUEUE može se iskoristiti za definiranje ADT koji služi za rad sa skupovima. Definirajte ADT SET kojim se, kao i u matemetici, može raditi sa skupovima pomoću sljedećih operacija: ADT SET Empty(S) - vraća nenultu vrijednost ako S prazan skup size(S) - vraća broj elemenata u skupu S, tj. vraća |S| insert(S, x) - stavlja element x u skup S, ako x ∉ S. Nakon toga je x ∈ S member(S, x) - vraća true ako je x ∈ S delete(S, x) - briše element x ako postoji u skupu S for_each(S, fun) - primjenjuje funkciju fun na sve elemente skupa S intersection(S1, S2) - vraća skup S koji je jednak presjeku skupova S1 ∩ S2 union(S1, S2) - vraća skup S koji je jednak uniji skupova S1 ∪ S2 difference(S1, S2) - vraća skup S koji je jednak razlici skupova S1 S2 Za implementaciju ovih operacija dovoljno je koristiti jednostruko vezanu listu. Zadatak: Često se kolekcija elemenata, koja, za razliku od skupa, može sadržavati više istovrsnih elemenata, naziva vreća (eng. BAG). Realizirajte ADT BAG pomoću vezane liste. 279
  • 280.
    Svakom elementu listepridodajte atribut kojeg se obično naziva referentni izbroj (eng. referent count). Primjerice, možete za čvor liste koristiti sljedeću strukturu: struct bag_node { int ref_count; void *element; struct bag_node *next; } Referentni izbroj pokazuje koliko je istih elemenata u vreći. Realizirajte generički ADT BAG koji podržava sljedeće operacije: ADT BAG empty(B) - vraća nenultu vrijednost ako je vreća prazna size(B) - vraća broj elemenata u vreći insert(B, x) - stavlja element x u vreću B po sljedećem algoritmu: ako u vreći već postoji element vrijednoti x, tada uvećavaj x.ref_count inače, kopiraj i alociraj element x u vreću, postavi x.ref_count na vrijednost 1. find(B, x) - vraća vrijednost x.ref_count koji znači koliko ima istovrsnih elemenata vrijednosti x, ili 0 ako element x nije u vreći delete(B, x) - briše element x ako postoji u vreći, po sljedećem algoritmu: smanji x.ref_count za jedan. ako je x.ref_count jednak nuli, tada dealociraj memoriju koju zauzima x vrati x.ref_count 18.9 Zaključak Vezana lista je dinamička strukutura. Povećava se i smanjuje prema zahtjevima korisnika. Liste su kolekcije sa “sekvencijalnim pristupom”, za razliku od nizova koje se koristi kao kolekcije sa “slučajnim pristupom”. Apstraktno, liste predstavljaju kolekciju elemenata. Operacije umetanja i brisanja elemenata kolekcije provode se jednostavnije s vezanom listom, nego je to slučaj s nizovima. Umetanje i brisanje elementa unutar jednostruko vezane liste je relativno spora operacija jer se u njoj mora odrediti i položaj elementa koji prethodi mjestu umetanja. Znatno je brže umetanje elemenata na glavi liste. Vezane liste se mogu slikovito usporediti s vlakom kojem su vagoni povezani jedan za drugim. Iz jednog vagona se može prijeći samo u susjedni vagon. Vagoni se mogu dodavati tako da se spoje na postojeću kompoziciju ili da se umetnu unutar kompozicije vlaka. Dodavanje vagona je jednostavna operacija, a umetanje vagona unutar kompozicije je složena operacija. Kada je nužno vršiti često umetanje i brisanje čvorova unutar liste, tada je povoljno koristiti dvostruko vezanu listu. Korištenjem tehnike definiranja apstraktnih tipova pokazano je da se pomoću liste mogu efikasno realizirati apstraktni tipovi STACK, QUEUE i DEQUEUE. U implementaciji ADT DEQUEUE pokazano je kako se mogu stvarati generičke kolekcije podataka. 280
  • 281.
    19 Razgranate strukture- stabla Naglasci: • binarno stablo • stablo s aritmetičkim izrazima • leksički i sintaktički analizator aritmetičkih izraza • višesmjerna stabla • hrpa i prioritetni redovi 19.1 Definicija stabla Stablo (eng. tree) je apstraktna struktura podataka koja se četo koristi za predstavljanje hijerarhijskih odnosa, primjerice, slika 19.1 prikazuje hijerarhiju porodičnog stabla. Struktura stabla je potpuno određena čvorovima stabla i vezama između čvorova. Čvorovi stabla sadrže neki podatak, a veze određuju hijerarhijske odnose, prema sljedećem pravilu: 1. svaki čvor (osim jednog) ima samo jednog prethodnika, 2. svaki čvor ima određen broj slijednika. Slika 19.1 Porodično stablo Ako nije ograničen broj slijednika, stablo se naziva općenito stablo (eng. general tree). Prikazano je na slici 19.2. Ako čvorovi stabla imaju maksimalno k slijednika, stablo se naziva k- stupanjsko stablo. U specijalnom slučaju, kada elementi stabla imaju maksimalno 2 slijednika, stablo se naziva binarno stablo. dubina korijen 0 a 1 b c x unutarnji čvor vanjski čvor 2 d e f g h y (list) 3 visina stabla i j Slika 19.2 Strukturalni prikaz općenitog stabla 281
  • 282.
    Jedinstveni čvor kojinema prethodnika naziva se korijen (eng. root) stabla. Čvorovi koji nemaju slijednika nazivaju se listovi stabla (eng. leaf) ili vanjski čvorovi, svi ostali čvorovi su unutarnji čvorovi, a predstavljaju podstabla (eng. subtree). Slijednik čvora se naziva dijete (eng. child), a prethodnik čvora se naziva roditelj (eng. parent). Ako više čvorova ima istog roditelja oni su jedan drugome braća (ili sestre) (eng. siblings). U binarnom stablu čvor može imati dvoje djece koja se nazivaju lijevo dijete (eng. left child) i desno dijete (eng. right child). Kada je važan redoslijed djece, stablo se naziva uređeno stablo (eng ordered tree). Uzmemo li niz čvorova n1, n2, . . . , nk na način da je ni roditelj od ni+1 for 1 ≤ i < k, tada ovaj niz tvori stazu od čvora n1 do čvora nk. Duljina staze je jednaka broju čvorova u stazi umanjeno za jedan (od čvora do njega samog vodi staza nulte duljine). Visina čvora u stablu jednaka je duljini najdulje staze od čvora do lista (visina stabla je jednaka visini korijena stabla). Dubina čvora je jednaka je duljini staze od korijena stabla do tog čvora. Ona određuje razinu čvora. Puno ili popunjeno k-stupanjsko stablo je stablo u kojem svi listovi imaju istu dubinu, a svaki čvor ima svih k grana. U takvom stablu, na dubini 0 postoji 1 (k0 ) čvor, na dubini 1 postoji k1 čvorova, na dubini 2 postoji k2 čvorova..., pa ukupan broj unutarnjih čvorova Nu u stablu visine h iznosi: h k h −1 N u = 1 + k 1 + k 2 + .. + k h −1 = ∑ k i = (1) i =0 k −1 i maksimalni broj vanjskih čvorova (listova) iznosi kh. 19.2 Binarno stablo Najčešće korišteni oblik stabla je binarno stablo. Definira se rekurzivno: Definicija: Binarno stablo je: 1. prazno stablo, ili 2. sastoji se od čvora koji se naziva korijen i dva čvora koji se nazivaju lijevo i desno dijete, a oni su također binarna stabla (podstabla). Slika 19.3 (a) Puno i (b) potpuno binarno stablo 282
  • 283.
    Ukupan broj unutarnjihčvorova punog binarnog stabla dobije se iz izraza (1). Tada je k=2 i Nu=2h-1. Broj vanjskih čvorova (listova) jednak je Nv = 2h. Makimalni broj čvorova u punom stablu, visine h, jednak je N = Nu+ Nu = 2h+1-1. Duljina staze od korijena do čvora jednaka je visini stabla. Kod punog stabla visina iznosi h = log2(N+1)-1. Kaže se da je binarno stablo potpuno 1 (eng. complete binary tree) ako je to puno stablo ili ako od tog stabla može nastati puno stablo dodavanjem čvorova na na donjoj razini s desna. Ova stabla su ilustrirana na slici 19.3. Kasnije ćemo vidjeti da se potpuna stabla mogu efikasno implementirati pomoću nizova. 19.2.1 Programska implementacija binarnog stabla Čvorovi binarnog stabla se najčešće opisuju samoreferentnim dinamičkim strukturama, primjerice u C jeziku čvor binarnog stabla se može definirati strukturom tnode: typedef char elemT; typedef struct tnode { elemT elem; struct tnode * left; struct tnode * right; } Tnode, *TREE; Ova struktura sadrži neki element i dva pokazivača: left pokazuje na lijevo dijete, a right pokazuje na desno dijete. Ponekad se ovoj strukturi dodaje i pokazivač na roditelja. Pokazivači služe uspostavljanju veza među čvorovima. Slika 19.4 prikazuje veze među čvorovima jednog stabla. Slika 19.4 Dva načina prikaza binarnog stabla: a) strukturalni prikaz i b) prikaz pomoću pokazivača na strukturu tnode. U strukturalnom prikazu krugovi označavaju unutarnje čvorove, a pravokutnici označavaju listove stabla Binarno stablo se može prikazati i tekstualnim zapisom, prema algoritmu prefiksne notacije stabla, na sljedeći način: Algoritam: Prefiksna notacija stabla 1. Ako čvor predstavlja prazno stablo (NULL pokazivač), tada zapiši prazne zagrade (): 2. Ako čvor predstavlja list stabla, tada zapiši element lista. 3. Ako čvor predstavlja unutarnji čvor, tada unutar zagrada zapiši element čvora te lijevi i desni čvor. 1 Neki autori puno stablo nazivaju potpuno stablo, a potpuno stablo nazivaju "skoro potpuno stablo". 283
  • 284.
    Za stablo saslike 19.4 vrijedi zapis: (A (B () D) ili (A (B () D) (C E F) ). (C E F) ) Ovaj oblik notacije se naziva prefiksna notacija jer se apstraktno može smatrati da element unutarnjeg čvora stabla predstavlja prefiksni operator koji djeluje na svoju djecu. Prefiksna notacija se često koristi za zapis aritmetičkih izraza. Ona se vrši na način da se u zagradama najprije zapiše operator, a zatim dva operanda. Operand može biti broj ili izraz. Ako je operand izraz, ponovo se primjenjuje isto pravilo. Primjerice, izraz infiksne notacije 8*(7+3) ima prefiksnu notaciju ( * 8 (+ 3 7) ). Općenito, svaki se aritmetički izraz može napisati u prefiksnoj notaciji. Zbog ovog svojstva aritmetički izrazi se mogu pohraniti u binarnom stablu. Primjer je prikazan na slici 19.5. infiksna notacija = (7 – 3) * (4 + 5) prefiksna notacija = ( * (- 7 3) (+ 4 5)) Slika 19.5 Binarno stablo aritmetičkog izraza Programski se stablo se može formirati pomoću funkcije make_tnode(). TREE make_tnode (elemT elem, TREE left, TREE right) { TREE t = malloc(sizeof(Tnode)); if(t) { t->left = left; t->right = right; t->elem = elem;} return t; } Argumenti funkcije su vrijednost elementa čvora (elem) i pokazivači na lijevo i desno dijete (left, right). Funkcija vraća pokazivač na formirani čvor, ili NULL ako se ne može izvršiti alokacija memorije. Kada se formira vanjski čvor stabla (list), tada se vrijednost argumenata left i right postavlja na vrijednost NULL. U svim ostalim čvorovima bar jedan od ova dva pokazivača mora biti različit od NULL. Primjer: Stablo sa slike 19.4 može se formirati sljedećim naredbama: /* 1. formiraj listove */ TREE l1 = make_tnode('7', NULL,NULL); TREE l2 = make_tnode('3', NULL,NULL); TREE l3 = make_tnode('4', NULL,NULL); TREE l4 = make_tnode('5', NULL,NULL); /* 2.formiraj podstabla */ TREE t1 = make_tnode('-', l1, l2); TREE t2 = make_tnode('+', l3, l4); /* 3. formiraj korijena stabla */ 284
  • 285.
    TREE t =make_tnode('*', t1, t2); Varijabla t je pokazivač na korijen stabla. Ostale varijable su pomoćne pokazivačke varijable za formiranje listova i podstabala. Isto se može ostvariti jednom naredbom: TREE t = make_tnode('*', make_tnode('-', make_tnode('7', NULL,NULL) ), make_tnode('3', NULL,NULL) )), make_tnode('+', make_tnode('4', NULL,NULL), make_tnode('5', NULL,NULL)) ); Uočite da se u ovom slučaju poziv funkcije make_tnode() obavlja prema prije opisanoj prefiksnoj notaciji stabla. U prethodnom primjeru korisnik obavlja sve operacije formiranja stabla. Kasnije će biti pokazano kako se može automatizirati postupak formiranja stabla. Prazno stablo se formira naredbom: TREE t = NULL; pa funkcija int tree_is_empty(TREE t) {return t == NULL;} vraća nulu ako je stablo prazno, ili nenultu vrijednost ako je stablo nije prazno. Korisna je i funkcija tree_is_leaf() kojom se određuje da li čvor predstavlja list stabla. int tree_is_leaf(TREE t) {return !(t->left || t->right);} tree_is_leaf()vraća nulu ako čvor t nije list stabla, ili nenultu vrijednost ako je čvor t list stabla. Rekurzivna definicija stabala čini da je većinu operacija nad stablom najlakše definirati rekurzivno. Primjer: Funkcija tree_size(t) vraća broj čvorova binarnog stabla. int tree_size(TREE t) { if(tree_is_empty(t)) return 0 ; else return 1 + tree_size(t->left) + tree_size(t->right); } Primjer: Funkcija print_prefiks() ispisuje prefiksnu notaciju stabla koje sadrži aritmetički izraz. Realizirana je prema rekurzivnom algoritmu prefiksne notacije stabla. void print_prefiks (TREE t) { if(t == NULL) {printf(" () "); return;} if(tree_is_leaf(t)) {printf("%c ",t->elem); return;} printf("( %c ",t->elem); print_prefiks (t->left); print_prefiks (t->right); printf(" )"); 285
  • 286.
    } Zadatak: Testirajte primjenufunkcije print_prefilks() u programu: /* Datoteka: print_nodes.c int main() { TREE t = make_tnode('*', make_tnode('-', make_tnode('7', NULL,NULL), make_tnode('3', NULL,NULL)), make_tnode('+', make_tnode('4', NULL,NULL), make_tnode('5', NULL,NULL)) ); print_prefiks (t); return 0; } 19.2.2 Obilazak binarnog stabla Obilazak stabla je postupak kojim se na sve čvorove stabla primjenjuje neka operacija. Kroz čvor se smije "proći" više puta, ali se operacija nad čvorom izvršava samo jedan put. Općeniti obrazac za rekurzivni obilazak binarnog stabla, počevši od čvora N (koji nije prazan), temelji se na tri operacije: (L) rekurzivno obiđi lijevo stablo. Nakon ove operacije ponovo si u čvoru N. (D) rekurzivno obiđi desno stablo. Nakon ove operacije ponovo si u čvoru N. (P) procesiraj čvor N. Ove tri operacije se mogu izvršiti bilo kojim redoslijedom. Ako se vrši (L) prije (D) tada je obilazak s lijeva na desno (eng. left-right traversal), u suprotnom je obilazak s desna na lijevo (right-left traversal). Prema redoslijedu operacija (L) (D) (P), razlikuju se tri načina obilaska stabla: Obilazak stabla Redoslijed operacija Pre-order ili prefiks (P) (L) (D) Post-order ili postfiks (L) (D) (P) In-order ili infiks (L) (P) (D) Slika 19.6 prikazuje stazu obilaska stabla za sva tri načina. 286
  • 287.
    Slika 19.6 Trinačina obilaska stabla (za obilazak s lijeva na desno). Usmjerene crtkane linije pokazuju redoslijed obilaska čvorova Obilazak stabla se programski može realizirati funkcijom tree_traverse(): enum torder {PREORDER,INORDER,POSTORDER}; void tree_traverse(TREE t, int order, void (*visit)(TREE)) { if (t != NULL){ if (order == PREORDER) visit(t); tree_traverse(t->left, order, visit); if (order == INORDER) visit (t); tree_traverse(t->right, order, visit); if (order == POSTORDER) visit(t); } } Funkciji tree_traverse() prvi je argument pokazivač čvora stabla. Drugi argument je vrijednost iz skupa {PREORDER, INORDER, POSTORDER}, kojom se određuje način obilaska stabla. Treći argument je pokazivač na tzv. visit funkciju kojoj je argument pokazivač čvora na koji ta funkcija djeluje. Primjerice, "visit" funkcija print_char() ispisuje vrijednost elementa čvora. void print_char(TREE t) { printf("%c ",t->elem); } Zadatak: Funkcije print_char() i tree_traverse() uvrstite u prethodni program "print_nodes.c", a u funkciji main(), umjesto naredbe print_prefiks(), napišite naredbe: printf("nPREORDER: "); tree_traverse(t, PREORDER, print_el); printf("nINORDER: "); tree_traverse(t, INORDER, print_el); printf("nPOSTORDER: "); tree_traverse(t, POSTORDER, print_el); Nakon izvršenja programa dobije se ispis: PREORDER: * - 7 3 + 4 5 INORDER: 7 - 3 * 4 + 5 POSTORDER: 7 3 - 4 5 + * Ovaj primjer pokazuje da se pomoću binarnog stabla aritmetički izrazi lako pretvaraju u prefiksni, infiksni i postfiksni oblik. 19.2.3 Brisanje stabla Brisanje stabla je operacija kojom se dealociraju svi čvorovi stabla, koje time postaje prazno stablo. Ovu operaciju treba izvesti tako da se najprije dealociraju listovi stabla, a tek onda unutarnji čvorovi. Slika 19.4 pokazuje da se to može izvesti postorder obilaskom stabla. Operacija brisanja stabla je implementirana u funkciji tree_delete(). TREE tree_delete( TREE t ) { if( t != NULL ) { tree_delete( t->left ); tree_delete( t->right ); 287
  • 288.
    free( t ); } return NULL; } Kod primjene ove funkcije treba uvijek voditi računa da se ona koristi u obliku t = tree_delete(t); jer na taj način t postaje jednako NULL, što označava prazno stablo. 19.2.4 Vrednovanje aritmetičkog izraza Gotovo svi moderni kompilatori i interpreteri, u prvoj fazi kompiliranja, pretvaraju izvorni kôd u oblik koji je podesan za optimiranje kôda i generiranje strojnog kôda ili interpretiranje kôda. Praksa je pokazala da je najbolji način da se izvorni kod najprije pretvori u jedan oblik "sintaktičkog" stabla. Dio tog sintaktičkog stabla, kojim se bilježi aritmetičke izraze, sličan je ovdje opisanom binarnom stablu. U prethodnim primjerima, zbog jednostavnosti, element čvora je bio tipa char. Ako se pak želi formirati stablo koje će sadržavati realne brojeve, tada treba izmijeniti definiciju tipa elementa čvora. Kod aritmetičkih izraza potrebno je da čvor sadrži ili znak operatora ili realni broj. To se može ostvariti tipom koji je unija elemenata tipa char (ili int) i tipa double. Koristit će se definicija: typedef union elemT { int op; double num; } elemT; typedef struct tnode { elemT elem; struct tnode * left; struct tnode * right; } Tnode, *TREE; Za formiranje čvora koristit će se funkcije make_opnode() i make_numnode(). TREE make_opnode (int op, TREE left, TREE right) { /* formira unutarnji čvor koji sadrži operator op*/ /* vraća pokazivač na taj čvor, ili NULL ako je greška*/ TREE t = malloc(sizeof(Tnode)); if(t) { t->left = left; t->right = right; t->elem.op = op;} return t; } TREE make_numnode (double num) { /* formira vanjski čvor koji sadrži realni broj - num*/ /* vraća pokazivač na taj čvor, ili NULL ako je greška */ TREE t = malloc(sizeof(Tnode)); if(t) { t->left = NULL; t->right = NULL; t->elem.num = num;} return t; } Programski jezici Lisp i Sheme koriste prefiksnu notaciju za zapis svih svojih konstrukcija, a pohrana i izračunavanje vrijednosti izraza (tzv. evaluacija) vrši se obilaskom binarnog stabla. Koristi se sljedeći algoritam: 288
  • 289.
    Algoritam: Vrednovanje aritmetičkogizraza koji je zapisan u binarnom stablu Temeljni slučaj: Ako čvor sadrži operand, vrati vrijednost operanda Pravilo rekurzije: Ako čvor sadrži operator, rekurzivno dobavi lijevi i desni operand, izvrši operaciju koju određuje operator i vrati rezultat Ovaj algoritam je implementiran u funkciji evaluate(). double evaluate(TREE t) { /* vraća vrijednost izraza koji je u stablu t*/ double x,y; int op; if(tree_is_leaf(t)) /* ako je t list stabla */ return t->elem.num; /* vrati vrijednost operanda */ /* inače, */ op = t->elem.op; /* dobavi operator */ x = evaluate(t->left); /* dobavi lijevi operand */ y = evaluate(t->right); /* dobavi desni operand */ switch (op) { /* izračunaj vrijednost izraza*/ case '+': return x+y; case '-': return x-y; case '*': return x*y; case '/': return (y == 0)? 0 : x/y; default: printf("nGreska"); return 0; } } Primjer: U programu evaluate.c, pokazana je primjena funkcije evaluate() za proračun vrijednosti prefiksnog izraza (*(- 7.5 3)(+ 4 5.1)). Program koristi funkcije i definicije koje su definirane u datotekama "prefix_tree.h" i "prefix_tree.c". /***************************************************************/ /* Datoteka: prefix_tree.h */ /***************************************************************/ #ifndef _PREFIX_TREE_H_ #define _PREFIX_TREE_H_ typedef union elementip { int op; double num; }elemT; typedef struct tnode { elemT elem; struct tnode * left; struct tnode * right; } Tnode, *TREE; int tree_is_leaf(TREE t); void print_prefiks (TREE t); TREE make_opnode (int op, TREE left, TREE right); TREE make_numnode (double num); double evaluate(TREE t); #endif /***************************************************************/ 289
  • 290.
    /* Datoteka: prefix_tree.c */ /***************************************************************/ #include <stdio.h> #include <stdlib.h> #include "prefix_tree.h" int tree_is_leaf(TREE t) {return !(t->left || t->right);} void print_prefiks (TREE t) { if(t == NULL) {printf(" () "); return;} if(tree_is_leaf(t)) {printf("%f ",t->elem.num); return;} printf("( %c ",t->elem.op); print_prefiks (t->left); print_prefiks (t->right); printf(" )"); } TREE make_opnode (int op, TREE left, TREE right) { TREE t = malloc(sizeof(Tnode)); if(t) { t->left = left; t->right = right; t->elem.op = op;} return t; } TREE make_numnode (double num) { TREE t = malloc(sizeof(Tnode)); if(t) { t->left = NULL; t->right = NULL; t->elem.num = num;} return t; } TREE tree_delete( TREE t ) { if( t != NULL ) { tree_delete( t->left ); tree_delete( t->right ); free( t ); } return NULL; } double evaluate(TREE t) { double x,y; int op; if(tree_is_leaf(t)) /* operand*/ return t->elem.num; /*else analyse operator*/ op = t->elem.op; x = evaluate(t->left); y = evaluate(t->right); switch (op) { case '+': return x+y; case '-': return x-y; 290
  • 291.
    case '*': returnx*y; case '/': return (y==0)? 0 : x/y; default: printf("nGreska"); return 0; } } /************************************************************/ /* Datoteka: evaluate.c */ /************************************************************/ #include <stdio.h> #include <stdlib.h> #include "prefix_tree.c" int main() { TREE t = make_opnode('*', make_opnode('-', make_numnode(7.5), make_numnode(3)), make_opnode('+', make_numnode(4), make_numnode(5.1)) ); printf("nVrijednost prefiksnog izraza:n"); print_prefiks(t); printf("njednaka je: %f", evaluate(t)); return 0; } Program se kompilira komandom: c:> cl evaluate.c prefix_tree.c Nakon izvršenja programa "evaluate.exe" dobije se ispis: Vrijednost prefiksnog izraza: ( * ( - 7.500000 3.000000 )( + 4.000000 5.100000 ) ) jednaka je: 40.950000 19.3 Interpreter prefiksnih izraza Sada će biti pokazano kako se realizira interpreter prefiksnih izraza koje unosi korisnik pomoću tipkovnice ili iz datoteke. 19.3.1 Dobavi-vrednuj-ispiši Rad interpretera se obično opisuje tzv. dobavi-vrednuj-ispiši petljom (eng. read-eval-print loop), koja je definirana sljedećim algoritmom: Algoritam : Interpreter s dobavi-vrednuj-ispiši petljom Ponavljaj: 1. Dobavi naredbu – dobavi izvorni kod naredbe i interno ga spremi u prikladnu podatkovnu strukturu. Ako je primljen zahtjev za kraj rada, završi petlju. 2. Vrednuj naredbu – izvrši operacije na internoj podatkovnoj strukturi, koje rezultiraju nekom vrijednošću. Primjerice, obilaskom binarnog stabla izračunaj vrijednost aritmetičkog izraza. 3. Ispiši rezultat. 291
  • 292.
    U prethodnoj sekcijije pokazano kako se vrednuje i ispisuje rezultat aritmetičkog izraza koji je zapisan u binarnom stablu. Sada će biti pokazano kako se realizira "dobavi" operacija, tj. kako se od korisnika dobavlja naredba te kako se vrši leksička i sintaktička analiza izvornog kôda naredbe, koja rezultira binarnom stablom aritmetičkog izraza. U leksičkoj i sintaktičkoj analizi koristit će se specifikacije u BNF notaciji, koja je opisana u poglavlju 6. U BNF notaciji gramatička se pravila zapisuju u obliku produkcije: A:α gdje je A neterminalni simbol, a α predstavlja niz terminalnih ili neterminalnih simbola X1 X2...Xn . Ako je simbol Xi neterminalni simbol, zapisuje se kurzivom. Na taj se način razlikuje od terminalnog simbola. Ako je simbol Xi opcioni, što znači da ne mora biti u produkciji, zapisuje sa sufiksom opt, tj. Xopt. Ako je neterminalni simbol A definiran s više alternativnih produkcija A : α1 , A : α2 ... A : αn., koristi se notacija: A : α1 | α2 | ...| αn U gramatičkim analizama opcioni simbol se tretira kao neterminalni simbol koji je definiran produkcijom: Xopt : X | ε gdje ε označava "praznu" produkciju. Kaže se da je Xopt definiran ili kao X ili kao prazna produkcija (ε-produkcija). 19.3.1 Leksička analiza prefiksnih aritmetičkih izraza Razmotrimo jedan prefiksni izraz, s realnim brojevima, koji je napisan u više linija teksta: (* (- 3.7 35) # komentar (/ 8 (+ 9.1 -5) ) ) # ....... $ # kraj unosa Ovaj izraz se može pročitati na sljedeći način: "pomnoži razliku 3.7 i 35 i kvocijent od 8 i zbroja 9.1 i -5". Nakon ovog izraza slijedi znak $. On označava kraj unosa. U prikazanom prefiksnom aritmetičkom izrazu su zastupljene sljedeće leksičke i gramatičke kategorije: leksem značaj token - gramatički vrijednost tokena (iz ulaznog toka) terminalni simbol ( ili atribut tokena) (, ), zagrade za grupiranje '( ' ')' '(' ')' izraza 3.7 35 -5 realni broj NUM numerička vrijednost realnog broja +, -, aritmetički operatori OPERATOR '+', '-', *, /, '*', '/' $, EOF oznak kraja unosa QUIT '$' ili EOF (EOF kod datoteka) razmak, tab, separatori (bijeli nova linija znakovi: 'n', 't', 'n' ) # ....... komentar (počinje 292
  • 293.
    znakom # ) Leksičkaanaliza je proces kojim se sekvencijalno u nizu ulaznih znakova prepoznaje neki leksem i gramatički simbol koji on predstavlja, tj. token. Primjerice, leksem "37.5" predstavlja token NUM. Ako je potrebno, uz token se bilježi i njegova vrijednost, koja predstavlja atribut tokena. Leksički analizator se može jednostavno implementirati pomoću funkcije getToken() i sljedećih deklaracija: /**************************************************************/ /* datoteka: prefix_lex.h */ /**************************************************************/ #ifndef _PREFIX_LEX_H_ #define _PREFIX_LEX_H_ /* tokeni (terminalni simboli) */ #define NUM 255 #define OPERATOR 256 #define QUIT 257 /* globalne varijable*/ extern FILE *input = stdin; /* ulazni tok */ extern elemT tokenval; /* vrijednost tokena */ extern char lexeme[]; /* leksem tokena */ int getToken(); /* Pri svakom pozivu funkcija vraća sljedeći token iz ulaznog toka, ili 0, ako je registriran nepredviđeni znak. Vrijednost tokena i leksem tokena se zapisuju u globalne varijable tokenval i lexeme. */ #endif Implementacija funkcije getToken() je u datoteci "prefix_lex.c". /**************************************************************/ /* datoteka prefix_lex.c */ /**************************************************************/ #include <stdio.h> #include <stdlib.h> #include <ctype.h> #include "lexan.h" FILE *input = stdin; /* ulazni tok */ elemT tokenval; /* vrijednost tokena */ char lexeme[64]; /* leksem tokena */ int getToken() { int ch; while(1) { ch = getc(input); 293
  • 294.
    /* preskoči bijeleznakove */ if(ch == ' ' || ch =='t' || ch =='n' || ch =='r') continue; /* zapamti prvi znak */ lexeme[0]=(char)ch; lexeme[1]='0'; if (isdigit(ch)) { /* dobavi broj */ int i = 1; /* prva znamenka je u lexeme[0]*/ ch = getc(input); while(isdigit(ch) && i<62) { lexeme[i++]=(char)ch; ch = getc(input); } if(ch == '.') { lexeme[i++]=(char)ch; ch = getc(input); while(isdigit(ch) && i<62){ lexeme[i++]=(char)ch; ch = getc(input); } } lexeme[i] = '0'; tokenval.num = atof(lexeme); if (ch != EOF) ungetc(ch,input); return NUM; } else if(ch == '+' || ch == '-' || ch == '*' || ch == '/' ) { tokenval.op = ch; return OPERATOR; }else if(ch == '(' || ch == ')') { return ch; }else if(ch == '$' || ch == EOF) { return QUIT; }else if(ch == '#'){ /*komentar do kraja linije */ while (ch != 'n') ch = getc(input); }else return 0; /* nedozvoljen znak */ } } U funkciji getToken() se unutar petlje dobavlja znak iz toka naredbom getc(input). Petlja se ponavlja ako su na ulazu "bijeli znakovi" ili ako je dobavljen znak '#', koji označava početak komentara. U svim ostalim slučajevima petlja se prekida nakon analize dobavljenog znaka, a analiza se vrši na sljedeći način: • Ako je dobavljeni znak numerička znamenka, dobavlja se realni broj oblika: realni_broj : niz_znamenki decimalni_dioopt decimalni_dio : . niz_znamenki opt Bilježenje broja se vrši prema prethodnom pravilu: najprije se bilježi niz znamenki, a zatim, ako iza njih slijedi točka, bilježi se decimalni dio. Leksem broja se bilježi u stringu lexeme, njegova numerička vrijednost u tokenval.num, a funkcija getToken() vraća token NUM. • Ako je dobavljeni znak operator +, -, * ili / , getToken() vraća token OPERATOR, a vrijednost tokena bilježi u tokenval.op. • Ako je dobavljeni znak lijeva ili desna zagrada, getToken() vraća ASCII vrijednost znaka, što služi kao oznaka tokena. • Ako je dobavljeni znak &, ili ako je kraj datoteke, getToken() vraća token QUIT. 294
  • 295.
    U svim ostalim slučajevima getToken() vraća vrijednost nula, što označava nepoznati token. Funkcija getToken() ima jednostavnu strukturu. Lako se može proširiti da obuhvati širi skup tokena. Zbog toga, ona može poslužiti kao obrazac prema kojem se može se napisati kvalitetni leksički analizator za znatno složeniji programski jezik. 19.3.3 Sintaktička analiza prefiksnih izraza U skladu s BNF notacijom gramatika naredbe za proračun prefiksnih aritmetičkih izraza glasi: Naredba : ( Izraz ) | QUIT Izraz : OPERATOR Operand Operand Operand : NUM | - NUM | ( Izraz ) gdje su Naredba, Izraz i Operand neterminalni simboli, a zagrade ( ), OPERATOR, NUM i QUIT su terminalni simboli (tokeni), koji postoje u ulaznom toku. Naredba je startni simbol gramatike. Desna strana njegove produkcije određuje temeljni oblik rečenice koja se može ostvariti ovom gramatikom. Moguća su dva oblika rečenice: "QUIT" – što predstavlja naredbu za kraj rada interpretera i "( Izraz )" – što predstavlja naredbu da se izračuna vrijednost Izraza. Ovaj drugi oblik sadrži neterminalni simbol Izraz. Zbog toga, to nije stvarna rečenica jezika, već se naziva rečenična forma (eng. sentential form). Izraz označava pravilo po kojem u ulaznom toku, na mjestu gdje se očekuje prefiksni izraz, simboli moraju biti poredani tako na najprije bude simbol OPERATOR, a iza njega moraju biti dva operanda. Ako u rečeničnoj formi (Izraz) zamijenimo neterminalni simbol Izraz s desnom stranom produkcije, tada rečenična forma ima oblik (OPERATOR Operand, Operand). Operand je neterminalni simbol koji je definiran pravilom po kojem u ulaznom toku, na mjestu gdje se očekuje operand, može biti ili broj (NUM) , ili negativni broj (-NUM) ili zagradama omeđeni Izraz. Rečenice, koje može generirati gramatika, su sve rečenice koje se mogu dobiti iz rečenične forme, na način da se u rečeničnoj formi neterminalni simboli zamijene s desnom stranom njihove produkcije, i to sve dok rečenična forma ne sadrži isključivo terminalne simbole. Opisani proces se naziva derivacija, a označava se simbolom ⇒. Evo primjera dvije moguće derivacije: Naredba Naredba ⇒ ( Izraz ) ⇒ ( Izraz ) ⇒ ( OPERATOR Operand Operand ) ⇒ ( OPERATOR Operand Operand ) ⇒ ( OPERATOR NUM Operand ) ⇒ ( OPERATOR NUM Operand ) ⇒ ( OPERATOR NUM ( Izraz ) ) ⇒ ( OPERATOR NUM -NUM ) ⇒ ( OPERATOR NUM ( OPERATOR Operand Operand ) ) ⇒ ( OPERATOR NUM ( OPERATOR NUM Operand ) ) ⇒ ( OPERATOR NUM ( OPERATOR NUM NUM ) ) ⇒ ( OPERATOR NUM ( OPERATOR NUM NUM ) ) 295
  • 296.
    U svakoj rečeničnjformi podcrtan je simbol koji se zamjenjuje s desnom stranom njegove produkcije. Različitost mogućih derivacija je posljedica alternativnih definicija neterminalnog simbola Operand. Primijetite da je moguć beskonačan broj različitih derivacija jer je Izraz definiran rekurzivno (Izraz je definiran pomoću Operanda, a Operand pomoću Izraza). Prvi zadatak sintaktičke analize je otkriti da li postoji derivacija koja odgovara ulaznom nizu tokena. Ona se može vršiti na više načina. Ovdje će biti korištena tzv. "top-down" analiza, u kojoj se ulazni niz promatra s lijeva na desno, jedan po jedan token, i istovremeno vrši derivacija rečenične forme koja slijedi iz položaja tokena u ulaznom nizu. Pogledajmo najprije kako čovjek može izvršiti sintaktičku analizu. U "top-down" analizi polazi se od startnog simbola gramatike i analizira desna strana njegove produkcija, koja predstavlja temeljnu rečeničnu formu. Simboli iz rečenične forme se analiziraju jedan po jedan, s lijeva na desno, i uspoređuju s ulaznim tokenom. Na temelju usporedbe rečeničnog i ulaznog simbola, vrše se dvije radnje: Prihvati - Ako u rečeničnoj formi slijedi token, on mora biti jednak ulaznom tokenu. Ako je to zadovoljeno, dobavlja se sljedeći ulazni token, inače ulazni niz nije u skladu s gramatikom. Proširi - Ako u rečeničnoj formi slijedi neterminalni simbol, umjesto njega se u rečeničnu formu umeće desna strana produkcije. Međutim, ako je neterminalni simbol definiran s više alternativnih produkcija, tada se umeće ona alternativna produkcija koja generira rečenice koje mogu počinjati s ulaznim tokenon. Ako neterminalni simbol predstavlja opcioni simbol, odnosno simbol koji može generirati praznu rečenicu, tada se umjesto njega umeće desna strana produkcija samo u slučaju ako taj simbol generira rečenice koje mogu počinjati s ulaznim tokenom, inače se taj simbol odstranjuje i analizira se sljedeći simbol. Analiza se nastavlja sve dok sljedeći ulazni token ne postane posljednji token ulaznog niza. Analiza je uspješna ako rečenična forma isključivo sadrži tokene. Primjer ovakve analize prikazuje slika 19.7. Prvi stupac sadrži rečeničnu formu, koja nastaje tijekom derivacije. Drugi stupac opisuje operaciju kojem se prihvaća ili proširuje rečenični oblik. Treći stupac prikazuje sljedeći ulazni token. U svakoj rečeničnoj formi podebljan je token kojeg se prihvaća i podcrtan je neterminalni simbol kojeg se zamjenjuje desnom stranom produkcije. Derivacija rečenične forme za ulazni niz ( * 9 (+ 8 3) ) Operacija Ulaz Naredba proširi Naredba ( ⇒ ( Izraz ) prihvati: (, proširi Izraz * ⇒ ( OPERATOR Operand Operand ) prihvati OP, proširi Operand 9 ⇒ ( OPERATOR NUM Operand ) prihvati NUM, proširi Operand ( ⇒ ( OPERATOR NUM ( Izraz ) ) prihvati (, proširi Izraz + ⇒ ( OPERATOR NUM ( OPERATOR Operand Operand ) ) prihvati OP, proširi Operand 8 ⇒ ( OPERATOR NUM ( OPERATOR NUM Operand ) ) prihvati NUM, proširi Operand 3 ⇒ ( OPERATOR NUM ( OPERATOR NUM NUM ) ) prihvati NUM, prihvati ) ) ⇒ ( OPERATOR NUM ( OPERATOR NUM NUM ) ) prihvati ) . ) Slika 19.7 Top-down sintaktička analiza – gramatička derivacija za ulazni niz (*9(+8 3)). Prethodno izvršena analiza naziva se LL(1) analiza. Ovaj naziv slijedi iz činjenice da je ulazni niz analiziran s lijeva (L) i da je u rečeničnoj formi vršena supstitucija najljevijeg 296
  • 297.
    neterminalnog simbola (L)na temelju poznavanja samo jednog sljedećeg tokena (1). Gramatika, koja se može analizirati na ovaj način, naziva se LL(1) gramatika. Tri su temeljna zahtjeva da bi gramatika bila LL(1): 1. Neterminalni simbol ne smije biti definiran s više alternativnih produkcija koje počinju s istim simbolom, jer se tada ne može znati koju produkciju primijeniti na temelju poznavanja samo jednog sljedećeg ulaznog tokena. Primjerice, u produkciji A:tB|tC obje alternative počinju s istim tokenom t. Ova restrikcija se može lako otkloniti ako se produkcija transformira oblik A : t A1 A1 : B | C 2. Neterminalni simbol ne smije biti definiran produkcijom koja počinje s tim istim simbolom. Takve produkcije se nazivaju lijevo rekurzivne produkcije. Primjerice, u produkciji A: x | A+x neterminal A može biti definiran s x, zbog toga alternativa, A + x, također može početi s x. Prema tome, ne može se odrediti koju produkciju primijeniti. To vrijedi bez obzira koliko ulaznih tokena poznajemo, jer se A može proširiti s A+x proizvoljan broj puta. 3. Ako u nizu simbola postoji opcioni simbol, odnosno simbol koja generira praznu rečenicu, tada se mora razlikovati skup tokena s kojim mogu počinjati rečenice koje generira taj simbol i skup tokena s kojim mogu počinjati rečenice koje generira simbol koji slijedi iza njega. Ako to nije ispunjeno ne može se odrediti da li analizirati opcioni simbol ili simbol koji slijedi iza njega. Bez obzira na ove gramatičke restrikcije, LL(1) gramatika je korištena u izradi mnogih interpretera i kompilatora, jer se za ovaj tip gramatike može jednostavno napisati program koji vrši sintaktičku analizu. 19.3.4 Rekurzivno silazni parser Program koji obavlja sintaktičku analizu, za bilo koji ulazni niz tokena, naziva se sintaktički analizator ili parser. Parser za gramatiku tipa LL(1) može se realizirati u obliku koji se naziva rekurzivno silazni parser (eng, recursive descent parser). Program parsera se sastoji od potprograma koji imaju imena neterminalnih simbola gramatike. Zadatak je svakog potprograma, počevši od startnog simbola gramatike, da provjeri može li se "proširiti" rečenična forma s desnom stranom produkcije, a na temelju poznavanja sljedećeg ulaznog tokena. Formalno se taj postupak može opisati na sljedećim algoritmom: Algoritam: Definiranje rekurzivno silaznog parsera za LL(1) gramatiku 1. Ako je neterminalni simbol definiran produkcijom oblika A:α gdje je α općenito predstavlja niz od jedan ili više tokena ili neterminalnih simbola, potprogram A() ima oblik: A() { Analiziraj α ; } Analiziraj α; simbolički označava naredbu koja ima oblik: Ako je α token t, tada 297
  • 298.
    Analiziraj t; ≡ Ako je ulazni token jednak t, dobavi sljedeći ulazni token inače dojavi grešku Ako je α neterminalni simbol N, tada Analiziraj N; ≡ N(); /*poziv potprograma za neterminalni simbol N*/ Ako je α = X1 X2...Xn , gdje Xi može biti token ili neterminalni simbol, tada Analiziraj X1 X2...Xn ; ≡ Analiziraj X1 ; Analiziraj X2 ; ... Analiziraj Xn ; Ako je α opcioni simbol Xopt , tada Analiziraj Xopt ; ≡ Ako X generira rečenice koje mogu počinjati s ulaznim tokenom, tada Analiziraj X; 2. Ako je neterminalni simbol definiran s alternativnim produkcijama, potprogram prvo treba odrediti koja alternativna produkcija odgovara položaju sljedećeg ulaznog tokena, a zatim se provjerava odabrana produkcija. Formalno, ako produkcija ima oblik A: α1 | α2 | .. αn gdje su α1, α2.. αn desne strane alternativnih produkcija, potprogram A() ima oblik: A() { Ako α1 generira rečenice koje mogu počinjati s ulaznim tokenom Analiziraj α1; inače, ako α2 generira rečenice koje mogu počinjati s ulaznim tokenom Analiziraj α2; .... inače, ako αn generira rečenice koje mogu počinjati s ulaznim tokenom Analiziraj αn; inače, dojavi grešku } 3. Rekurzivni silazni parser se realizira pozivom potprograma koji ima ime startnog simbola gramatike. Taj potprogram se definira prema prethodna dva pravilu, uz dopunu da prva naredba mora biti dobava sljedećeg ulaznog tokena, koji se tretira kao globalna varijabla. 4. Dojava greške, u najprostijem obliku, je operacija kojom se ispisuje poruka o nastaloj greški i prekida izvršenje programa. Postoje naprednije metode obrade greške, u kojima se ne prekida izvršenje programa, međutim one neće biti opisane u ovoj knjizi. Primjer: Parser prefiksnih aritmetičkih izraza. Parser koristi funkciju getToken() za dobavu sljedećeg tokena, koji se bilježi u globalnoj varijabli next_token. Globalna varijabla tokenval sadrži vrijednost tokena. Funkcija za dojavu sintaktičke greške i prekid programa naziva se syn_error(). void syn_error(char *poruka) /* Dojava sintaktičke greške */ { /* u obliku "Greska: poruka" */ printf("nGreska: %s",poruka); 298
  • 299.
    exit(1); /* i prekid programa */ } int next_token; /* sljedeći ulazni token */ Za rad parsera definirane su tri funkcije: Naredba(), Izraz() i Operand(), čija imena su neterminalni simboli gramatike, a prema algoritmu za rekurzivno silazni parser. void Izraz(); void Operand(); void Naredba() /* Naredba : ( Izraz ) | QUIT*/ { next_token = getToken(); if(next_token == '(') { next_token = getToken(); Izraz(); if(next_token != ')') syn_error(" nedostaje ')' !"); } else if(next_token == QUIT) return; else syn_error(" nedozvoljen simbol!"); } void Izraz() /* Izraz : OPERATOR Operand Operand */ { if(next_token == OPERATOR) next_token = getToken(); else else syn_error(" nedozvoljen operator!"); Operand(); Operand(); } void Operand() /* Operand : NUM |-NUM | ( Izraz ) */ { if( next_token == NUM) { next_token = getToken(); } else if( next_token == OPERATOR && tokenval.op == '-') { next_token = getToken(); if( next_token == NUM) next_token = getToken(); else syn_error("pogresno unesen broj!"); } else if( next_token == '(') { next_token = getToken(); Izraz(); if(next_token == ')') next_token = getToken(); else syn_error(" nedostaje ')' !"); } else syn_error("pogresno unesen broj!"); } Postupak parsiranja se može provjeriti sljedećim programom: int main() { printf("Otkucaj prefiksni izraz, primjerice (*5(- 8 3.1))n"); Naredba(); 299
  • 300.
    printf("Parsiranje uspjesno!"); return 0; } Nakon izvršenja ovog programa korisnik će vidjeti samo dva moguća ispisa: Ako je izraz unesen u skladu s gramatikom bit će ispisano: Parsiranje uspjesno! inače, bit će dojavljena greška, primjerice u obliku: Greska: nedostaje ) ! Ovaj parser jedino zna prepoznti da li je ulazni niz u skladu s zadanom gramatikom. Međutim, mi želimo da on obavi još jednu radnju - da generira binarno stablo koje sadrži aritmetički izraz. Kako se to može obaviti, bit će pokazano u sljedećem odjeljku. 19.3.5 Semantičke akcije Radnje, koje slijede iz značenja gramatički ispravnog zapisa, nazivaju se semantičke akcije. U programskim jezicima često je dovoljno sagledati samo dijelove rečenice da bi se izvršile semantičke akcije. Primjerice, kod analize aritmetičkih izraza, koje želimo prikazati pomoću binarnog stabla, znamo da kada bude prepoznat token NUM tada treba formirati čvor koji je list binarnog stabla. Taj čvor sadrži atribut tokena NUM (tj. njegovu numeričku vrijednost). Postavlja se pitanje: kome pridjeliti pokazivač na taj čvor? Možemo rezonirati na sljedeći način: ako je prepoznat NUM, to ujedno znači da je prepoznata produkcija Operand : NUM, pa apstraktno možemo uzeti da Operand ima semantički atribut (ili vrijednost) koja je jednaka pokazivaču na čvor stabla. Slično rezoniramo i ako je prepoznat operator i dva operanda. Tada treba formirati unutarnji čvor binarnog stabla. Pošto to ujedno znači da je prepoznata produkcija za Izraz, možemo uzeti da Izraz ima semantički atribut koji je, također, pokazivač na čvor stabla. Konačno, možemo uzeti da simbol Naredba ima semantički atribut tipa pokazivača na čvor stabla, i to na korijen stabla, jer analizom produkcije Naredba : (Izraz) | QUIT, se dobije pokazivač na čvor stabla kojim počinje Izraz. Ako je pak prepoznata naredba QUIT, tada se ne formira stablo, pa u tom slučaju semantički atribut simbola Naredba mora biti NULL pokazivač. Uvedimo sada sljedeće oznake semantičkih atributa: • NUM:num je vrijednost tokena NUM (dobije se iz globalne varijable tokenval.num) • OPERATOR:op je vrijednost tokena OPERATOR (dobije se iz globalne varijable tokenval.op) • Naredba:t je pokazivač korijena binarnog stabla. • Izraz:t je pokazivač unutarnjeg čvora binarnog stabla, koji sadrži operator i pokazivače na operande. • Operand:t je pokazivač na čvor binarnog stabla, koji je ili list stabla ili pokazivač na čvor koji sadrži izraz. Sada se semantičke akcije mogu specificirati zajedno sa specifikacijom gramatike, kao što je prikazana na slici 19.8. Gramatika Pseudo kôd za semantičke akcije Naredba : 300
  • 301.
    ( Izraz ) Naredba:t = Izraz:t; | QUIT Naredba:t = NULL; Izraz : OPERATOR Operand Operand Izraz:t = tree_opnode(OPERATOR:op, Operand : Operand:t1, Operand:t2); NUM | - NUM Operand:t = tree_numnode( NUM:num); | ( Izraz ) Operand:t = tree_numnode( NUM:num); Operand:t = Izraz:t; Slika 19. 8 Semantičke akcije za generiranje stabla s aritmetičkim izrazima. Prikazane semantičke akcije se mogu uvrstiti u izvorni kôd parsera na sljedeći način: 1. Za sve neterminalne simbole, kojima je atribut stablo izraza, deklariraju se parsne funkcije koje vraćaju pokazivač stabla. Deklaracija triju funkcija parsera glasi: TREE Naredba(); TREE Izraz(); TREE Operand(); Vrijednost koju ove funkcije vraćaju je vrijednost semantičkog atributa odgovarajućih terminalnih simbola, primjerice, vrijednost koju vraća funkcija Izraz() u pseudo kôdu semantičkih akcija je označena s Izraz:t. 2. Unutar definicije funkcija parsera na prikladan način se umeću semantičke akcije koje su definirane na slici 19.5. Primjerice, funkcija Izraz() ima oblik: TREE Izraz() { TREE t1,t2, t = NULL; if(next_token == OPERATOR) { int op = tokenval.op; next_token = getToken(); t1 = Operand(); t2 = Operand(); t = make_opnode(op, t1, t2); } else syn_error(" nedozvoljen operator!"); return t; } Podebljano je prikazan kôd semantičkih akcija. Unutar tijela funkcije definirane su tri varijable tipa TREE. Varijabla t je semantički atribut simbola Izraz, i njenu vrijednost vraća ova funkcija. Varijable t1 i t2 su semantički atributi simbola Operand, a dobiju se pozivom funkcije Operand(). Formiranje čvora stabla se vrši tek kada su "prepoznati" operator i oba operanda. Vrijednost semantičkog atributa tokena OPERATOR se bilježi u varijabli op. Sličan je postupak definiranja funkcija Naredba() i Operand(). Kompletni izvorni kôd je u programu prefix_int.c. 301
  • 302.
    /* Datoteka: prefix_int.c #include<stdio.h> #include <stdlib.h> #include "prefix_lex.h" #include "prefix_tree.h" void syn_error(char *str) /* dojava greške */ { printf("nGreska: %s",str); exit(1); } int next_token; TREE Izraz(); TREE Operand(); TREE Naredba () /* Naredba : ( Izraz ) | QUIT */ { TREE t; next_token = getToken(); if(next_token == '(') { next_token = getToken(); t = Izraz(); if(next_token != ')') syn_error(" nedostaje ')' !"); } else if(next_token == QUIT) return NULL; else syn_error(" nedozvoljen simbol !"); return t; } TREE Izraz() /* Izraz : OPERATOR Operand Operand */ { TREE t1,t2, t = NULL; if(next_token == OPERATOR) { int op = tokenval.op; next_token = getToken(); t1 = Operand(); t2 = Operand(); t = make_opnode(op, t1, t2); } else syn_error(" nedozvoljen operator!"); return t; } TREE Operand () /* Operand : NUM | - NUM | (Izraz)*/ { TREE t = NULL; if( next_token == '(') { next_token = getToken(); 302
  • 303.
    t = Izraz(); if(next_token != ')') syn_error(" nedostaje ')' !"); else next_token = getToken(); } else if( next_token == NUM) { t = make_numnode(tokenval.num); next_token = getToken(); } else if( next_token == OPERATOR && tokenval.op == '-') { next_token = getToken(); if( next_token == NUM) { t = make_numnode(-tokenval.num); next_token = getToken(); } else syn_error("krivo unesen broj !"); } else syn_error("krivo unesen broj !"); return t; } int main(int argc, char **argv) { double rez; TREE t; if(argc == 2) { /* unos iz datoteke */ input = fopen(argv[1],"r"); if (input == NULL) { printf("Ne postoji datoteka imena: %s", argv[1]); exit(1); } } else /* unos tipkovnicom - input == stdin*/ printf( "Unesite prefiksni aritmeticki izraz ili $ za krajn>"); while (1) { t = Naredba(); if( t == NULL) break; rez = evaluate(t); if(input == stdin) /* unos tipkovnicom */ printf("nRezultat: %fn>", evaluate(t)); else { /* unos iz datoteke */ print_prefiks (t); printf("n=%fn", evaluate(t)); } t = tree_delete(t); } if(input != stdin) fclose(input); return 0; } Program se kompilira komandom: 303
  • 304.
    c:> cl prefix_int.cprefix_tree.c prefix_lex.c Program "prefix_int.exe" se može koristiti na dva načina. Prvi je način da se program pozove bez argumenata komandne linije. Tada program čeka da korisnik unese aritmetički izraz ili znak za kraj unosa ($) s tipkovnice. Drugi je način da se program pozove s argumentom koji je ime datoteke u kojoj je zapisano više prefiksnih izraza, primjerice, komandom c:> prefix_int pr.txt analizira se tekstualna datoteka "pr.txt". Ako ona ima sljedeći sadržaj: (* 8 9) (* 8 (+ 8 9)) (+ 8 (+ 8 9)) program ispisuje rezultat u obliku: ( * 8.000000 9.000000 ) =72.000000 ( * 8.000000 ( + 8.000000 9.000000 ) ) =136.000000 ( + 8.000000 ( + 8.000000 9.000000 ) ) =25.000000 Primijetite da u datoteci "pr.txt" nije korišten znak $, koji pri unosu s tipkovnice predstavlja naredbu za kraj unosa, jer program prihvaća i znak za kraj datoteke (EOF) kao naredbu za kraja unosa. Zadatak: Napišite interpreter infiksnih izraza, za kojeg vrijedi sljedeća gramatika: Naredba : Izraz ENTER ; Izraz : Clan '+' Izraz | Clan '-' Izraz | Clan Clan : Faktor '*' Clan | Faktor '/' Clan | Faktor Faktor : NUM | - NUM | ( Izraz ) Tokeni su: ENTER - znak nove linije NUM - realni broj +,-,*,/ - operatori Interpreter očekuje da korisnik otkuca aritmetički izraz u infiksnom obliku, primjerice 67.8*(7-9) i da pritisne <enter>, tj znak nove linije. Nakon toga interpreter ispisuje rezultat. U analizi ovog zadatka prvo treba primijetiti da prethodna gramatika nije LL(1) jer u drugom i trećem pravilu postoje zajednički lijevi prefiksi. Zbog toga, treba koristiti modificiranu gramatiku: Naredba : Izraz ENTER Izraz : Clan IzrazOptopt IzrazOpt : '+' Izraz | '-' Izraz Clan : Faktor ClanOptopt ClanOpt: '*' Clan 304
  • 305.
    | '/' Clan Faktor : NUM | - NUM | ( Izraz ) Ovo je LL(1) gramatika i za nju se može napisati rekurzivni silazni parser. Primjerice, funkcija parsera za pravilo Clan ima oblik: TREE Clan () { TREE t2, t1; t1 = Factor(); if(next_token == '*' || next_token = '/') { int tok = next_token; t2 = ClanOpt(); return make_tnode(tok, t1, t2); } else return t1; } TREE ClanOpt() { if(is_next_token('*')) { match('*'); return Clan (); } else if(is_next_token('/')) { match('/'); return Clan (); } else syn_error("greška") } Napišite ostale funkcije parsera i izvršite potrebne izmjene u leksičkom analizatoru (funkcija getToken() treba vraćati poseban token za sve operatore i za znak nove linije). 19.4 Stabla s proizvoljnim brojem grana Ako čvorovi stabla imaju više djece, kao u M-stupanjskom stablu tada se oni mogu opisati samoreferentnom strukturom Node koja sadrži niz od M pokazivača na djecu čvora. #define NUM_CHILD M typedef int elemT: typedef struct _node { void * data; struct _node * child[NUM_CHILD]; } Node; Čvor se formira pomoću funkcije makeNode(): Node * makeNode( elemT elem, Node *N0, Node *N1,..., Node *Nm) { Node *n=(Node *) malloc(sizeof(Node)); if(n) { n->elem = elem; n->child[0] = N0; 305
  • 306.
    n->child[1] = N1; ..... n->child[m] = Nm; } return n; } Ako podstabla imaju različite stupnjeva, zovemo ih višesmjerna stabla (eng. multiway tree). U tom slučaju može se koristiti prethodna struktura tako da se svi child[i] pokazivači postave jednakim nuli za i-smjerni čvor. Primjerice, dvosmjerni čvor bi formirali naredbom: makeNode( elem, N0, N1, NULL,..,NULL); Loša strana ovakvog pristupa je da se troši memorija za pokazivače nepostojećih čvorova. Taj se problem može riješiti na dva načina. Prvi je da se i niz child alocira dinamički te da se u strukturi Node bilježi broj alociranih čorova. Tada struktura Node može biti ovakva: typedef struct _node { elemT elem; int num_child; /*broj alociranih čvorova*/ struct _node **child; /*pokazivač na niz pokazivača*/ } Node; Drugi način za stvaranje čvorova višesmjernog stabla je da se koriste dva pokazivača. Prvi, nazvat ćemo ga first_children, pokazuje prvo dijete čvora, a drugi, nazvat ćemo ga next_sibling pokazuje na listu braće prvog djeteta. typedef struct _node { elemT elem; struct _node *first_child; struct _node *next_sibling; } Node; Dakle, next_sibling je pokazivač glave jednostruko vezane liste, koja sadrži djecu čvora osim prvog djeteta. Kraj liste se uvijek označava NULL pokazivačem. Za stvaranje čvorova i uspostavljanje veza među njima mogu poslužiti sljedeće funkcije: Node *makeLeaf(elemT elem) { /*stvaranje lista*/ Node *n= (Node*)malloc(sizeof(Node)); if(n) { n->first_child = n->next_sibling = 0; n->elem = elem; } return n; } Node * make1ChildNode(elemT elem, Node *N1) { /*čvor s jednim djetetom*/ Node *n=makeLeaf(elem); if(n) n->first_child = N1; return n; } Node * make2ChildNode(elemT elem, Node *N1, Node *N2) { /*čvor s dva djeteta*/ Node *n=makeLeaf(elem); if(n) { 306
  • 307.
    n->first_child = N1; N1->next_sibling = N2; N2->next_sibling = 0; } return n; } Node * make3ChildNode(elemT elem, Node *N1, Node *N2, Node *N3) { /*čvor s tri djeteta*/ Node *n=makeLeaf(elem); if(n) { n->first_child = N1; N1->next_sibling = N2; N2->next_sibling = N3; N3->next_sibling = 0; } return n; } itd. Veze čvora zapisane u Veze čvora zapisane u Ekvivalentno binarno Node *child[3]; Node *first-child stablo Node *next-sibling 1 1 1 /| / / / | / 2 / | / / 2 3 4 2---3---4 5 3 / | / / 5 6 7 5---6 7 6 4 / / / 8 9 8---9 7 / 8 a) b) c) 9 Slika 19.9 Tri načina zapisa stabla kojem čvorovi imaju promjenjljivi broj djece. a) pokazivači čvorova su u nizu child[], b) pokazivači čvorova su first_child i elementi liste next_sibling, c) ekvivalentno binarno stablo koje se dobije iz drugog oblika rotiranjem liste čvorova braće za 45o u smjeru okretanja kazaljke sata. Važno je primijetiti da zapis stabla pomoću first_child i next_sibling čvorova ima ekvivalentan zapis pomoću binarnog stabla. To je pokazano na slici 19.9. Stablo je oformljeno naredbama: Node *N1 = make2ChildNode (7, makeLeaf(8), makeLeaf(9)); Node *N2 = make1ChildNode (4, N1); Node *N3 = make2ChildNode (2, makeLeaf(5), makeLeaf(6)); Node *N0 = make3ChildNode (1, N3, makeLeaf(3), N2); Ekvivalentno binarno stablo se dobije iz višesmjernog stabla (first_child-next_sibling tipa) "rotiranjem" veza next_sibling liste za 45o u smjeru okretanja kazaljke sata. 307
  • 308.
    Definicija: Višesmjerno stabloT se može predstaviti odgovarajućim binarnim stablom B. Ako su {n1,..., nk} čvorovi T, a {n'1,..., n'k} čvorovi B, tada čvor nk odgovara čvoru n'k. Oni imaju isti sadržaj. Ako je nk korijen od T, tada je n'k korijen od B. Veze među čvorovima su: • Ako je nl prvo dijete od nk, tada je n'l lijevo dijete od n'k. (Ako nk nema djece, tada n'k nema lijevo dijete.) • Ako je ns sljedeći neposredno desni brat od nk, tada je n's desno dijete od n'k. Bez obzira na ovu ekvivalentnost, sva tri opisana oblika implementacije višesmjernog stabla imaju različiti apstraktni značaj pa se i koriste u različitim primjenama. Zapis s nizom pokazivača djece često se koristi kod izrade sintaktičkih stabala, a zapis s first_child i next_sibling pokazivačima je uobičajen kod interpretetera jezika LISP i SCHEME. Algoritmi za obilazak višesmjernog stabla slični su algoritmima binarnog stabla, i lako se definiraju rekurzivno: Algoritam: visina stabla kojem je korijen n tree_height(n) = Ako je n != 0 tada vrati 0, inače vrati 1 + MAX(tree_height (child1), … tree_height (childn) ) Algoritam: veličina stabla kojem je korijen n tree_size(n) = Ako je čvor n != 0 tada vrati 0, inače vrati 1 + tree_size(child1) + … + tree_size(childn) Algoritam: obilazak stabla - postorder postorder (n) = Ako je čvor n != 0 tada za svako dijete od n pozovi postorder(dijete). Posjeti čvor. Algoritam: obilazak stabla - preorder preorder (n) = Ako je čvor n != 0 tada Posjeti čvor n, zatim za svako dijete od n pozovi postorder(dijete). Primjer: funkcija postorder() vrši postorder obilazak stabla i na čvorove primjenjuje funkciju (*visit)(). void postorder(Node * n, void (*visit)(Node *)) { Node * c; if (n == NULL) return; c = n->first_child; while (c != NULL) { postorder(c, visit); c = c->next_sibling; } visit(n); } Stablo, kojem je pokazivač korijena t, možemo izbrisati naredbom postorder(t, free);. Primjer: funkcija tree_size() vrši postorder obilazak stabla i vraća broj čvorova stabla. int tree_size(Node * n) { /* postorder obilazak */ 308
  • 309.
    Node * c;int m; if (n == NULL) return 0; m = 1; c = n->first_child; while (c != NULL) { m += tree_size(c); c = c->next_sibling; } return m; } Primjer: funkcija print_prefiks()vrši preorder obilazak stabla i ispisuje prefiksnu notaciju stabla. void print_prefiks (Node * n) { Node *x; if(n == NULL) { printf(" () "); return;} if(isLeaf(n)) { printf("%d ",n->data); return; } printf("( %d ",n->data); x = n->first_child; while(x != 0) { print_prefiks (x); x = x->next_sibling; } printf(" )"); } Ako se ova funkcija primijeni na stablo koje je prikazano na slici 19.9 (print_prefiks(N0);), dobije se ispis: ( 1 ( 2 5 6 ) 3 ( 4 ( 7 8 9 ) ) ) Pitanje: Zašto kod višesmjernih stabala nema smisla "inorder" obilazak stabla? Obilazak stabla može biti i proizvoljan. Tada obično korisnik ispituje sadržaj čvora i na temelju toga određuje u kojem smjeru će dalje obilaziti stablo. 19.5 Prioritetni redovi i hrpe U prethodnim odjeljcima stabla su programski implementirana pomoću pokazivača i samoreferentnih struktura. Sada će biti pokazan primjer u kojem se stabla mogu efikasno implementirati pomoću nizova. Analizirat će se posebni tipovi stabala, odnosno nizova, koji se nazivaju hrpa i pomoću kojih će biti realizirani tzv. prioritetni redovi (eng. priority queue). Najjednostavnije kazano, prioritetni red je skup podataka u kojem se svakom elementu skupa pridružuje oznaka prioriteta . Prioritetni redovi se često koriste. Evo dva primjera: Primjer 1: Pacijenti ulaze u čekaonicu, te iz nje odlaze liječniku na pregled. Prvi na redu za liječnika nije onaj koji je prvi ušao, već onaj čije je stanje najteže. Primjer 2: U računalu više procesa čeka u redu za izvršenje. Redoslijed kojim se izvršavaju procesi određen je prioritetom procesa. Procesor uzima iz reda program s najvećim prioritetom, te ga izvodi. U odnosu na obične redove, koji sadrže neki element x, prioritetni redovi sadrže elemente koji imaju dva atributa: ključ(x) i vrijednost(x). Ključ simbola određuje njegov prioritet. Najčešće se uzima da najveći prioritet ima simbol s najmanjim ključem, ali ponekad se uzima i obrnuto, da najveći prioritet ima simbol s najvećim ključem. 309
  • 310.
    Kod prioritetnog redavažne su samo dvije operacije : ubaci element i izbaci element s najmanjim ključom (ili alternativno, s najvećim ključem). Ovdje ćemo pokazati izvedbu apstraktnog dinamičkog tipa PQUEUE koji je definiran na sljedeći način: ADT PQUEQUE insert(Q, x) - ubacuje element x u red Q. delete_min(Q) - vraća najmanji element reda i izbacuje ga iz reda Q. Operacija nije definirana ako Q ne sadrži ni jedan element, tj ako je size(Q) = 0. size(Q) - vraća broj elemenata u prioritetnom redu Q. Ovaj ADT se može jednostavno realizirati pomoću linearne sortirane liste. U tom slučaju operacija delete_min(), u kojoj se pronalazi i izbacuje prvi element u listi, ima vremensku složenost O(1). Operacija insert() mora ubaciti novi element na “sortirano mjesto”. To znači da u prosjeku treba obići bar pola sortirane liste, pa je vremenska složenost operacije insert() O(n). Efikasnija implementacija se može napraviti pomoću hrpe (eng. heap). Hrpa je naziv za potpuno binarno stablo koje je zapisano pomoću niza elemenata, prema sljedećim pravilima: 1. Pravilo rasporeda: Potpuno stablo s N čvorova, koji sadrže elemente tipa <ključ,vrijednost>, može se bilježiti u nizu elemenata, na indeksima i = 1,2,..N-1, prema sljdedećem pravilu: Ako element indeksa i predstavlja čvor stabla, tada - element indeksa 2*i+1 predstavlja lijevo dijete čvora, - element indeksa 2*i+2 predstavlja desno dijete čvora, - element indeksa (i-1)/2 predstavlja roditelja čvora, 2. Pravilo hrpe: Ključ roditalja nekog čvora uvijek je manji od ključa tog čvora, pa je najmanji ključ u korijenu stabla. 13 21 16 24 31 19 69 65 26 32 13 21 16 24 31 19 69 65 26 32 Slika 19.10 Potpuno binarno stablo i ekvivalentni niz hrpe. Na slici 19.10 prikazano je potpuno binarno stablo i ekvivalentni niz hrpe. Uočimo: - indeks namanjeg elementa (koji je u korijenu stabla) jednak je i = 0. - indeks kranjeg elementa je N-1. To je krajnji desni list stabla. Kapacitet niza treba biti veći od broja elemenata niza. Zbog toga je povoljno da se niz realizira kao dinamički niz koji se može povećavati (realocirati) ako, pri operaciji umetanja, broj elemenata niza dosegne kapacitet niza. 310
  • 311.
    Postupno će bitiopisana implementacija ADT PQUEUE na temelju specifikacije koja je opisana u datoteci "pq.h". /* Datoteka: pqueue.h */ /* Specifikacija ADT PQUEUE */ typedef struct pq_elem { /* element prioritetnog reda */ int key; /* ključ za postavljanje prioriteta */ void *val; /* vrijednost elementa (bilo što) */ } elemT; PQUEUE pqueue_new(int capacity) /* Konstruktor ADT-a. * Argument: capacity - određuje početni kapacitet hrepe * Vraća: pokazivač ADT-a */ void pqueue_free(PQUEUE pQ) /*Destruktor objekta pQ*/ unsigned pqueue_size(PQUEUE pQ); /*Vraća broj elemenata u redu pQ*/ void pqueue_insert(PQUEUE pQ, elemT x); /* Umeće element na kojeg pokazuje px u red * Argumenti: pQ - red * x - element kojeg se umeće */ elemT pqueue_delete_min(PQUEUE pQ); /* Briše najmanji element iz reda PQ * Vraća: element koji je izbrisan */ Implementacija ADT PQUEUE je opisana u datoteci "pq.c". /* Datoteka: pq.c */ #include <stdlib.h> #include <stdio.h> #include <memory.h> #include <assert.h> #include "pq.h" typedef struct pq { int capacity; /* kapacitet PQUEUE */ int N; /* broj umetnutih elemenata */ elemT *elements; /* niz elemenata reda */ } PQueue, *PQUEUE; PQUEUE pqueue_new(int capacity) { PQUEUE pq = (PQUEUE)malloc(sizeof(PQueue)); if(pq) { pq->N=0; pq->capacity=capacity; pq->elements = malloc(sizeof(elemT)*capacity); if(!pq->elements) exit(1); } return pq; 311
  • 312.
    } void pqueue_free(PQUEUE pQ) { /*Destruktor ADT-a*/ assert(pQ); free(pQ->elements); free(pQ); } 19.5.1 Umetanje elementa u prioritetni red Ako želimo dodati element u prioritetni red možemo ga dodati na kraj niza. To je ekvivalentno dodavanju krajnjeg desnog lista u stablu (prikazan crtkano na slici 19. 10). Ta operacija ima za posljedicu da se N uveća za jedan, ali i da možda stablo ne zadovoljava pravilo hrpe. Da bi se zadovoljilo pravilo hrpe koristi se postupak, ilustriran na slici 19.11, opisan u sljedećem algoritmu: Algoritam: Umetanje elementa x u hrpu 1. Element iza krajnjeg elementa niza se tretira kao prazan (to je sljedeći krajnji desni list stabla). Podrazumijeva se da je kapacitet niza veći od broja elemenata u nizu. 2. Ako je ključ roditelja praznog čvora manji od ključa elementa x, tada se element x upisuje u prazni čvor. Time postupak završen. 3. Ako je ključ roditelja praznog čvora veći od ključa elementa x, tada se element roditelja kopira u prazni čvor, a čvor u kojem je bio roditelj se uzima kao prazni čvor. Postupak se ponavlja korakom 2. 13 17 21 16 17 24 31 19 69 65 26 32 13 21 16 24 31 19 69 65 26 32 Slika 19.11 Stablo sa slike 19.10 prilikom dodavanja ključa 17. Strelice pokazuju koje elemente treba pomaknuti da bi se otvorilo mjesto za novi element. Prethodni algoritam je primijenjen u funkciji pqueue_insert(). U toj funkciji se najprije provjerava da li je popunjen kapacitet reda. Ako je popunjen, dvostruko se povećava kapacitet reda. Nakon toga se vrši umetanje elementa prema prethodnome algoritmu. void pqueue_insert(PQUEUE pQ, elemT x) { int i; assert(pQ); /* provjeri kapacitet reda i povećaj ga ako je N>=kapacitet*/ if ( pQ->N >= pQ->capacity-1 ) { pQ->capacity *= 2; /*udvostruèi kapacitet hrpe*/ pQ->elements = (elemT *) realloc(pQ->elements, sizeof(elemT)*pQ->capacity); if(!pQ->elements) exit(1); } 312
  • 313.
    /* umetni elementx i povećaj N */ i = pQ->N++; while (i) { int parent = (i-1)/2; if (x.key > pQ->elements[parent].key) break; pQ->elements[i].key = pQ->elements[parent].key; i = parent; } pQ->elements[i] = x; } 19.5.2 Brisanje elementa iz prioritetnog reda Brisanje elementa iz prioritetnog reda se vrši samo na jedan način: briše se element s minimalnim ključem pomoću funkcije delete_min(). Minimalni element se nalazi u korijenu stabla, odnosno u hrpi na indeksu 0. Ako ga izbrišemo tada je korijen prazan i u njega treba zapisati neki drugi element. Najzgodnije je u njega zapisati krajnji element niza i smanjiti veličinu hrpe. Problem je što tada možda nije zadovoljeno pravilo hrpe. Dovođenje niza u stanje koje zadovoljava pravilo hrpe vrši se funkcijom heapify(), prema sljedećem algoritmu: Algoritam: uređenje hrpe kada vrh hrpe nije u skladu s pravilom hrpe 1. Započni s indeksom koji predstavlja vrh hrpe. Spremi taj element u varijablu x. Nadalje se uzima da je vrh hrpe "prazan". 2. Analiziraj djecu praznog čvora i odredi koje dijete je manje. 3. Ako je ključ od x manji od ključa manjeg djeteta spremi x u prazan čvor i završi, inače upiši element manjeg djeteta u prazan čvor i postavi da čvor manjeg djeteta bude prazan čvor. Ponovi korak 2. void heapify(elemT *elements,int vrh, int N) { int min_child, i = vrh; /*zapamti element kojem tražimo mjesto u hrpi */ elemT x = elements[i]; /* (i) označava "prazni" element*/ while (i < N/2) { int left = min_child = 2*i+1; int right = left + 1; /* Prvo odredi indeks manjeg djeteta - min_child */ if ( (left < N-1) && (elements[left].key > elements[right].key) ) min_child = right; /* Ako je min_child manji od x, upiši ga na prazno mjesto (i), inače break */ if ( x.key < elements[min_child].key) break; elements[i] = elements[min_child]; i = min_child; } /* konačno stavi x na prazno mjesto*/ elements[i] = x; } Sada je implementacija funkcije pqueue_delete_min() jednostavna. u njoj je najprije zapamćen minimalni element, koji je na indeksu 0, u varijabli minimum. Zatim je krajnji element upisan na početak niza, a veličina hrpe je smanjena. Slijedi poziv funkcije heapify(), kojom se uređuje hrpa, ako je poremećeno pravilo hrpe. Postupak brisanja elementa je ilustriran na slici 19.12. 313
  • 314.
    elemT pqueue_delete_min(PQUEUE pQ) { elemT minimum; assert(pQ); assert( pQ->N > 0 ); /* minimum je u korijenu (indeks 0) */ minimum = pQ->elements[0]; /* zadnji element prebaci u korijen i smanji veličinu hrpe*/ pQ->elements[0] = pQ->elements[--pQ->N]; /* preuredi hrpu od vrha prema dnu (vrh je na indeksu 0) */ heapify(pQ->elements, 0, pQ->N); return minimum; } 13 minumum 16 16 X > 16 17 16 17 X > 19 17 x 19 24 21 19 69 24 21 19 69 24 21 31 69 65 26 32 31 X = 31 65 26 32 65 26 32 Slika 19.12 Primjer kako se izvršava operacija delete_min(). Iz reda se odstranjuje ključ 13 koji je na vrhu hrpe. Strelice pokazuju koje elemente treba pomaknuti da bi se otvorilo mjesto za element s kraja hrpe ( x=31). Testiranje se provodi programom "pq-test.c". U programu se najprije 10 slučajno generiranih brojeva upisuje u prioritetni red pQ, a zatim se briše element po element i ispisuje vrijednost ključa. /* Datoteka: pq-test.c*/ /* 1. Generira se 10 slučajnih brojeva i sprema u pqueue * 2. Briše iz pqueue i ispisuje element po element */ #include <stdlib.h> #include <stdio.h> #include <memory.h> #include <assert.h> #include "pq.h" int main() { int i; elemT elem; PQUEUE pQ =pqueue_new(20); printf("Slucajno generirani brojevi:n"); for(i=0; i<10; i++) { elem.key = rand() % 100 +1; pqueue_insert(pQ, elem); printf("%d ", elem.key); } 314
  • 315.
    printf("nIzlaz iz prioritetnogreda:n"); while(pqueue_size(pQ)) { elem = pqueue_delete_min(pQ); printf("%d ", elem.key); } pqueue_free(pQ); printf("n"); return 0; } Kada se program izvrši dobije se ispis: Slucajno generirani brojevi: 42 68 35 1 70 25 79 59 63 65 Izlaz iz prioritetnog reda: 1 25 35 42 59 63 65 68 70 79 Vidimo da je izlaz sortiran, jer smo iz reda brisali jedan po jedan minimalni element. Vremenska složenost operacija insert() i delete_min() je malena jer se obje operacije provode samo po jednoj stazi od vrha do dna hrpe ili obrnuto. Kod potpunog stabla duljina staze iznosi log2(n+1)-1. Pošto nas zanima asimptotska vrijednost (za veliki n), vremenska složenost iznosi O(log2n). 19.5.3 Sortiranje pomoću prioritetnog reda i heapsort metoda Prethodni prinjer testiranja ADT PQUEUE pokazuje kako se može izvršiti sortiranje pomoću prioritetnog reda Algoritam: sortiranje niza A pomoću prioritetnog reda 1. formiraj objekt tipa PQUEUE pQ. 2. za sve elemente niza A[i], i =0,1,..N-1 pqueue_insert(pQ, A[i]) 3. za sve i =0,1,..N-1 A[i] = pqueue_deletemin(pQ); 4. dealociraj objekt pQ Ovaj algoritam ima vremensku složenost O(n logn), jer se u njemu n puta ponavljaju operacije umetanja i brisanja, a one imaju složenost O(logn). Loša strana algoritma je da zahtijeva dodatnu memoriju veličine originalnog niza. Funkcija heapify() se može efikasno upotrijebiti za sortiranje nizova bez upotrebe dodatne memorije. Algoritam je jednostavan i primijenjen je u funkciji heapsort(). Sastoji se od dva koraka. U prvom koraku se od proizvoljnog niza stvara hrpa. Funkcija heapify() se primjenjuje n/2 puta, na svim unutarnjim čvorovima, najprije na najmanjem podstablu, koji je trivijalno potpuno stablo, a zatim iterativno na svim većim podstablima do samog korijena. U drugom koraku se element nultog indeksa (koji je u ovom slučaju minimalan) iterativno zamjenjuje s krajnjim elementom, a zatim se funkcija heapify() primjenjuje na niz s umanjenim brojem elemenata. void heapsort(elemT *A, int N ) { /* sortira niz A duljine N -> A[i] > A[i+1]*/ int i; /*1. stvari hrpu od postojeceg niza */ 315
  • 316.
    for(i = N/2-1;i >= 0; i--) heapify( A, i, N ); /*2. zamijeni minimum s posljednjim elementom i uredi hrpu */ for(i = N-1; i>0; i-- ) { elemT tmp = A[i]; A[i]=A[0]; A[0]= tmp; heapify( A, 0, i); } } Ova verzija heapsort() funkcije sortira nizove od veće prema manjoj vrijednosti. Ako bi trebali suprotan raspored elemenata tada treba izmijeniti funkciju heapify(), tako da ona stvara hrpu s najvećim elementom na vrhu hrpe. Izmjena je jednostavna - treba u naredbama usporedbe zamijeniti operatore < i >. Vremenska složenost heapsort metode je O(nlog2n) i to u najgorem slučaju, jer se u njoj funkcija heapify() poziva maksimalno 3n/2 puta. Brzina izvršenja u prosječnom slučaju je nešto sporija nego kod primjene funkcije quicksort(), ali postoje slučajevi kada heapsort() daje najveću brzinu izvršenja. Zadatak: Napišite verziju ADT-a za prioritetni red PQ kojem je na vrhu hrpe element s maksimalnim ključem. ADT PQ insert(Q, x) - ubacuje element x u red Q. delete_max(Q,) - vraća najveći element reda i izbacuje ga iz reda Q Operacija nije definirana ako Q ne sadrži ni jedan element, tj ako je size(Q) = 0. size(Q) - vraća broj elemenata u prioritetnom redu Q. 19.6 Zaključak Stabla omogućuju jednostavnu apstrakciju različitih podatkovnih struktura, od slike porodičnog stabla do jezičkih sintaktičkih i semantičkih veza. U sljedećem poglavlju bit će pokazano da stabla omogućuju stvaranje općih podatkovnih struktura tipa tablice, rječnika i skupova. Stabla se najčešće koriste u obliku binarnog stabla. Pokazano je da se višesmjerna stabla mogu prikazati ekvivalentnim binarnim stablima. Pokazana su dva načina programske implementacije stabla. U prvoj se stablo realizira pomoću samoreferentnih struktura koje sadrže pokazivače, a u drugoj se stablo realizira pomoću prostih nizova. Korištenje stabala je uvijek povezano uz neku primjenu za koju se definira potreban skup operacija. Pokazana je potpuna realizacija interpretera prefiksnih aritmetičkih izraza, a pokazano je i kako se može napraviti interpreter prefiksnih izraza. Izneseni su temelji sintaktičke analize jednostavnih jezičkih konstrukcija koje zadovoljavaju LL(1) tip gramatike. Pokazano je kako se realizira jednostavni leksički analizator i kako se iz BNF notacije gramatike realizira rekurzivno silazni parser. Opisana je izvedba apstraktnog tipa podataka PQUEUE kojom se stvaraju prioritetni redovi. Korištena je struktura tipa hrpe i pomoću nje je realizirana metoda sortiranja koja se naziva heapsort. 316
  • 317.
    20 Strukture zabrzo traženje podataka Naglasci: • Tablice simbola i rječnici • Hash tablica • Otvorena hash tablica • Zatvorena hash tablica • BST - binarno stablo traženja • RBT - crveno-crno stablo 20.1 Tablice simbola i rječnici Tablice simbola i rječnici imaju sličnu karakteristiku, a to je da svaki simbol ima neku ključnu oznaku (ključ) i vrijednost (ili definiciju) koja opisuje značaj simbola. Može se i reći da je tablica simbola skup parova <ključ, vrijednost>. Ključ je najčešće tipa string, ali može biti i numerička vrijednost. Tri su temeljne operacije koje definiraju ADT tablice (ili rječnika): ADT TABLE insert (T, k, v) - umeće simbol, ključa k i vrijednosti v u tablicu T, ako već nije u T. find (T, k) - vraća vrijednost simbola ako u T postoji ključ k, delete(T, k) - iz tablice T briše simbol kojem je ključ k, ako postoji . Brzina ovih operacija ovisna je o tome koja struktura podataka je upotrijebljena za tablicu simbola. Za implementaciju tablice moguće je koristiti nizove i liste. To ćemo pokazati primjerom. Primjer: Implementacija tablice pomoću niza. Simbol opisuje struktura: typedef struct symbol { char *key; /* ključ simbola */ void *val; /* pokazivač vrijednost simbola */ }Symbol; Tablica se sastoji od niza simbola : typedef struct _table { Symbol *array; /* alocirani niz simbola */ int N; /* broj simbola u nizu */ int M; /* kapacitet alociranog niza */ } *TABLE Primjer funkcije za traženje: 317
  • 318.
    Symbol *find(TABLE T,char *key) { /* vraća pokazivač simbola ili NULL */ int i; for (i = 0; i < T->N; i++) if(!strcmp(T->array[i].key, key); return &T->array[i]; return NULL; } /*sami napišite funkcije insert() i delete()*/ Zbog jednostavnosti uzeto je da je ključ tipa string i da je vrijednost simbola označana pokazivačem tipa void * (dakle, pokazivačem na bilo koji tip podatka). Primjer: Implementacija tablice pomoću vezane liste se može temeljiti na sljedećim strukturama: typedef struct symbol { struct symbol *next /* veza u listi */ char * key; /* ključ simbola */ void * val; /* pokazivač vrijednost simbola */ }Symbol; typedef struct _table { Symbol *list; /* tablica je lista*/ } *TABLE Symbol *find(TABLE T, char * key) { Symbol *L = T->list; while( L != NULL) { if (!strcmp(L->key, key)) return L; L = L->next; } return NULL; } U oba slučaja kompleksnost algoritma traženja je O(n) pa ovakova rješenja mogu zadovoljiti samo za implementaciju manjih tablica. Kada je potrebno imati veće tablice cilj je smanjiti kompleksnost na O(log n) ili čak na O(1). Ta poboljšanja se mogu ostvariti pomoću tzv. "hash" tablica i specijano izvedenih stabala za traženje (BST, AVL, RED-BLACK, B-tree). Neke od ovih struktura upoznat ćemo u ovom poglavlju. 20.2 Hash tablica Ideju hash2 tablice se može opisati na slijedeći način. Kada u telefonskom imeniku tražimo neko ime, najprije moramo pronaći stranicu na kojoj je zapisano to ime, a zatim pregledom po stranici dolazimo do traženog imena. Iskustvo pokazuje da više vremenu treba za traženje stranice, nego za traženje imena na stranici. Zbog toga se kod telefonskih imenika grupe stranica označavaju slovima abecede, kako bi se brže pronašla željena stranica. Na sličan način podaci u hash tablici se grupiraju u više skupova (ili buketa). U kojem skupu se nalaze podaci, određuje se iz ključa simbola pomoću posebne funkcije koja se naziva hash-funkcija. 2 eng. hash – znači podjelu na manje dijelove, koji su otprilike jednake veličine 318
  • 319.
    Definicija: Funkcija hash(k,m)raspodjeljuje argument k u jedan od m skupova, sa statistički uniformnom razdiobom. Funkcija vraća vrijednost iz intervala i ∈ [0..m-1]. Ta vrijednost označava da string pripada i-tom skupu. Postoji više izvedbi hash-funkcije koje zadovoljavaju navedenu definiciju. Za rad sa stringovima ovdje će biti korišten sljedeći oblik hash-funkcije: unsigned hash(char *s, unsigned m) { /* Argument: s – string * m - maksimalni broj grupa * Vraća: hash vrijednost iz intervala [0..m-1] */ unsigned x=0; while(*s != '0') { s = *s + 31 * x; k++; } return x % m; } Može se primijetiti sličnost ove funkcije s funkcijom rand(), koja je opisana u poglavlju 11. Tada je slučajni broj xi , iz intervala [0..n-1], bio generiran iz prethodne vrijednost xi-1, prema izrazu: xi = (a * xi-1 + b) % m gdje su a i b konstante, koje su određene uvjetom da se broj generira sa statistički uniformnom razdiobom. U slučaju prikazane hash-funkcije ova se zakonitost primjenjuje kumulativno za sve elemente stringa (a=31, b=*s). Kada je ključ cjelobrojnog tipa tada se može koristiti funkcija unsigned hash(int k, unsigned M) { return k > 0? k % M : - k % M; } 20.2.1 Otvorena hash tablica Sada će biti opisana izvedba tablice simbola, koja se naziva otvorena hash-tablica (eng. Open Hashing ili Separate Chaining). Ilustrirana je na slici 20.1. Otvorena hash tablica se realizira kao niz koji sadrži pokazivače na liste simbola koje imaju istu hash vrijednost. Te liste se nazivaju buketi (eng. bucket). 319
  • 320.
    Slika 20.1 Hashtablica s vezanom listom Operacije se provode prema sljedećem pravilu: insert(k,v) Umetni simbol<k, v> na glavu liste bucket[hash(k, m)]. find (k) Traži simbol ključa k u listi bucket[hash(k, m)]. Vraća simbol ili NULL delete(k) Odstrani simbol<k, v> iz liste bucket[hash(k, m)]. Element niza buckets[i] je pokazivač i-te liste. Početno su svi pokazivači jednaki NULL. Kada u tablici treba tražiti ili unijeti simbol kojem je ključno ime key, to se vrši u listi kojoj je pokazivač jednak buckets[hash(key,m)]. Slijedi opis implementacije tablice, prema specifikaciji ADT TABLE iz datoteke "table.h". Specifikacija za generičku hash tablicu /*Datoteka: table.h */ /*Apstraktni tip TABLE */ typedef struct _table *TABLE; typedef struct _symbol *SYMBOL; /* Primjer za string */ typedef int (*CompareFuncT)(void *, void *); /* strcmp() */ typedef void *(*CopyFuncT)(void *); /* strdup() */ typedef void (*FreeFuncT)(void *); /* free() */ typedef unsigned (*HashFuncT)(void *, unsigned); /* hash() */ TABLE table_new(unsigned m, CompareFuncT f, HashFuncT h); /* Konstruktor tablice * Argument: m - veličina tablice * f - pokazivač funkcije za poredbu ključa (kao strcmp) * h - pokazivač hash funkcije (kao hash_str) * Ako se umjeto pokazivača f i h upiše 0 * podrazumjeva se rad s ključem tipa int * Vraća: pokazivač tablice ili NULL ako se ne može oformiti tablica */ void table_set_alloc(TABLE T, CopyFuncT copykey, FreeFuncT freekey, CopyFuncT copyval, FreeFuncT freeval); /* Postavlja pokazivače funkcija za alociranje ključa i vrijednosti * Argumenti: copykey - pokazivač funkcije za alociranje ključa * freekey - pokazivač funkcije za dealociranje ključa * copyval - pokazivač funkcije za alocira vrijednosti 320
  • 321.
    * freeval - pokazivač funkcije za dealocirane vrijednosti */ void table_free(TABLE T); /* Briše tablicu - oslobađa memoriju*/ int table_insert(TABLE T, void *k, void *v); /* Umeće simbol <k,v>, u tablicu simbola, ako već nije u tablici. * Argumenti: T - tablica simbola (rječnik) * k - ključ simbola * v - pokazivač vrijednosti simbola * Vraća: 1 ako je umetanje uspješno, inaće vraća 0 */ SYMBOL table_find(TABLE T, void *k); /* Traži simbol poznatog ključa k u tablici simbola T * Argumenti: T – tablica simbola (rječnik) * k – ključ simbola * Vraća: pokazivač simbola, * ili NULL ako simbol nije pronađen */ void *table_symbol_value(SYMBOL S); /* Vraća pokazivač vrijednosti simbola S * ili cijeli broj, ako je vrijednost cijeli broj */ void *table_symbol_key(SYMBOL s); /* Vraća pokazivač ključa simbola S * ili cijeli broj, ako je ključ cijeli broj */ int table_delete(TABLE T, void *k); /* Traži simbol poznatog ključa k i briše ga iz tablice * Argumenti: T – tablica simbola (rjecnik) * k – ključ simbola * Vraća: 1 ako je simbol izbrisan ili 0 ako simbol nije u tablici */ unsigned table_size(TABLE T); /* Vraća broj elemenata u tablici */ Pokazivači ključa i vrijednosti simbola su tipa void *. Na taj način specifikacija se može koristiti za implementaciju generičkih tablica, za bilo koji tip ključa i vrijednosti simbola. Za potpunu generičku primjenu ADT-a, nakon inicijalizacije treba registrirati funkcije koje se koriste za dinamičko alociranje i dealociranje memorije. To se vrši funkcijom table_set_aloc(). Njeni argumenti su pokazivači na funkcije za alociranje i dealociranje ključa i vrijednosti simbola. Ovo pokazivači na funkcije se koriste i definiraju na isti način koji je opisan kod ADT DEQUEUE u poglavlju 18. Zbog zahtjeva za generičkim operacijama potrebno je prilagoditi i deklaracije hash funkcija. U datoteci "hash.c" definirane su hash funkcije za rad sa stringovima (hash_str) i cijelim brojevima (hash_int): /* Datoteka: hash.c */ unsigned hash_str(void *key, unsigned M) 321
  • 322.
    {/* Vraća hashvrijednost iz intervala [0..M-1] za string key */ unsigned hashval=0; char *s = key; while(*s != '0') { hashval = *s + 31 * hashval; s++; } return hashval % M; } unsigned hash_int(void *p, unsigned M) {/* Vraća hash vrijednost iz intervala [0..M-1] za cjeli broj p */ /* Prvi argument će biti int, iako je deklariran void *p */ int k = (int)p; return k > 0? k % M : - k % M; } Prema specifikacji ADT TABLE, tablica T, čiji simboli imaju ključ i vrijednost tipa string, inicijalizira se naredbama: TABLE T = new_table(1023, strcmp, hash_str); table_set_alloc(T, strdup, free, strdup, free); Veličina tablice se odabire proizvoljno. Poželjno je da m bude prosti broj. Ako je ključ cjelobrojnog tipa inicijalizacija se vrši samo jednom naredbom. Primjerice, tablicu veličine m=1023, kojoj su ključ i vrijednost tipa int inicijalizira se naredbom: TABLE T = new_table(1023, 0, 0); To znači da u implementaciji ADT-a treba predvidjeti automatsko postavljanje funkcije usporedbe ključeva i hash funkcije za rad s cijelim brojevima, kada su drugi i treći argument jednaki nuli. Pored apstraktnog objekta TABLE, specificiran je apstraktni objekt SYMBOL. Ideja je sljedeća: želimo da su simboli tablice dostupni korisniku. Simboli imaju dva atributa: ključ i vrijednost. Ako želimo pronaći vrijednost (ili ključ) simbola iz tablice, koristimo najprije funkciju table_find(), koja vraća tip SYMBOL, a zatim pomoću funkcije table_symbol_value() ili table_symbol_key(), iz poznatog simbola, dobijemo pokazivač vrijednosti ili ključa simbola. Primjerice, nakon naredbe char *v = table_symbol_value(table_find(T, "xxx")); varijabla v pokazuje string koji je vrijednost simbola koji ima ključ "xxx". Ako ključ "xxx" nije u tablici varijabla v sadrži NULL pokazivač. Ako simbol sadrži ključ i vrijednost cjelobrojnog tipa, tada se umjesto pokazivača radi s cijelim brojevima,primjerice int v = (int) table_symbol_value(table_find(T, 123)); Implementacije ovih funkcija i objekata tipa TABLE i SIMBOL mogu biti različite. Sada će biti pokazana implementacije otvorene hash tablice, a zatim će biti pokazan drugi tip implementacije koji se naziva zatvorena hash tablica. Implementacija otvorene hash tablice 322
  • 323.
    /* Datoteka htable1.c */ /* Otvorena hash tablica */ #include <stdlib.h> #include <string.h> #include <assert.h> #include "table.h" typedef struct _symbol { /* čvor liste simbola */ struct _symbol *next; /* pokazivač vezane liste */ void *key; /* pokazivač ključa simbola */ void *val; /* pokazivač vrijednosti simbola */ }Symbol; typedef struct _table { unsigned M; /* veličina tablice */ unsigned N; /* broj simbola u tablici */ Symbol **bucket; /* niz pokazivača na listu simbola */ CompareFuncT compare; /* funkcije usporedbe kljuca */ HashFuncT hash; /* hash funkcija */ CopyFuncT copy_key; /* funkcija alociranja ključa */ CopyFuncT copy_val; /* funkcija alociranja vrijednosti */ FreeFuncT free_key; /* funkcija dealociranja ključa */ FreeFuncT free_val; /* funkcija dealociranja vrijednosti*/ } Table, *TABLE; static int compareInternal(void *a, void *b) { /* usporedba dva integera a i b*/ if( (int)a > (int)b ) return 1; else if( (int)a < (int)b ) return -1; else return 0; } static unsigned hash_int(void *p, unsigned M) {/* Vraća hash vrijednost iz intervala [0..M-1] za cjeli broj p */ /* prvi argument će biti int iako je deklariran void *p */ int k = (int)p; return k > 0? k % M : - k % M; } TABLE table_new(unsigned M, CompareFuncT compare, HashFuncT hash) { TABLE T = (TABLE) malloc(sizeof(Table)); if (T == NULL) return NULL; T->M = M; T->N = 0; T->bucket = (Symbol **) calloc(M, sizeof(Symbol *) ); T->hash = hash ? hash : hashInternal; T->compare = compare? compare : compareInternal; T->free_key = T->free_val = NULL; T->copy_key = T->copy_val = NULL; return T; } void table_set_alloc(TABLE T, CopyFuncT copykey, FreeFuncT freekey, CopyFuncT copyval, FreeFuncT freeval) { T->copy_key = copykey; T->copy_val = copyval; 323
  • 324.
    T->free_key = freekey; T->free_val = freeval; } static void free_node(TABLE T, Symbol *n) { assert(T); if(n == NULL) return; /* oslobodi sadržaj čvora: key, val i sam čvor*/ if(T->free_key) T->free_key(n->key); if(T->free_val) T->free_val(n->val); free(n); } static Symbol *new_node(TABLE T, void *key, void *val) { Symbol *np; assert(T); np = (Symbol *)malloc(sizeof(Symbol)); if (np == NULL ) return NULL; np->key =(T->copy_key)? T->copy_key(key) : key; np->val =(T->copy_val)? T->copy_val(val) : val; return np; } void table_free(TABLE T) { unsigned i; assert(T); for(i=0; i<T->M; i++) { Symbol *p, *t; p = T->bucket[i]; /* glava liste */ while (p != NULL) { /* briše listu iz memorije */ t = p; p = p->next; free_node(T, t); } } free(T->bucket); free(T); } unsigned table_size(TABLE T) { assert(T); return T->N; } SYMBOL table_find(TABLE T, void *key) { Symbol *p; assert(T); p = T->bucket[T->hash(key, T->M)]; while( p != NULL) { if (T->compare(key, p->key) == 0) return (void *) p; /* pronađen simbol */ p = p->next; } return NULL; /* nije pronađen */ } void *table_symbol_value(SYMBOL S) 324
  • 325.
    { if(S) return S->val; else return NULL; } void *table_symbol_key(SYMBOL S) { if(S) return S->key; else return NULL; } int table_insert(TABLE T, void *key, void *val) { SYMBOL s; unsigned h; assert(T); s = table_find(T, key); if (s == NULL) { /* ako ne postoji ključ */ Symbol *np = new_node(T, key, val); if (np == NULL) return 0; h = T->hash(key, T->M); np->next = T->bucket[h]; T->bucket[h] = np; T->N++; return 1: } return 0; } int table_delete(TABLE T, void *key) { Symbol *p , *prev; unsigned h = T->hash(key, T->M); assert(T); prev = 0; p = T->bucket[h]; while (p && T->compare(p->key, key)) { prev = p; p = p->next; } if (!p) return 0; /* p - čvor koji brišemo, prev - prethodni čvor*/ if (prev) prev->next = p->next; else T->bucket[h] = p->next; /* glava buketa */ --T->N; free_node (T, p); return 1; } Testiranje se provodi programom hash_test.c. U njemu se formira tablica simbola, i u nju se unosi nekoliko simbola. Zatim se provjerava da li se simbol "while" nalaze u tablici. Zatim se taj simbol briše i ponovo provjerava da li je u tablici. /* Datoteka: hash_test.c*/ #include <stdio.h> #include <stdlib.h> #include <string.h> #include "table.h" #include "hash.c" int main( ) { char str[20]; SYMBOL s; 325
  • 326.
    TABLE T =table_new(127, strcmp, hash_str); table_set_alloc(T, strdup, free, strdup, free); table_insert(T, "if", "naredba selekcije"); table_insert(T, "while", "petlja"); table_insert(T, "for", "petlja"); table_insert(T, "printf", "C-funkcija"); strcpy(str, "while"); s=table_find(T, str); if(s) printf("n%s -> %s", str, table_symbol_value(s)); else printf("n%s -> nije u tablici", str); /*izbriši simbol i provjeri da li je u tablici*/ table_delete(T, str); s = table_find(T, str); if(s) printf("n%s -> %s", str, table_symbol_value(s)); else printf("n%s -> nije u tablicin", str); table_free(T); return 0; } Nakon izvršenja programa dobije se ispis: while -> petlja while -> nije u tablici Hash-tablica s vezanom listom omogućuje realiziranje tablice simbola u kojoj se vrlo brzo izvršavaju operacije traženja i umetanja u tablicu. Kada se radi s vrlo velikim skupom podataka, može se uzeti da u prosječnom slučaju obje operacije imaju složenost O(1). Samo u najgorem slučaju, kada svi ključevi imaju ustu vrijednost, složenost iznosi O(n). Do tog zaključka se dolazi sljedećom analizom složenosti. Uzmimo da je u tablici s m buketa pohranjeno n simbola. Definirajmo faktor popunjenosti α (eng. load faktor) kao omjer α = n/m. On pokazuje koliko prosječno ima simbola u jednom buketu. Ukupno vrijeme za pretraživanje tablice, uključujući vrijeme za proračun hash funkcije, iznosi O(1+α). Ako uzmemo da je broj buketa m barem proporcionalan broju simbola u tablici n, dobije se n = cm= O(m), i iz toga α = n/m=O(m)/m=O(1). Dakle, pretraživanje u prosjeku ima složenost O(1). 20.2.2 Zatvorena hash tablica Kada otprilike znamo koliko će elemenata sadržavati tablica, tada se ona može efikasno realizirati kao niz koji sadržava simbole, a ne pokazivače na bukete simbola. Ideja je da se simbol upiše u niz na indeksu koji je određen s hash vrijednošću. Pošto više simbola može imati istu hash vrijednost, ta pozicija može biti zauzeta s prethodno upisanim simbolom. U tom slučaju, koji se naziva kolizija, ispituje se da li se simbol može upisati na lokacijama koje sukscesivno, ili po nekom drugom zakonu, slijede iza te lokacije. Na isti način se vrši traženje u tablici. Niz, koji sadrži ovakvu tablicu, mora imati kapacitet m veći od broja simbola n, pa je faktor popunjenosti zatvorene tablice uvijek manji od jedinice (α = n/m <1). Općenito, indeks je funkcija ključa k i broja ispitivanja i. Koriste se tri načina određivanja indeksa: 1. Linerano ispitivanje : indeks(k, i) = (hash(k) +i ) % m 326
  • 327.
    2. Kvadratično ispitivanje: indeks(k, i) = (hash(k) +i2 ) % m 3. Dvostruko "heširanje" : indeks(k, i) = (hash1(k) +i⋅ hash2(k) ) % m U sva tri slučaja ispitivanje počinje s indeks(k, 0) = hash(k), a operacije ADT TABLE se mogu realizirati na sljedeći način: insert (k,v) - umeće simbol <k,v> u niz htable, prema algoritmu: Pronađi najmanji i=0,1,.m. za koji htable[indeks(k,i)] nije popunjen Na tu poziciju upiši simbol <k,v> find(k) - vraća vrijednost simbola koji ima ključ k, ili null, prema algoritmu: Algoritam: Za sve i za koje je redom htable[indeks(k,i)] popunjen, Ako je ključ simbola htable[indeks(k,i)] jednak k, Vrati vrijednost simbola Vrati null delete(k) - briše simbol koji ima ključ k, prema algoritmu: Pronađi simbol ključa k Ako je pronađen, označi da je taj simbol izbrisan Ilustrirajmo proces linearnog ispitivanja: U ovom primeru uzet ćemo niz veličine m=10, ključ je cijeli broj i hash funkcija je oblika hash(k) = k % 10. Početno su sve pozicije prazne, što je označeno simbolom ∅; 0 1 2 3 4 5 6 7 8 9 ∅ ∅ ∅ ∅ ∅ ∅ ∅ ∅ ∅ ∅ Ako redom umećemo ključeve 15, 17 i 8, ne postoji preklapanje i rezultat je: 0 1 2 3 4 5 6 7 8 9 ∅ ∅ ∅ ∅ ∅ 15 ∅ 17 8 ∅ Umetnimo sada ključ 35. Računamo indeks(k,0) = 35 %10 = 5. Pozicija 5 je zauzeta (s ključem 15), pa moramo ispitati sljedeću poziciju. Indeks(k,1) = (5+1) %10 = 6. Pozicija 6 je slobodna pa u nju umećemo ključ 35. 0 1 2 3 4 5 6 7 8 9 ∅ ∅ ∅ ∅ ∅ 15 35 17 8 ∅ Umetnimo sada ključ 25. Najprije probamo poziciju (25 % 10). Ona je zauzeta. Zatim linearno ispitujemo sljedeću poziciju 6 ((5+1) % 10). Ona je također zauzeta. Nastavljamo ispitivati poziciju 7 ((5+2) % 10); ona je zauzeta kao i sljedeća pozicija 8 ((5+3)% 10). Slobodna je tek pozicija 9 ((5+4) % 10) i u nju umećemo ključ 25. 0 1 2 3 4 5 6 7 8 9 ∅ ∅ ∅ ∅ ∅ 15 35 17 8 25 Umetnimo sada ključ 75. Ponovo se ispituje počevši od pozicije 5 (75 % 10). Ispitivanje pozicija 6, 7, 8 i 9, pokazuje da su one zauzete. Ključ 75 umećemo tek na poziciji 0 ((5+5) % 10), koja je slobodna. 0 1 2 3 4 5 6 7 8 9 75 ∅ ∅ ∅ ∅ 15 35 17 8 25 327
  • 328.
    Analiza efikasnosti pristupazatvorenoj hash tablici Prethodni primjer pokazuje problem kod linearnog ispitivanja: kada više ključeva ima istu hash vrijednost tada oni kumulativno povećavaju grupiranje oko primarnih pozicija (eng. clustering). Problem se može umanjiti upotrebom kvadratičnog ispitivanja ili još bolje dvostukim "heširanjem", ali i tada postoje problemi: o Problem je kvadratičnog ispitivanja da se njime sigurno pronalazi slobodna pozicija samo ako je tablica ispunjena manje od polovine i ako je kapacitet tablice m prosti broj. o Problem dvostrukog heširanja dolazi do izražaja kada je ključ tipa string. Tada dvostruko računanje hash vrijednosti često zazima više vremena nego višestruko ispitivanje slobodnog mjesta u tablicu. Kada je ključ cjelobrojnog tipa, dvostruko haširanje daje izvrsne rezultate, jer se može provesti jednostavnim funkcijama hash1(k) = k % m i hash2(k) = 1+ k %(m-1) uz uvjet da je m prosti broj. Uz pretpostavku da hash funkcija uniformno raspodjeljuje ključeve u tablici, može se izvesti da se pri operaciji insert, u tablici s faktorom popunjenosti α, u prosječnom slučaju vrši 1/(1-α) ispitivanja. To slijedi iz činjenice da faktor popunjenosti α = n/m predstavlja vjerojatnost pronalaženja popunjenog mjesta u tablici. Zbog toga, vjerojatnost nalaženja slobodnog mjesta iznosi (1-α), pa zaključujemo da očekivani broj ispitivanja pri traženju slobodnog mjesta iznosi 1/(1-α). Analogno, kod bacanja kocke vjerojatnost pojave broja tri iznosi 1/6, pa se može očekivati da se taj broj pojavi prosječno u 6 bacanja. Prethodno razmatranje pokazuje da pri popunjenosti tablice 50% prosječno treba izvršiti 2 ispitivanja, a pri popunjenosti 90% treba izvršiti 10 ispitivanja. Prije opisani problem grupiranja kod linearnog ispitivanja znatno pogoršava ovu idealiziranu procjenu. Knuth je statitičkom analizom izveo da tada prosječno stvarni broj ispitivanja iznosi (1 + 1/(1 - α)2)/2. To znači da pri popunjenosti tablice 50% prosječno treba izvršiti 2,5 ispitivanja, a pri popunjenosti 90% treba izvršiti 50,5 ispitivanja. Implementacija hash tablice metodom kvadratičnog ispitivanja Specifikacija za implementaciju tablice je u prije opisanoj datoteci "table.h", a koriste se prethodno definirani algoritmi. Tablica je definirana kao dinamički niz koji sadrži strukture tipa Symbol. Ta struktura pored vrijednosti ključa (key) i vrijednost (val), sadrži podatak o statusu simbola. Ako je status = 0 (EMPTY) simbol tek treba bit definiran. Ako je status = 1 (FULL) simbol je definiran, a ako je status = -1 (DELETED) simbol je izbrisan. Kod kvadratično ispitivanja indeks se računa prema izrazu indeks(k, i) = (hash(k) +i2 ) % m Kvadriranje je spora operacije, pa se za efikasni proračun indeksa koristi iterativni izraz: indeks(k, i) = (indeks(k, i-1) + 2*i - 1 ) % m Lako je pokazati da su dva izraza ekvivalentna; dovoljno je uvrstiti prvi izraz u desnu stranu dugog izraza (za indeks i-1). Kod kvadratičnog ispitivanja se traži da veličina tablice bude duplo veća od broja elemenata, te da veličina tablice bude prosti broj. Zbog toga se pri inicijalizaciji tablice poziva funkcija get_next_prime(M); koja daje prvi prosti broj koji je veći od M. /* Datoteka htable2.c */ /* Zatvorena hash tablica s kvadratičnim ispitivanjem*/ 328
  • 329.
    #include <stdlib.h> #include <string.h> #include <assert.h> #include "table.h" typedef struct _symbol { void *key; /* pokazivač ključa simbola */ void *val; /* pokazivač vrijednosti simbola */ short status; /* status: prazno(0), */ }Symbol; /* popunjeno(1) ili izbrisano (-1) */ typedef struct _table { unsigned M; /* veličina tablice */ unsigned N; /* broj elemenata u tablici */ Symbol *htable; /* dinamièki alocirani niz simbola */ CompareFuncT compare; /* funkcije usporedbe kljuèa */ HashFuncT hash; /* hash funkcija */ CopyFuncT copy_key; /* funkcija alociranja kljuca */ CopyFuncT copy_val; /* funkcija alociranja vrrijednosti */ FreeFuncT free_key; /* funkcija dealociranja kljuca */ FreeFuncT free_val; /* funkcija dealociranja vrijednosti */ } Table, *TABLE; #define KEY(i) (T->htable[(i)].key) #define VAL(i) (T->htable[(i)].val) #define EMPTY(i) (T->htable[(i)].status == 0) #define SET_EMPTY(i) (T->htable[(i)].status = 0) #define FULL(i) (T->htable[(i)].status == 1) #define SET_FULL(i) (T->htable[(i)].status = 1) #define DELETED(i) (T->htable[(i)].status == -1) #define SET_DELETED(i) (T->htable[(i)].status = -1) #define NOT_FOUND -1 static int compareInternal(void *a, void *b) { if( (int)a > (int)b ) return 1; else if( (int)a < (int)b ) return -1; else return 0; } static unsigned hashInternal(void *p, int m) {/* daje hash za integer*/ int k = (int)p; return (k > 0)? (k % m) : (-k % m); } static unsigned prime_list[] = { /*niz prostih brojeva*/ 7u, 11u, 13u, 17u, 19u, 23u, 29u, 31u, 53u, 97u, 193u, 389u, 769u, 1543u, 3079u, 6151u, 12289u, 24593u, 49157u, 98317u, 196613u, 393241u, 786433u, 1572869u, 3145739u, 6291469u, 12582917u, 25165843u, 50331653u, 100663319u, 201326611u, 402653189u, 805306457u, 1610612741u, 3221225473u, 4294967291u }; static unsigned get_next_prime(unsigned M) 329
  • 330.
    { /*vraca sljedeciprosti broj*/ int i, len = sizeof(prime_list)/sizeof(unsigned); for(i=0; i<len; i++) { if(prime_list[i] >= M) return prime_list[i]; } return prime_list[len-1]; } TABLE table_new(unsigned M, CompareFuncT compare, HashFuncT hash) { TABLE T = (TABLE) malloc(sizeof(Table)); if (!T) return NULL; T->M = get_next_prime(M); T->N = 0; T->htable = (Symbol *)calloc(T->M, sizeof(Symbol) ); T->hash = hash ? hash : hashInternal; T->compare = compare? compare : compareInternal; T->free_key = T->free_val = NULL; T->copy_key = T->copy_val = NULL; return T; } void table_set_alloc(TABLE T, CopyFuncT copykey, FreeFuncT freekey, CopyFuncT copyval, FreeFuncT freeval) { T->copy_key = copykey; T->copy_val = copyval; T->free_key = freekey; T->free_val = freeval; } void table_free(TABLE T) { unsigned i; short status; assert(T); for(i = 0; i < T->M; i++) { if(FULL(i)) { if(T->free_key) (*T->free_key)(KEY(i)); if(T->free_val) (*T->free_val)(VAL(i)); } } free(T->htable); free(T); } int find_sym_idx(TABLE T, void *key) { unsigned i = 0; unsigned idx = T->hash(key, T->M); while (1) { if(EMPTY(idx)) return NOT_FOUND; if(FULL(idx) && T->compare(KEY(idx), key) == 0) return idx; /*pronaðen simbol*/ i++; idx = (idx + (2*i-1)) % T->M; } 330
  • 331.
    return NOT_FOUND; } SYMBOL table_find(TABLET, void *key) { int i; assert(T); i = find_sym_idx (T, key); if(i != NOT_FOUND) return &T->htable[i]; else return NULL; /* nije pronaðen */ } void *table_symbol_value(SYMBOL s) { if(s) return s->val; else return NULL; } void *table_symbol_key(SYMBOL s) { if(s) return s->key; else return NULL; } int table_insert(TABLE T, void *key, char *val) { unsigned idx, i = 0; assert(T); if(T->N >= T->M/2) return 0; /*ovdje primijeni rehash funkciju*/ idx = T->hash(key, T->M); for(;;) /* probaj sljedeći index*/ { if(!FULL(idx)) { KEY(idx) = (T->copy_key)? T->copy_key(key) : key; VAL(idx) = (T->copy_val)? T->copy_val(val) : val; SET_FULL(idx); T->N++; return 1; } else if(T->compare(KEY(idx), key) == 0) { return 0; /* već postoji simbol*/ } i++; idx = (idx + (2*i-1)) % T->M; } return 0; } int table_delete(TABLE T, void *key) { int i; assert(T); i = find_sym_idx (T, key) ; if(i != NOT_FOUND && !(DELETED(i))) { if(T->free_key) T->free_key(KEY(i)); if(T->free_val) T->free_val(VAL(i)); SET_DELETED(i); --T->N; 331
  • 332.
    return 1; } return 0; /* nije pronađen */ } unsigned table_size(TABLE T) { assert(T); return T->N; } Testiranje ovog ADT provodimo s istim programom kojim je testirana prethodna implementacija otvorene hash tablice. Zadatak: Usporedite brzinu izvršena otvorene i zatvorene hash tablice u programu table_timer_test.c. U tom programu su pomoću funkcije clock() mjeri vrijeme izvršenja petlje u kojoj se umeću i brišu cjelobrojni slučajno odabrani ključevi simbola. Petlja se ponavlja 1000000 puta. /*Datoteka: table_timer_test.c*/ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <assert.h> #include <time.h> #include "table.h" int main() { float sec; long start = 0L, end = 0L; int maxnum, maxrand, status, i; int val= 9; int key; TABLE T; /* procesiraj 100000 slučajnih brojeva */ maxnum = 100000; maxrand = 1501; printf("maxnum = %dn", maxnum); T = table_new(maxrand*2, 0, 0); start = clock (); for (i = maxnum; i>0; i-- ){ key = rand() % maxrand; if ( table_find(T,(void *)key) ) { status = table_delete(T,(void *)key); if (!status) printf("fail: status = %dn", status); } else { status = table_insert(T, (void *)key,(void *) val); if (!status) printf("fail: status = %dn", status); } } end = clock (); sec=(float)(end-start)/(float)CLOCKS_PER_SEC; printf("n %d kljuceva procesirano u %f ms",maxnum, sec*1000); 332
  • 333.
    table_free(T); return 0; } Napomena: Otvorenu hash tablicu inicijalizirajte na veličinu maxrand/2, a zatvorenu tablicu na veličinu maxrand*2. Mijenjajte veličinu maxrand od 127 do 10000. Primjetit ćete da se operacije s otvorenom hash tablicom, brže izvršavaju od operacija s zatvorenom tablicom. Zadatak: U prethodnoj implementaciji zatvorene hash tablice metodom kvadratičnog ispitivanja izvršte sljedeću izmjenu: Ukoliko broj simbola u tablice n premaši vrijednost m/2, treba udvostručiti vrijednost tablice. Na taj način tablica može primiti proizvoljan broj simbola. Za tu svrhu koristite funkciju table_rehash(T): TABLE table_rehash(TABLE T ) /* Vraća pokazivač tablice koja ima dvostuko veći kapacitet * od ulazne tablice T, i koja sadrži simbole it T */ { unsigned int i; TABLE Tnew; /* Formiraj uvećanu tablicu i umetni vrijednosti iz T*/ Tnew = table_new( get_next_prime(2*T->M)); for( i=0; i<T->M; i++ ) if(T->htable[i].key != NULL ) table_insert( Tnew, T->htable[i].key, T->htable[i].val ); free(T->htable ); free(T); return Tnew; } Analizom programerske prakse vidljivo je da se znatno više koristi otvorene tablice. One daju znatno bolju efikasnost kod visoke popunjenosti tablice. 20.3 BST - binarno stablo traženja BST je kratica za naziv binarno stablo traženja (eng. binary search tree). Temeljna karakteristika binarnog stabla traženja je da uvijek sadrži sortirani raspored čvorova. Jedan od elemenata BST čvora predstavlja jedinstveni ključ. Usporedbom vrijednosti ključa određuje se red kojim se umeću ili pretražuju čvorovi stabla, prema sljedećoj rekurzivnoj definiciji. Definicija: Binarno stablo traženja (BST) je: 1. Prazno stablo, ili 2. Stablo koje se sastoji od čvora koji se naziva korijen i dva čvora koji se nazivaju lijevo i desno dijete, a oni su također binarna stabla (podstabla). Svaki čvor sadrži ključ čija vrijednost je veća od vrijednosti ključa čvora lijevog podstabla, a manja ili jednaka vrijednosti ključa čvora desnog podstabla. 333
  • 334.
    a) b) Slika 20.2 Sortirano binarno stablo, koje nastaje kad je redosljed unošenja elemenata: a) 6, 9, 3, 8, 5, 2. b) 3, 2, 5, 6, 8, 9. Najljeviji čvor sadrži najmanji ključ (2), a najdesniji čvor sadrži najveći ključ (9). Strelice pokazuju INORDER obilazak stabla. Slika 20.2a) prikazuje binarno stablo u kojem su elementi uneseni redom: 6, 9, 3, 8, 5, 2, prema prethodnoj definiciji BST. Strelice pokazuju da se INORDER obilaskom stabla dobije sortirani raspored elemenata(2, 3, 5, 6, 8, 9). Slika 20.2b) prikazuje binarno stablo u kojem su elementi uneseni redom: 3,2,5,6,8,9. Iako stabla sa slika 20.2a) i 20.3b) sadrže iste elemente, oblik stabala je različite jer oblik ovisi o redoslijedu elemenata. Najveća visina stabla nastaje kada se unose elementi u sortiranom rasporedu, a najmanja kada se elementi unose po slučajnom uzorku. U najboljem slučaju dobije se potpuno stablo. Tada je potreban minimalan broj operacija za prijeći stazu od korijena do lista, bilo za traženje ili unošenje elementa u stablo. Nešto kompleksnija statistička analiza pokazuje da se u najboljem i u prosječnom slučaju vrši O(log2 n) operacija. U najgorem slučaju, kod sortiranog unosa, vrši se O(n) operacija, jer stablo degenerira u "kosu" listu. Može se zaključiti da BST predstavlja manje efikasno rješenje za izradu tablica ili rječnika od hash tablice koji nudi efikasnost O(1). Ipak, BST nudi neke mogućnosti koje se ne može ostvariti hash tablicom. To se u prvom redu odnosi na formiranje dinamičkih sortiranih skupova u kojima se lako određuje raspored elemenata. Za tu svrhu definira se skup operacija: ADT BSTREE insert (T, k, v) - umeće simbol, kojem je ključ k i vrijednost v u T, ako već nije u T. find (T, k) - vraća simbol ako u T postoji simbol ključa k, ili NULL delete(T, k) - briše simbol kojem je ključ k, ako postoji u T. minimum(T) - vraća simbol iz T koji ima minimalnu vrijednost ključa. maximum(T) - vraća simbol iz T koji ima maksimalnu vrijednost ključa. predecessor(T, x) - vraća simbol iz T koji prethodi simbolu x succcessor(T, x) - vraća simbol iz T koji prethodi simbolu x Ove operacije definiraju ADT BSTREE, kojem je specifikacija operacija u C jeziku zapisana u datoteci "bst.h". #ifndef _RBT_BST_H_ #define _RBT_BST_H_ /* odstrani komentar ako zelis raditi s RED-BLACK stablom*/ /* #define RED_BLACK */ typedef struct _symbol *SYMBOL; typedef struct bs_tree *BSTREE; 334
  • 335.
    typedef int (*CompareFuncT)(void*, void *); typedef void *(*CopyFuncT)(void *); typedef void (*FreeFuncT)(void *); BSTREE bst_new(CompareFuncT compare); /* Stvara ADT BSTREE. * Argument: compare - pokazivač funkcije za usporedbu dva ključa * Ako je NULL, uzima se da stablo sadrži cijele brojeve * Vraća: pokazivač ADT-a */ void bst_set_alloc(BSTREE T, CopyFuncT copykey, FreeFuncT freekey, CopyFuncT copyval, FreeFuncT freeval); /* Postavlja funkcije za alociranje (kopiranje) i dealociranje simbola * Argumenti: T - ADT stablo (mora biti formiran) * copykey - pokazivač funkcije za kopiranje ključa * copyval - pokazivač funkcija za kopiranje vrijednosti * freekey - pokazivač funkcija za dealociranje ključa * freeval - pokazivač funkcija za dealociranje vrijednosti */ void bst_free(BSTREE T); /* Dealocira cijelo stablo T*/ int bst_size(BSTREE T); /* Vraća broj simbola u stablu T*/ int bst_insert(BSTREE T, void * key, void *val); /* Umeće simbol <key,val> u stablo T, ali samo ako ključ key nije u stablu * Argumenti: T - ADT stablo (mora bit formiran) * key - pokazivač ključa (ili cijeli broj) * val - pokazivač vrijednosti (ili cijeli broj) * Vraća: 1 ako je izvršeno umetanje, inače vraća 0 */ int bst_delete(BSTREE T, void * key); /* Briše simbol koji ima ključ key, iz stabla T * Argumenti: T - ADT stablo (mora bit formiran) * key - pokazivač ključa (ili cijeli broj) * Vraća: 1 ako je izvršeno brisanje, inače vraća 0 */ SYMBOL bst_find(BSTREE T, void * key); /* Vraća pokazivač simbola, ako ključ postoji */ void *bst_symbol_value(SYMBOL S); /* Vraća pokazivač vrijednosti simbola S (ili cijeli broj)*/ void *bst_symbol_key(SYMBOL s); /* Vraća pokazivač ključa simbola S (ili cijeli broj)*/ SYMBOL bst_minimum(BSTREE T); /* Vraća pokazivač simbola koji ima minimalni ključa u stablu T*/ SYMBOL bst_maximum(BSTREE T); /* Vraća pokazivač simbola koji ima najveæi ključ u stablu T*/ 335
  • 336.
    SYMBOL bst_succesor(SYMBOL S); /*Vraća pokazivač simbola koji slijedi iza simbola S * PRE: S mora biti pokazivač čvora stabla */ SYMBOL bst_predecessor(SYMBOL S); /* Vraća pokazivač simbola koji prethodi simbolu S * PRE: S mora biti pokazivač čvora stabla */ #endif Primijetite da je na početku ove datoteke, unutar komentara definirano: #define RED_BLACK Ako se primijeni ova definicija tada će se navedene operacije provoditi po algoritmu za tzv. crveno-crna stabla (eng. red-black trees). Ideja i algoritmi za crveno-crna stabla bit će opisani u sljedećem odjeljku. Ukratko, kod crveno-crnog stabla svakom se čvoru pridjeljuje boja (eng. color). Taj se podatak koristi u algoritmima za uravnoteženje stabla, s ciljem da sve staze stabla, i u najgorem slučaju, budu dostupne sa složenošću O(log2 n). Prije nego se objasni postupak izgradnje crveno-crnog stabla neće se analizirati dio kôda koji je napisan unutar #ifdef RED_BLACK ........ /* kod za RED-BLACK stabla */ #endif Implementacija ADT BSTREE je u dana u datoteci "bst.c". Najprije je izvršeno definiranje struktura za implementaciju ADT BSTREE. Prva struktura, struct _simbol, opisuje čvor binarnog stabla. Također je s typedef definiran i sinonim za ovu strukturu imena Node. Definirana su tri pokazivača: na lijevo dijete, na desno dijete i na roditelja. Pokazivač na roditelja je potreban za implementaciju operacija successor() i predecessor(). Ako ove operacije nisu potrebne, tada se može izvršiti implementacija ADT-a i bez pokazivača na roditelja. Simboli su određeni ključem (key) i vrijednošću (val). U ovoj implementaciji key i val su definirani tipom void *, što znači da će se dinamičkim alociranjem memorije moći registirati simbole bilo kojeg tipa ključa i vrijednosti. Primijetimo da se u strukturi Node opciono bilježi "boja" čvora u članu Color, koji može imati samo dvije pobrojane vrijednosti: RED i BLACK. Za potpunu generičku implementaciju pri inicijalizaciji ADT-a bit će potrebno registrirati funkcije koje se koriste za dinamičko alociranje i dealociranje memorije. Pokazivači na ove funkcije zajedno s pokazivačem korijena stabla se bilježe u strukturi bs_tree. Tip pokazivača na ovu strukturu - BSTREE služi za označavanje ADT-a. /* Datoteka: bst.c * Implementacija bst ili red-black stabla (ako je definirano RED_BLACK) */ #include <stdlib.h> #include <string.h> #include <assert.h> #include "bst.h" typedef enum { BLACK, RED } nodeColor; typedef struct _symbol Node; 336
  • 337.
    struct _symbol { struct _symbol *left; /* lijevo dijete */ struct _symbol *right; /* desno dijete */ struct _symbol *parent; /* roditelj */ void * key; /* ključ simbola */ void * val; /* vrijednost simbola */ #ifdef RED_BLACK /* */ nodeColor color; /* boja (BLACK, RED) */ #endif }; struct bs_tree { int N; Node *root; CompareFuncT compare; /* funkcija za usporedbe ključa */ CopyFuncT copy_key; /* funkcija za alociranje ključa */ CopyFuncT copy_val; /* funkcija za alociranje vrijednosti */ FreeFuncT free_key; /* funkcija za dealociranje ključa */ FreeFuncT free_val; /* funkcija za dealociranje vrijednosti */ }; Slika 20. 3. Standardni i prošireni oblik binarnog stabla. Kod standardnog oblika listovi nemaju djece i pokazuju na NULL. Kod proširenog oblika stabla svi su podaci smješteni u unutarnjim čvorovima. Listovi ne sadrže podatke već su to virtualni čvorovi na koje pokazuje jedinstveni NIL pokazivač. Zbog lakše izvedbe algoritama koristit će se prošireni oblik binarnog stabla, u kojem se svi podaci nalaze u unutarnjim čvorovima, a vanjski čvorovi su tzv. NIL čvorovi (slika 20.2), tj. čvorovi na koje pokazuje NIL pokazivač. Roditelj korijena stabla također je NIL čvor. Vanjski čvorovi ne sadrže podatke, oni služe jedino kao graničnici u nekim algoritmima, pa se za vezu s vanjskim čvorovima koristi jedinstveni pokazivač - NIL - koji pokazuje na jedan globalni čvor kojeg se naziva sentinel. Sentinel pokazuje na samog sebe, jer mu se svi pokazivači (left, right i parent) inicijaliziraju na vrijednost NIL. To je ostvareno definicijama: #define NIL &sentinel /* svi listovi su NIL sentinel */ #ifdef RED_BLACK static Node sentinel = { NIL, NIL, NIL, 0, 0, BLACK}; #else static Node sentinel = { NIL, NIL, NIL, 0, 0}; #endif Zbog preglednijeg zapisa programa koristit će se sljedeće definicije: #define Left(x) (x)->left 337
  • 338.
    #define Right(x) (x)->right #define Parent(x) (x)->parent #define Color(x) (x)->color #define Key(x) (x)->key #define Val(x) (x)->val #define Root(x) (x)->root Prve operacije koje treba definirati su operacije kojima se inicijalizira ADT i kojima se briše objekt ADT iz memorije. Funkcija bst_new() prima za argument pokazivač na funkciju za usporedbu ključeva. Ako je ovaj argument jednak nuli postavlja se interna funkcija CompareInternal() kojo se vrši usporedba cijelih brojeva. Korijen stabla je početno prazan i njemu se pridjeljuje vrijednost NIL pokazivača. static int CompareInternal(void *a, void *b) { /*funkcija usporedbe cijelih brojeva*/ if( (int)a > (int)b ) return 1; else if( (int)a < (int)b ) return -1; else return 0; } BSTREE bst_new(CompareFuncT compare) { BSTREE T =(BSTREE)malloc(sizeof(struct bs_tree)); if(T){ Root(T) = NIL; T->N = 0; T->compare = compare? compare : compareInternal; T->free_key = T->free_val = NULL; T->copy_key = T->copy_val = NULL; } return T; } void bst_set_alloc(BSTREE T, CopyFuncT copykey, FreeFuncT freekey, CopyFuncT copyval, FreeFuncT freeval) { /*registriraj funkcije za kopiranje i dealociranje */ T->copy_key = copykey; T->copy_val = copyval; T->free_key = freekey; T->free_val = freeval; } Dvije interne funkcije newNode() i freeNode() služe za formiranje i dealociranje novog čvora, Funkcija deletaAll() , koristeći "postorder" obilazak stabla, briše sve čvorove. Nju koristi javna funkcija bst_free() kojom se dealocira cijelo stablo. static Node *newNode(BSTREE T, void * key, void *val, Node *parent) { Node *x = (Node *) malloc (sizeof(Node)); if (x == 0) return 0; Left(x) = NIL; Right(x) = NIL; Key(x) = (T->copy_key)? T->copy_key(key) : key; Val(x) = (T->copy_val)? T->copy_val(val) : val; Parent(x) = parent; #ifdef RED_BLACK 338
  • 339.
    Color(x) = RED; #endif return x; } static void freeNode(BSTREE T, Node *n) { if(n != NIL && n != NULL) { if(T->free_key) T->free_key(n->key); if(T->free_val) T->free_val(n->val); free(n); } } static void deletaAll(BSTREE T, Node *n ) {/* dealocira čvorove */ if( n != NIL && n != NULL) { deletaAll(T, Left(n)); deletaAll(T, Right(n)); freeNode(T, n); } } void bst_free(BSTREE T) {/* dealocira cijelo stablo*/ assert(T); deletaAll(T, Root(T)); free(T); } Najednostavnija je operacija size() koja vraća broj simbola u stablu: int bst_size(BSTREE T) { assert (T); return T->N; } Operacija traženja se jednostavno implementira funkcijom bst_find(). Polazi se od korijena stabla i ispituje da li čvor sadrži ključ koji je veći, manji ili jednak argumentu key. Ako je jednak, traženje je završeno i funkcija vraća pokazivač čvora. Ako je argument key veći od ključa u čvoru, traženje se nastavlja u desnom stablu, inače se traženje nastavlja u lijevom čvoru. Traženje se ponavlja sve do čvor ne pokazuje NIL. Ako se ne pronađe traženi ključ funkcija vraća NULL. SYMBOL bst_find(BSTREE T, void * key) {/* Vraca pokazivač simbola ako pronađe key, ili NULL */ Node *n; assert(T); n = Root(T); while(n != NIL) { int cmp = T->compare(key, Key(n)); if(cmp == 0) return n; else if(cmp > 0) n = Right(n); else /*(cmp < 0)*/ n = Left(n); } 339
  • 340.
    return NULL; } Funkcija bst_find() vraća vrijednost tipa SYMBOL. Iako u implementaciji SYMBOL predstavlja pokazivač čvora, tom se čvoru ne može pristupiti jer je implementacija skrivena od korisnika. Za dobivanje ključa i vrijednosti simbola definirane su funkcije bst_symbol_key() i bst_symbol_value(). void *bst_symbol_value(SYMBOL s) { if(s) return s->val; else return NULL; } void *bst_symbol_key(SYMBOL s) { if(s) return s->key; else return NULL; } Pronalaženje maksimalne vrijednosti ključa je trivijalno; počevši od korijena stabla ispituje se desni čvor, sve do lišća stabla, jer najdesniji čvor ima maksimalni ključ. Kod traženja najmanjeg elementa ispituje se lijevi čvor jer najljeviji čvor ima minimalni ključ. SYMBOL bst_maximum(BSTREE T) {/* vraća simbol s najvećim ključem u sortiranom stablu T */ Node *n; assert(T); n = T->root; while (Right(n) != NIL) n = Right(n); return (n == NIL)? NULL : n; } SYMBOL bst_minimum(BSTREE T) { /* vraća simbol s minimalnim ključem u sortiranom stablu T*/ Node *n; assert(T); n = T->root; while (Left(n) != NIL) n = Left(n); return (n == NIL)? NULL : n; } Slijedi opis funkcija bst_predecessor(x) i bst_successor(x). Sljednik čvora x (successor) je čvor s najmanjim ključem koji je veći od Key(x). Analogno tome je definiran prethodnik čvora x (predecessor) kao čvor s najvećim ključem koji je manji od Key(x). SYMBOL bst_succesor(SYMBOL x) { /* Vraća sljednika od čvora x * ili NULL ako sljednik ne postoji*/ /* PRE: x mora biti čvor stabla */ Node *y; if(x == NIL || x == NULL) return NULL; if((y = Right(x)) != NIL){ while (Left(y) != NIL) y = Left(y); return y; /* minimum desnog stabla od x*/ } y = Parent(x); while (y != NULL && x == Right(y)) { x = y; 340
  • 341.
    y = Parent(y); } return y; } Funkcija successor(x) vrši dvije analize. Prvo ako, čvor x ima desno dijete, znači da postoji veća vrijednost u desnoj stazi. Nju se dobije kao minimum(Right(x)). U drugom slučaju, ako je Right(x) == NULL, tada treba unatrag analizirati roditelja čvora i tražiti njegovo lijevo dijete (ukoliko je x desno dijete). Na analogan način se implementira funkcija predecessor(x). SYMBOL bst_predecessor(SYMBOL x) {/* Vraća prethodnika od čvora x ili NULL ako ne postoji*/ /* PRE: x mora biti čvor stabla */ Node * y; if(x == NIL || x == NULL) return NULL; if ((y = Left(x)) != NIL) { while (Right(y) != NIL) y = Right(y); return y; /* maksimum lijevog stabla od x*/ } else { y = Parent(x); while(y != NULL && x == Left(y)) { x = y; y = Parent(y); } return(y); } } Umetanje novog simbola (čvora) stabla se vrši prema sljedećem algoritmu: 1) Analiziraj stablo od korijena do lista. Ako je zadani ključ manji od ključa u čvoru, analiziraj čvor lijevo, inače analiziraj čvor desno. Ako je zadani ključ jednak ključu u čvoru, prekini ispitivanje, jer se simbol već postoji. Ako dođeš do lista, zapamti njegovog roditelja i prekini ispitivanje. 2) Alociraj novi čvor s zadanim ključem, vrijednošću i roditeljem. 3) Ako je roditelj jednak NULL novi čvor postaje korijen stabla inače postaje lijevo ili desno dijete, ovisno o tome da li je ključ bio manji ili veći od ključa u čvoru roditelja. Ovaj algoritam je implementiran u funkciji bst_insert(). int bst_insert(BSTREE T, void * key, void *val) {/*Vraća 1 ako je umetnut simbol <key, val> u stablo T, ili 0*/ Node *parent = 0; Node *x = Root(T); while (x != NIL) { int cmp = T->compare(key, Key(x)); if(cmp == 0) /*ako postoji ključ završi i vrati 0*/ return 0; parent = x; if(cmp > 0) x = Right(x); else /*(<0)*/ x = Left(x); } 341
  • 342.
    /* stvori novičvor */ x = newNode(T, key, val, parent); if (x == 0) return 0; /* umeti čvor stablo */ if(parent) { int cmp = T->compare(key, Key(parent)); if(cmp < 0) Left(parent) = x; else Right(parent) = x; } else { Root(T) = x; } #ifdef RED_BLACK insertFixup(T,x); #endif T->N++; return 1; } Operacija brisanja čvora je implementirana u funkciji bst_delete(). Treba uočiti tri slučaja: 1) čvor koji treba izbrisati je list 2) čvor koji treba izbrisati ima samo jedno dijete 3) čvor koji treba izbrisati ima dvoje djece Ilustrirani su na slici 20.4. Prva dva slučaja su jednostavna: čvor odstranjujemo na način da roditelju čvora pridijelimo pokazivač na dijete čvora (koje može biti NIL), a zatim dealociramo čvor. U trećem slučaju čvor ima dva djeteta. Njega možemo odstraniti, tako da u njega upišemo sadržaj čvora s prvim većim ključem (to je čvor s najmanjim ključem u desnom podstablu), a da zatim odstranimo taj prvi veći čvor. Pošto je prvi veći čvor najmanji čvor u desnom podstablu, on ne može imati lijevo dijete, pa se njega briše jednostavno, kao u prva dva slučaja. Slika 20.4 Tri slučaja brisanja čvora (podebljeno je prikazan čvor koji se briše) 342
  • 343.
    int bst_delete(BSTREE T,void * key) { Node *x, *y, *z; z = bst_find(T, key); if (!z) return 0; if (Left(z) == NIL || Right(z) == NIL) { /* y ima NIL djecu - mozeš ga premostiti */ y = z; } else { /* ako postoje oba djeteta - nađi prvog većeg */ /* to je minimalni s desna - on ima samo jedno dijete */ y = Right(z); while (Left(y) != NIL) y = Left(y); } /* y ćemo kasnije odstraniti */ /* y ima jedno dijete, spremi to dijete u x */ x = (Left(y) != NIL)? Left(y) : Right(y); /* odstrani y iz lanca roditelja */ /* tako da x postane lijevo ili desno dijete od Parent(y)*/ Parent(x) = Parent(y); if (Parent(y) == NIL) Root(T) = x; else if (y == Left(Parent(y))) Left(Parent(y)) = x; else Right(Parent(y)) = x; if (y != z) { /*zamijeni sadržaj y i z*/ void *tmp = Key(z); Key(z) = Key(y); Key(y) = tmp; tmp = Val(z); Val(z) = Val(y); Val(y) = tmp; } #ifdef RED_BLACK if (Color(y) == BLACK) deleteFixup (T,x); #endif freeNode(T, y); T->N--; return 1; } Testiranje ADT BSTREE se može provesti programom "bst_test.c". Demonstrira se umetanje, traženje i brisanje simbola s cjelobrojnim ključem, te ispis u sortiranom rasporedu od manjeg prema većem ključu i obrnuto. /*Datoteka: bst_test.c*/ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <assert.h> #include "bst.h" int main() { int maxnum, ct, status; int key, val= 9; SYMBOL s; BSTREE T = bst_new(0); 343
  • 344.
    /* procesiraj 100slučajnih brojeva */ maxnum = 100; printf("maxnum = %dn", maxnum); for (ct = maxnum; ct; ct-- ) { key = rand() % 19 + 1; if ( bst_find(T,(void *)key) ) { status = bst_delete(T,(void *)key); if (!status) printf("fail: status = %dn", status); } else { status = bst_insert(T, (void *)key,(void *) val); if (!status) printf("fail: status = %dn", status); } } printf("nU stablu ima %d simbola.n", bst_size(T)); s = bst_minimum(T); while(s) { printf("%d ", (int)bst_symbol_key(s) ); s = bst_succesor(s); } printf("nReverzni ispis:n"); s = bst_maximum(T); while(s){ printf("%d ", (int)bst_symbol_key(s) ); s = bst_predecessor(s); } bst_free(T); return 0; } Nakon izvršenja programa dobije se ispis: U stablu ima 8 simbola. 1 5 6 7 12 16 17 19 Reverzni ispis: 19 17 16 12 7 6 5 1 20.4 Crveno-crna stabla Crveno-crno stablo (eng. red-black tree) je modificirano binarno stablo traženja kojem svaki čvor ima jedan dodatni atribut: može biti crven ili crn. Crveno-crno stablo je određeno sljedećim svojstvima: Definicija: Crveno-crno stablo je binarno stablo sa svojstvima: 1. Svaki čvor je ili crn ili crven. 2. Korijen je crn 3. Svaki list (NIL) je crn. 4. Ako je čvor crven, njegova oba djeteta su crni (to znači da roditelj crvenog čvora ne smije biti crven). 5. Svaka prosta staza od nekog čvora do listova sadrži jednak broj crnih čvorova. 344
  • 345.
    Posljedica ove definicijeje a) da ni jedna staza od korijena do NIL čvorova nije duplo dulja od ostalih staza i b) visina crveno-crnog stabla s n čvorova iznosi najviše 2log2(n+1). Ovo svojstvo se lako dokazuje. Dokaz: Prvo ćemo dokazati da bilo koje podstablo čvora x sadrži barem 2bh(x)-1 unutarnjih čvorova. Funkcija bh(x) daje broj crnih čvorova u bilo kojoj stazi od čvora x (ne uključujući x) do listova. Ako je visina 0, x je list (NIL) - tada ima 2bh(x)-1 = 20-1= 0 unutarnjih čvorova. Razmotrimo sada unutarnji čvor s dva djeteta. Njegova visina je bh(x) ili bh(x)-1 ovisno o tome da li je dijete crveno ili crno. Pošto je to dijete od x ima manju visinu od x, možemo zaključiti da dijete ima barem 2bh(x)-1-1 unutarnjih čvorova, pa podstablo od x ima barem (2bh(x)-1-1)+ (2bh(x)-1-1)+1 = 2bh(x)-1 unutarnjih čvorova, čime je dokazan početni stav. Ako je visina stabla h, a prema svojstvu 4 bar pola čvorova mora biti crno, tada bh visina korijena mora biti barem h/2, pa broj čvorova ispod korijena iznosi n ≥ 2h/2-1. Logaritmiraući ovaj izraz dobijemo da je h ≤ 2 log2(n+1). Ovaj dokaz pokazuje da operacije traženja u crveno-crnom stablu uvijek ima složenost O(logn). Postavlja se pitanje, kako izvršiti operacije umetanja i brisanja, uz uvjet da se uvijek zadrži crveno-crno svojstvo stabla. Ideja je jednostavna, nakon umetanja čvora (i nakon brisanja čvora) treba provjeriti raspored crnih i crvenih čvorova. Ako raspored ne zadovoljava prethodnu definiciju svojstava crveno-crnog stabla, treba izvršiti transformaciju stabla. Transformacije stabla se vrše pomoću dva temeljna postupka - lijeve i desne rotacije čvorova. Ti postupci su ilustrirani na slici 20.5. Primjetite da nakon obje rotacije raspored čvorova ostaje nepromijenjen: Ključ(A) < Ključ(B) < Ključ (C), dakle rotacije mijenjaju oblik stabla, ali ne mijenjaju "inorder" raspored čvorova. Slika 20.5 Desna i lijeva rotacija čvorova Rotacije čvorova sa slike 20.5 vrše se pomoću funkcija rotateLeft() i rotateRight(). #ifdef RED_BLACK static void rotateLeft(BSTREE T, Node *x) { Node *y; /* rotira čvor x u lijevo */ assert (x); assert(Right(x)); y = Right(x); /* veza s Right(x) */ Right(x) = Left(y); if (Left(y) != NIL) Parent(Left(y)) = x; /* poveži s Parent(y) */ if (y != NIL) Parent(y) = Parent(x); if (Parent(x)) { if (x == Left(Parent(x))) Left(Parent(x)) = y; else 345
  • 346.
    Right(Parent(x)) = y; } else { Root(T) = y; } /* poveži x i y */ Left(y) = x; Parent(x) = y; } static void rotateRight(BSTREE T, Node *x) { Node *y; /* rotira čvor x u desno */ assert (x); y = Left(x); /* poveži s Left(x) */ Left(x) = Right(y); if (Right(y) != NIL) Parent(Right(y))= x; /* poveži s Parent(y) */ if (y != NIL) Parent(y) = Parent(x); if (Parent(x)) { if (x == Right(Parent(x))) Right(Parent(x)) = y; else Left(Parent(x)) = y; } else { Root(T) = y; } /* poveži x i y */ Right(y) = x; Parent(x) = y; } #endif Ove funkcije će biti korištene u funkciji insertFixup(), kojom se ispravlja ravnoteža stabla nakon umetanja čvora, i u funkciji deleteFixup(), kojom se ispravlja ravnoteža stabla nakon brisanja čvora. Funkcija insertFixup() se poziva na završetku funkcije bst_insert(), a funkcija deleteFixup() se poziva na završetku funkcije bst_delete(). Najprije će biti opisan algoritam koji se koristi u funkciji insertFixup(). Uravnoteženje stabla nakon umetanja čvora Prilikom umetanja čvora, u funkciji bst_insert(), uvijek se umeće crveni čvor. Ako je i roditelj umetnutog čvora crven, nastaje neuravnoteženo stablo jer više nije zadovoljeno svojstvo 4 iz definicije crveno-crnog stabla. Ta se neuravnoteženost ispravlja pozivom funkcije insertFixup(). Slike 20.6 i 20.7 omogućuju objašnjenje algoritma iz ove funkcije. Ukoliko umetnuti čvor nije korijen stabla, funkcija se izvršava unutar petlje sve dok je roditelj umetnutog čvora crven. Analizira se i ispravlja uravnoteženost stabla. Prvo se analiziraju ujaci umetnutog čvora: (Left(Parent(Parent(x))) i Right(Parent(Parent(x))). Ako su oni crveni, dovoljno je promijeniti boju čvorova, kao na slici 20.6, a ako su crni tada se vrši jednostruka ili dvostruka rotacija, uz promjenu boje čvorova, prema slici 20.7. U analizi nas ne zanimaju djeca umetnutog čvora, jer ako je roditelj umetnutog čvora crven, tada je, prema svojstvu 4, sigurno da su mu djeca crna. 346
  • 347.
    Slika 20.6 Prvislučaj analize umetanja čvora x. Crni čvorovi su prikazani tamnije. Nije zadovoljeno svojstvo 4 jer su crveno obojani i x i njegov roditelj. Ujak od x, odnosno y=Right(Parent(Parent(x))) je također crven. Svako podstablo A,B,C,D i E ima crni korijen i jednaku crnu-visinu. Nisu nužne rotacije već samo izmjena boje čvorova. Nakon prikazane promjene boje čvorova while petlja nastavlja s čvorom "Novi x" i ispituje mogući problem, tj. da li je njegov roditelj crven. #ifdef RED_BLACK static void insertFixup(BSTREE T, Node *x) { /* stvara Red-Black ravnotežu nakon umetanja čvora x */ /* podrazumjeva se da je Color(x) = RED; */ while (x != Root(T) && Color(Parent(x)) == RED) { if (Parent(x) == Left(Parent(Parent(x)))) { Node *y = Right(Parent(Parent(x))); if (Color(y) == RED) {/* ujak je RED - slučaj 1 */ Color(Parent(x)) = BLACK; Color(y) = BLACK; Color(Parent(Parent(x))) = RED; x = Parent(Parent(x)); } else { /* ujak je BLACK - slučaj 2*/ if (x == Right(Parent(x))) { /* postavi x za lijevo dijete */ x = Parent(x); rotateLeft(T, x); } /* oboji i rotiraj - slučaj 3 */ Color(Parent(x)) = BLACK; Color(Parent(Parent(x)))= RED; rotateRight(T, Parent(Parent(x))); } } else {/* slika u zrcalu prethodnog slučaja*/ Node *y = Left(Parent(Parent(x))); if (y->color == RED) {/* ujak je RED - slučaj 1 */ Color(Parent(x)) = BLACK; 347
  • 348.
    y->color = BLACK; Color(Parent(Parent(x))) = RED; x = Parent(Parent(x)); } else { /* ujak je BLACK - slučaj 2*/ if (x == Left(Parent(x))) { x = Parent(x); rotateRight(T, x); } /* oboji i rotiraj - slučaj 3 */ Color(Parent(x)) = BLACK; Color(Parent(Parent(x))) = RED; rotateLeft(T, Parent(Parent(x))); } } } Color(Root(T))= BLACK; } #endif z y D u Slučaj 2 A v x rotateLeft(u) B C z u y y D v v A v rotateRight(z) x rotateLeft(u) x x u z u C B z A B C D A B C D Slučaj 3 Slučaj 3 v y A z rotateRight(z) x u D Slučaj 2 B C Slika 20.7 Drugi i treći slučaj analize umetanja čvora x. Sada su nužne jednostruke i dvostruke rotacije. Svako podstablo A,B,C,D i E ima crni korijen i jednaku crnu-visinu. Slovom x je označen čvor koji se analizira unutar while petlje (to je umetnuti čvor), a y označava njegovog lijevog ili desnog ujaka. Uočite da se slučaj 2 i 3 razlikuju od slučaja 1 po boji ujaka. U slučaju 2 vrši se rotacija Parent(x) i dobije se slučaj 3. Zatim se vrši rotacija Parent(Parent(x) u konačni oblik. Vremensku složenosti umetanja čvora procjenjujemo na sljedeći način: 348
  • 349.
    1. Funkcija bst_insert()se izvršava u O(lg n) vremena jer se mjesto za umetanje čvora traži u uravnoteženom stablu. 2. U funkciji insertFixup() svaka iteracija uzima O(1) vremena, i pri svakoj iteraciji se pomičemo dvije razine naviše. Pošto ima O(log2n) razina, vrijeme izvršenja insertFixup() je također O(log2n) Ukupno, umetanje u crveno-crno stablo ima vremensku složenost O(log2n). Uravnoteženje stabla nakon brisanja čvora Nešto kompliciraniji zahtjevi za uravnoteženjem stabla se javljaju kada se u funkciji bst_delete() izbriše čvor y. Problem nastaje ako se izbriše crni čvor jer tada može nastati slučaj da su čvor i njegovo dijete crveni. To je u suprotnosti sa svojstvom 3 crveno-crnog stabla. Zbog toga se na kraju funkcije bst_delete(T, y) poziva funkcija deleteFixup(T,x) koja vrši uravnoteženje stabla. Čvor x je jedino dijete izbrisanog čvora y (možda jednak NIL). Unutar ove funkcije se prvo analizira boja od x, sa sljedećim posljedicama: 1. Ako je boja od x crvena, ili ako je x korijen stabla, tada se ne izvršava petlja, već se jedino boja od x promijeni u crnu boju. U tom slučaju nije potrebno analizirati uravnoteženost stabla, jer ako smo izbacili crni čvor (y), sada sada povratili crni čvor (x) Time smo osigurali da je crna-visina nepromijenjena. Pošto smo izbacili i crveni čvor (x) osigurali smo se da neće biti dva crvena čvora u odnosu dijete-roditelj. 2. Ako je boja od x crna, i ako x nije korijen stabla izvršava se petlja u kojoj se analizira uravnoteženost stabla. Ideja je da tražimo uzlaznom stazom,od čvora x, crveni čvor koji bi mogli pretvoriti u crni čvor. Na taj način bi se održala ravnoteža crnih čvorova (jer je prije izbrisan crni čvor y). Slika 20. 8 pokazuje četiri slučaja kod kojih treba izvršiti promjenu boje i/ili izvršiti potrebne rotacije u stablu, pri traženju tog crvenog čvora. Analiziraju se brat od x, to je čvor w, i roditelj od čvora x. Moguće je 8 slučajeva, 4 kad je brat desno od roditelja i 4 kad je brat lijevo od roditelja. Na slici 20.8 prikazana su 4 slučaja kada je brat desno dijete roditelja x. Slučajeve razlikujemo po boji djece od w. Čvor w je zgodan za analizu jer ne može biti NIL, inače bi imali odstupanje od svojstva 5 za čvor Parent(x) koji je i roditelj od w. (Ako je x crne boje, on mora imati brata, jer staza od Parent(x) do lista, na lijevo i na desno mora imati bar jedan crni čvor koji nije NIL). 349
  • 350.
    b d x a d w b e Slučaj 1 x c e a Novi w c E F A B C D E F A B C D b s Novi x b s x a d w a d c e Slučaj 2 c e A B A B C D E F C D E F b s b s w Novi w x a d x a c Slučaj 3 A B c e A B C d C D E F D e E F s s b d x a d w b e Slučaj 4 S' A B c S' e a c E F C D E F A B C D Novi x = Root(T) Slika 20.8 Slučajevi koji se obrađuju u while-petlji funkcije deleteFixup().Crni čvorovi su prikazani tamnije, crveni čvorovi s zadebljanim rubom,a ostali čvorovi, koji mogu biti ili crni ili crveni prikazani su s tanjim rubom i označeni slovima s i s'. Slova A,B,C,.. označavaju podstabla. Petlja se ponavlja jedino u drugom slučaju #ifdef RED_BLACK static void deleteFixup(BSTREE T, Node *x) { /* stvara Red-Black ravnotežu nakon brisanja čvora*/ while (x != Root(T) && Color(x) == BLACK) { if (x == Left(Parent(x))) { Node *w = Right(Parent(x)); if (Color(w) == RED) { /*slučaj 1*/ Color(w) = BLACK; Color(Parent(x)) = RED; rotateLeft(T, Parent(x)); w = Right(Parent(x)); } if (Color(Left(w)) == BLACK && Color(Right(w)) == BLACK) { Color(w) = RED; /* slučaj 2*/ 350
  • 351.
    x = Parent(x); } else { if (Color(Right(w)) == BLACK) { Color(Left(w)) = BLACK; /*slučaj 3*/ Color(w) = RED; rotateRight(T, w); w = Right(Parent(x)); } Color(w) = Color(Parent(x)); /*slučaj 4*/ Color(Parent(x)) = BLACK; Color(Right(w)) = BLACK; rotateLeft(T, Parent(x)); x = Root(T); } } else {/*slika u zrcalu (zamijeni lijevi <-> desni)*/ Node *w = Left(Parent(x)); if (Color(w) == RED) { Color(w) = BLACK; Color(Parent(x)) = RED; rotateRight(T, Parent(x)); w = Left(Parent(x)); } if (Color(Right(w)) == BLACK && Color(Left(w)) == BLACK) { Color(w) = RED; x = Parent(x); } else { if (Color(Left(w)) == BLACK) { Color(Right(w)) = BLACK; Color(w) = RED; rotateLeft(T, w); w = Left(Parent(x)); } Color(w) = Color(Parent(x)); Color(Parent(x)) = BLACK; Color(Left(w)) = BLACK; rotateRight(T, Parent(x)); x = Root(T); } } } Color(x) = BLACK; } #endif Slučaj 1: w je crven o w mora imati crnu djecu (prema svojstvu 5) o Postavi boju w crno i Parent(x) crveno (to kasnije može značiti kraj ispitivanja) o Lijevo rotiraj Parent(x) 351
  • 352.
    o Novi brat od x je bio dijete od w prije rotacije, pa mora bit crn o Nastavi razmatrati stanje prelaskom na slučajeve 2,3 ili4, u kojima će se, ovisno o položaju Novog w odrediti da li se Parent(x), koji je crven, pretvara u crni čvor. Slučaj 2: w je crn i oba djeteta od w su crna [Napomena: Čvor nepoznate boje označen tankim rubom i slovom s] o Promijeni boju od w u crveno o Pomakni promatranje na viši čvor tako da Parent(x) postane Novi x o Ponovi iteraciju. Iteracija prestaje ako je Novi x = Parent(x) crven (to je uvijek slučaj ako smo došli iz slučaja 1) Tada se Novi x konačno oboji u crno. Kraj! Primjetite da je zadržan broj crnih čvorova u stazi od x i u stazi od w. U obje staze najprije je smanjen broj crnih čvorova, a zatim je na kraju dodan crni čvor. Slučaj 3: w je crn, lijevo dijete od w je crveno, a desno dijete od w je crno o Pomijeni boju od w u crveno i boju lijevog djeteta od w u crno o Desno rotiraj w o Novi w, tj. novi brat od x je crn s crvenim desnim djetetom o Proslijedi analizu na slučaj 4 Slučaj 4: w je crn, lijevo dijete od w je crno, a desno dijete od w je crveno [Napomena: Dva čvora nepoznate boje označeni su slovima s i s'.] o Postavi boju w jednaku boji Parent(x) o Postavi boju Parent(x) crno i boju desnog djeteta od w crno o Zatim lijevo rotiraj Parent(x) o Pošto je jedan crveni čvor pretvoren u crni (Right(w)), cilj je ispunjen. Završi petlju na način da postaviš da je x jednak Root(T). Kraj! Vremensku složenost ovih operacija određuje složenost tri procesa: 352
  • 353.
    1. Složenost bst_delete()je O(log2n), jer se traženje vrši u zravnoteženom stablu. 2. Složenost deleteFixup() je određena slučajem 2 jer se jedino u njemu može javiti potreba za više iteracije. U svakoj iteraciji se analiza podiže jednu razina više, pa je maksimalno moguće O(log2n) iteracija 3. Slučajevi 1, 3 i 4 imaju samo 1 rotaciju, pa ukupno ne može biti više od 3 rotacije. Ukupna vremenska složenost je O(log2n) i to s malim konstantnim faktorom. Upravo brzina operacije brisanja daje crveno-crnom stablu malu prednost nad drugim metodama uravnoteženja stabla (primjerice, AVL metoda, u najgoram slučaju, kod brisanja vrši O(log2n) rotacija). Zadatak: Napišite specijaliziranu verziju ADT binarnog stabla koja se od ADT BSTREE razlikuje po tome da simboli nisu određeni parom <ključ, vrijednost>, već samo sadrže ključ. Rezultirajući ADT nazovite imenom MULTISET, čime se aludira na skup podataka koji može sadržavati više istih objekata. Definirajte sve operacije koje postoje kod BST, ali promijenite imena operacija tako da sva imena počinju prefiksom mset_. Testirajte ADT MULTISET na problemu sortiranja niza pomoću stabla traženja. Koristite algoritam za sortiranje stablom koji se obično naziva Treesort, a sastoji se od sljedećih koraka. Problem: Sotiraj niz A koji sadrži od n objekta tipa T, tako da vrijedi A[i] < A[i+1] Algoritam: Inicijaliziraj ADT MULTISET imena Skup, za tip podataka T Ponavljaj za sve elemente niza A[i], i=0,1, n-1 mset_insert(Skup, A[i]) SYMBOL s ← mset_minimum(Skup); Ponavljaj za i=0,1, n-1 A[i] ← mset_symbol_key(s) s ← mset_sucessor(s) Kraj! Primjetite da umetanje jednog podatka u stablo traje O(log2n). Pošto ukupno postoji n podataka, unos u stablo traje O(n log2n). Ispis iz stabla traje O(n). Zaključujemo da sortiranje stablom (treesort) ima vremensku složenost O(n log2n) + O(n) = O(n log2n). Teorijski ovo je isti rezultat kao kod Mergesort ili Heapsort metode, ili kao prosječni slučaj složenosti kod Quicksort metode. U praksi, zbog manjeg konstantnog faktora, preferira se Quicksort metoda. 353
  • 354.
    Literatura [1] American NationalStandards Institute: American national standard for information systems—Programming language - C, ANSI X3.159-1989, 1989. [2] International Standard ISO/IEC 9899: Programming Language C, 1999. [3] B. W. Kernighan and D. M. Ritchie: The C programming language (2nd ed.), Englewood Cliffs, NJ: Prentice -Hall, 1988. [4] D. M. Ritchie: The development of the C language. Second ACM HOPL Conference, Cambridge, MA. 1993. [5] R. Sedgewick: Algorithms in C, Addison-Wesley, 1998. [6] M. A. Weiss: Data Structures and Algorithm Analysis in C, Addison-Wesley, 1997. [7] T. H. Cormen, C. E. Leiserson and R. L. Rivest: Introduction to Algorithms, MIT Press, 1990. [8] D. E. Knuth: Sorting and Searching, volume 3 of The Art of Computer Programming, Addison-Wesley, 1973. [9] A. V. Aho, J. E. Hopcroft and J. D. Ullman: Data Structures and Algorithms, Addison- Wesley, 1983. [10] A. V. Aho, J. E. Hopcroft and J. D. Ullman: Compilers, Addison-Wesley, 1985. [11] E. Horowitz and S. Sahni: Fundamentals of Computer Algorithms, Computer Science Press, 1978. [12] N. Wirth: Algorithms and Data Structures, Prentice Hall, 1986. [13] A. M. Berman: Data Structures via C++, Oxford Press, 1997. [13] W. J. Collins: Data Structures and Standard Template Library, McGraw-Hill, 2003. 354
  • 355.
    Dodatak Dodatak A -Elementi dijagrama toka 355
  • 356.
    Dodatak B -Gramatika C jezika Sintaktička i leksička pravila C-jezika su zapisana na sljedeći način: Neterminalni simboli su zapisani kurzivom. Terminalni simboli su zapisani na isti način kao u ciljnom jeziku Opcioni simboli su označeni indeksom opt (Simbolopt ili Simbolopt). Pravila imaju oblik: neterminalni-simbol : niz-terminalnih-i-neterminalnih-simbola Alternativna pravila su zapisana u odvojenim redovima. Iznimno, kada su zapisana u jednom retku, odvojena su okomitom crtom Sintaksa deklaracija i definicija C jezika kompilacijaska-jedinica: definicija-funkcije deklaracija kompilacijaska-jedinica deklaracija kompilacijaska-jedinica definicija-funkcije definicija-funkcije: deklaracija-specifikatoraopt deklarator lista-deklaracijaopt složena-naredba lista-deklaracija: deklaracija lista-deklaracija deklaracija deklaracija: deklaracija-specifikatora lista-init-deklaratoraopt; deklaracija-specifikatora: specifikacija-klase-spremanja deklaracija-specifikatoraopt specifikacija-tipa deklaracija-specifikatoraopt specifikacija-kvalifikacije deklaracija-specifikatoraopt specifikacija-klase-spremanja : auto | register | static | extern | typedef specifikacija-tipa: void | char | short | int | long float | double | signed | unsigned struct-ili-union-specifikator enum-specifikator typedef-ime specifikacija-kvalifikacije: const | volatile struct-ili-union-specifikator: struct-ili-union identifikatoropt { struct-lista-deklaracija } struct-ili-union identifikator struct-ili-union: struct | union struct-lista-deklaracija: struct-deklaracija struct-lista-deklaracija struct-deklaracija lista-init-deklaratora: init-deklarator lista-init-deklaratora, init-deklarator init-deklarator: deklarator deklarator = inicijalizacija struct-deklaracija: lista-specifikatora lista-struct-deklaratora; lista-specifikatora: specifikacija-tipa lista-specifikatoraopt 356
  • 357.
    specifikacija-kvalifikacije lista-specifikatoraopt lista-struct-deklaratora: struct-deklarator lista-struct-deklaratora , struct-deklarator struct-deklarator: deklarator deklaratoropt : konstanti-izraz enum-specifikator: enum identifikatoropt { enumerator-lista } enum identifikator enumerator-lista: enumerator enumerator-lista , enumerator enumerator: identifikator identifikator = konstanti-izraz deklarator: pokazivačopt direktni-deklarator direktni-deklarator: identifikator (deklarator) direktni-deklarator [ konstanti-izrazopt ] direktni-deklarator ( parametari-funkcije ) direktni-deklarator ( lista-identifikatoraopt ) pokazivač: * lista-specifikacija-kvalifikacijeopt * lista-specifikacija-kvalifikacijeopt pokazivač lista-specifikacija-kvalifikacije: specifikacija-kvalifikacije lista-specifikacija-kvalifikacije specifikacija-kvalifikacije parametari-funkcije: lista-parametara lista-parametara , ... lista-parametara: deklaracija-parametra lista-parametara , deklaracija-parametra deklaracija-parametra: deklaracija-specifikatora deklarator deklaracija-specifikatora apstraktni-deklaratoropt lista-identifikatora: identifikator lista-identifikatora , identifikator inicijalizacija: izraz-pridjele { lista-inicijalizacije } { lista-inicijalizacije , } lista-inicijalizacije: inicijalizacija lista-inicijalizacije , inicijalizacija ime-tipa: lista-specifikatora apstraktni-deklaratoropt apstraktni-deklarator: pokazivač pokazivačopt direktni-apstraktni-deklarator direktni-apstraktni-deklarator: ( apstraktni-deklarator ) direktni-apstraktni-deklaratoropt [konstanti-izrazopt ] direktni-apstraktni-deklaratoropt ( parametari-funkcijeopt ) typedef-ime: identifikator 357
  • 358.
    Sintaksa naredbi naredba: naredba-iteracije: naredbeni izraz while ( izraz ) naredba složena-naredba do naredba while ( izraz ) ; naredba-selekcije for ( izrazopt ; izrazopt ; izrazopt ) naredba naredba-iteracije naredba-skoka naredba-selekcije: označena-naredba if ( izraz ) naredba naredbeni izraz : if ( izraz ) naredba else naredba izrazopt ; switch ( izraz ) naredba složena-naredba : naredba-skoka : { lista-deklaracijaopt niz-naredbiopt } goto identifikator ; lista-deklaracija : continue; deklaracija break; lista-deklaracija deklaracija return izrazopt ; niz-naredbi: označena-naredba: naredba identifikator: naredba niz-naredbi naredba case konstanti-izraz : naredba default : naredba Sintaksa izraza primarni-izraz: relacijski-izraz: identifikator posmačni-izraz konstanta relacijski-izraz < posmačni-izraz literalni-string relacijski-izraz > posmačni-izraz (izraz ) relacijski-izraz <= posmačni-izraz postfiks-izraz: relacijski-izraz >= posmačni-izraz primarni-izraz izraz-jednakosti: postfiks-izraz [ izraz ] relacijski-izraz postfiks-izraz ( lista-argumenata-opt ) izraz-jednakosti == relacijski-izraz postfiks-izraz . identifikator izraz-jednakosti != relacijski-izraz postfiks-izraz -> identifikator AND-izraz: postfiks-izraz ++ izraz-jednakosti postfiks-izraz – AND-izraz & izraz-jednakosti ( ime-tipa ) { lista-inicijalizacije } XOR-izraz: ( ime-tipa ) { lista-inicijalizacije , } AND-izraz lista-argumenata: XOR-izraz ^ AND-izraz izraz-pridjele OR-izraz: lista-argumenata , izraz-pridjele XOR-izraz unarni-izraz: OR-izraz | XOR-izraz postfiks-izraz logički-AND-izraz: ++ unarni-izraz OR-izraz -- unarni-izraz logički-AND-izraz && OR-izraz unarni-operator cast-izraz logički-OR-izraz: sizeof unarni-izraz logički-AND-izraz sizeof ( ime-tipa ) logički-OR-izraz || logički-AND-izraz unarni-operator: uvjetni-izraz: & | * | + | - | ~ | ! logički-OR-izraz cast-izraz: logički-OR-izraz ? expr : uvjetni-izraz unarni-izraz izraz-pridjele: ( ime-tipa ) cast-izraz uvjetni-izraz multiplikativni-izrazr: unarni-izraz operator-pridjele izraz-pridjele cast-izraz operator-pridjele: multiplikativni-izrazr * cast-izraz = | *= | /= | %= | += | -= multiplikativni-izrazr / cast-izraz | <<= | >>= | &= | ^= | |= multiplikativni-izrazr % cast-izraz izraz: aditivni-izraz: izraz-pridjele multiplikativni-izraz izraz , izraz-pridjele aditivni-izraz + multiplikativni-izraz konstantni-izraz: aditivni-izraz - multiplikativni-izraz uvjetni-izraz posmačni-izraz: aditivni-izraz posmačni-izraz << aditivni-izraz posmačni-izraz >> aditivni-izraz 358
  • 359.
    Regularna gramatika zazapis literalnih konstanti konstanta: integer-konstanta nenulta-znamenka: float-konstanta 1|2|3|4|5|6|7|8|9 char-konstanta enum-konstanta znamenka : literalni-string 0|nenulta-znamenka integer-konstanta: heksa-znamenka: 0 znamenka decimalna-konstanta int-sufiksopt |A|B|C|D|E|F|a|b|c|d|e|f oktalna-konstanta int-sufiksopt hexadecimalna-konstanta int-sufiksopt oktalna-znamenka : 0|1|2|3|4|5|6|7 decimalna-konstanta: nenulta-znamenka niz-znamenki: decimalna-konstanta znamenka znamenka niz-znamenki znamenka oktalna-konstanta: 0oktalna_znamenka oktalna-konstanta oktalna-namenka char-konstanta: 'char' heksa-konstanta: L 'char' 0xheksa-znamenka 0Xheksa-znamenka char: heksa-konstanta heksa-znamenka reg-char escape-sequence int-sufiks: unsigned-sufiks long-sufiksopt reg-char: long-sufiks unsigned-sufiksopt bilo koji ASCII znak osim apostrofa ('), backslash() i znaka za novu liniju. unsigned-sufiks: u | U long-sufiks: l | L escape-sekvenca: float-konstanta: ' | " | | d floating-konstanta float-sufiksopt | dd | ddd | xd | xdd | xddd | a | b | f float-sufiks: L | l | F | f | n | r | t | v (d je znamenka) floating-konstanta: enum-konstanta: frakciona-konstanta exponent identifikator frakciona-konstanta niz-znamenki eksponent literalni_string: "" frakciona-konstanta: "char-sekvenca" niz-znamenki.niz-znamenki L"char-sekvenca" .niz-znamenki niz-znamenki. char-sekvenca: char eksponent: char-sekvenca char e predznak niz-znamenki E predznak niz-znamenki e niz-znamenki Napomena: E niz-znamenki Znak navodnika(") se ne može direktno unositi u string, već kao escape-sekvenca ". predznak: +|- Prefiks L označava prošireni znakove (wchar_t) 359
  • 360.
    Klučne riječi C- jezika auto double int struct break else long switch case enum register typedef char extern return union const float short unsigned continue for signed void default goto sizeof volatile do if static while Tip operatora Operator Asocijativnost primarni [] . -> () s lijeva na desno postfiks –unarni ++ -- s lijeva na desno prefiks – unarni ++ -- & * + - ~ ! sizeof cast s desna na lijevo multiplikativni */% s lijeva na desno aditivni +- s lijeva na desno posmačni << >> s lijeva na desno relacijski < > <= >= s lijeva na desno jednakost == != s lijeva na desno bitznačajni "i" & s lijeva na desno ekskluzivni "ili" ^ s lijeva na desno bitznačajni "ili" | s lijeva na desno logički "i" && s lijeva na desno logički "ili" || s lijeva na desno ternarni uvjetni op. ?: s desna na lijevo pridjela vrijednosti = *= /= %= += -= <<= >>= &= ^= |= s desna na lijevo zarez , s lijeva na desno Prioritet i asocijativnost operatora 360
  • 361.
    Dodatak C -Standardna biblioteka C jezika Standardna biblioteka C jezika definirana je dokumentom: American national standard for information systems - Programming language C, ANSI X3.159-1989, American National Standards Institute. 1989. Sadrži niz korisnih funkcija, konstanti (makro naredbi), typedef tipova i globalnih varijabli (pr. stdin, stdout). Deklaracija tih objekata zapisana je u datotekama koje se nazivaju zaglavlja ili h-datoteke (eng. headers), a koriste se kao početne #include datoteke u gotovo svakom izvornom programu. Funkcionalna podjela h-datoteke (zaglavlja) temeljne definicije <stddef.h> rad s ulazno izlaznim uređajima i datotekama <stdio.h> radnje s znakovima i stringovima <string.h>, <ctype.h>, <stdlib.h>, <stdio.h> alociranje memorije <stdlib.h> matematičke funkcije <math.h> datum i vrijeme <time.h> promjenljivi broj argumenata <stdarg.h> globalni skokovi <setjmp.h> asertacija <assert.h> prihvat signala stanja računalnog procesa <signal.h> prihvat i dojava pogreške <errno.h>, <stdio.h>, <string.h> minimalne i maksimalne vrijednosti tipova <limits.h>, <float.h> lokalizacija znakovnih zapisa <locale.h>, <stdlib.h>, <wchar.h>, <wctype.h> sučelje s operativnim sustavom <stdlib.h> Slijedi opis funkcija definiranih standardom. Za svaku funkciju navodi se deklaracija, opis argumenata funkcije i tip vrijednosti koju funkcija vraća. C 1 <stdio.h> U zaglavlju <stdio.h>, definiran je niz funkcija za rad s ulazno/izlaznim tokovima. Svakom toku je pridijeljen pokazivač na strukturu FILE koja je također definirana u <stdio.h>. Standardni ulaz, standardni izlaz i standardni tok dojave greške se automatski inicijaliziraju pri pokretanju programa, a njihov pokazivač na strukturu FILE je u globalnim varijablama: FILE *stdin; /* pokazivač toka standardnog ulaza */ FILE *stdout; /* pokazivač toka standardnog izlaza */ FILE *stderr; /* pokazivač toka dojave greške */ Iniciranje pokazivača datotečnih tokova vrši se pomoću funkcije fopen(). Kada se završi rad s datotekom treba zatvoriti njen tok pomoću funkcije fclose(). U zaglavlju <stdio.h> definirana je i simbolička konstanta EOF koja služi kao oznaka kraja datoteke. Otvaranje i zatvaranje toka fopen FILE *fopen(const char *name, const char *mode); 361
  • 362.
    fopen() otvara tokza datoteku imena name. Ako je operacija uspješna, funkcija vraća pokazivač toka, a ako je neuspješna tada vraća NULL. String mode označava način na koji će biti otvorena datoteka. Prvi znak tog stringa mora biti 'r', 'w', ili 'a', što označava da se datoteka otvara za čitanje, pisanje ili dopunjavanje. Ako se nakon prvog znaka napiše znak '+' tada se datoteka otvara za čitanje i pisanje. Dodatno se može napisati i znak 'b'. To je oznaka da se na MS-DOS sustavima datoteka tretira kao binarna datoteka. U suprotnom datoteka se otvara u tekstualnom modu. MS-DOS /Windows sustavi znak za kraj retka 'n' u tekstualnoj datoteci pretvaraju u dva znaka 'rn', također se znak Ctrl-Z (ASCII vrijednost 26) tretira kao oznaka kraja datoteke. Ove pretvorbe ne postoje na Unix sustavima. freopen FILE *freopen(const char *name, const char *mode, FILE *fp); freopen() služi kao i fopen(), ali na način da se datoteka imena name poveže s postojećim pokazivačem toka fp. Tipično se koristi za redirekciju sa standardnih tokova, primjerice, naredbom freopen("output.log", "w", stdout); budući izlaz, koji generiraju funkcije koje djeluju na stdin (primjerice putchar i printf), bit će preusmjeren u datoteku output.log. fclose int fclose(FILE *fp); Funkcija fclose() zatvara tok fp. U slučaju greške vraća EOF, inače vraća 0. Funkcija fclose() se automatski primjenjuje na sve otvorene tokove pri normalnom završetku programa. fflush int fflush(FILE *fp); Ako je otvoren tok fp, funkcijom fflush() svi se podaci iz privremenog datotečnog spremnika zapišu u datoteku. Funkcija vraća 0, ili EOF ako nastupi greška. Ulazno izlazne opracije getchar, getc, fgetc int getchar(); int getc(FILE *fp); int fgetc(FILE *fp); Funkcija getc() vraća sljedeći dobavljivi znak iz toka fp, ili EOF ako je kraj datoteke. Obično EOF ima vrijednost -1, kako bi se razlikovao od ostalih znakova. Zbog toga ova funkcija vraća tip int, a ne tip char. Funkcija getchar() je ekvivalentna getc(stdin). Funkcija fgetc() je ekvivalentna getc(), ali može biti implementirana kao makro naredba. putchar, putc, fputc int putchar(int c); 362
  • 363.
    int putc(int c,FILE *fp); int fputc(int c, FILE *fp); Funkcija putc() upisuje znak c u tok fp. Funkcija putchar(c) je ekvivalentna putc(c, stdout). Funkcija fputc() je ekvivalentna putc(),ali može biti implementirana kao makro naredba. Sve tri funkcije vraćaju upisani znak, ili EOF u slučaju greške. printf, fprintf int printf(const char *format, ...); int fprintf(FILE *fp, const char *format, ...); Funkcija fprintf() upisuje tekstualno formatirane argumente u tok fp. Tri točkice označavaju listu argumenata. Format se određuje u stringu format, koji sadrži znakove koji se direktno upisuju na izlazni tok i specifikatore formatiranog ispisa argumenata. printf(format, …) je ekvivalentno fprintf(stdout, format, …). Specifikator formata se sastoji od znaka %, iza kojeg slijede oznake za širinu i preciznost ispisa te tip argumenta, u sljedećem obliku: %[prefiks][širina_ispisa][. preciznost][veličina_tipa]tip_argumenta Specifikator formata mora započeti znakom % i završiti s oznakom tipa argumenta. Sva ostala polja su opciona (zbog toga su napisana unutar uglatih zagrada). U polje širina_ispisa zadaje se minimalni broj kolona predviđenih za ispis vrijednosti. Ako ispis sadrži manji broj znakova od zadane širine ispisa, na prazna mjesta se ispisuje razmak. Ako ispis sadrži veći broj znakova od zadane širine, ispis se proširuje. Ako se u ovo polje upiše znak * to znači da će se broj kolona indirektno očitati iz slijedećeg argumenta funkcije, koji mora biti tipa int. Polje prefiks može sadržavati jedan znak koji ima sljedeće značenje: - Ispis se poravnava prema lijevoj granici ispisa određenog poljem širina_ispisa. (inače se poravnava s desne strane) U prazna mjesta se upisuje razmak + Pozitivnim se vrijednostima ispisuje i '+' predznak. razmak Ako je vrijednost positivna, dodaje se razmak prije ispisa (tako se može poravnati kolone s pozitivnim i negativnim brojevima). 0 Mjesta razmaka ispunjuju se znakom 0. # Alternativni stil formatiranja Polje . preciznost određuje broj decimalnih znamenki iza decimalne točke kod ispisa realnog broja ili minimalni broj znamenki ispisa cijelog broja ili maksimalni broj znakova koji se ispisuje iz nekog stringa. Ovo polje mora započeti znakom točke, a iza nje se navodi broj ili znak *, koji znači da će se preciznost očitati iz slijedećeg argumenta tipa int. Ukoliko se ovo polje ne koristi, tada se podrazumijeva da će realni brojevi biti ispisani s maksimalno šest decimalnih znamenki iza decimalne točke. Polje tip_argumenta može sadržavati samo jedan od sljedećih znakova znakova: c Argument se tretira kao int koji se ispisuje kao znak iz ASCII skupa. d, i Argument se tretira kao int, a ispisuje se decimalnim znamenkama. e, E Argument je float ili double, a ispis je u eksponentnom formatu. f Argument je float ili double, a ispis je prostom decimalnom formatu. Ako je prefiks # i 363
  • 364.
    preciznost .0, tadase ne ispisuje decimalna točka. g, G Argument je float ili double, a ispis je prostom decimalnom formatu ili u eksponencijalnom formatu, ovisno o tome koji daje precizniji ispis u istoj širini ispisa. o Argument je unsigned int, a ispisuje se oktalnim znamenkama. p Argument se tretira kao pokazivač tipa void *, pa se na ovaj način može ispisati adresa bilo koje varijable. Adresa se obično ispisuje kao heksadecimalni broj. s Argument mora biti literalni string odnosno pokazivač tipa char *. u Argument je unsigned int, a ispisuje se decimalnim znamenkama. x, X Argument je unsigned int, a ispisuje se heksadecimalnim znamenkama. Ako se zada prefiks # , ispred heksadecimalnih znamenki se ispisuje 0x ili 0X. % Ne odnosi se na argument, već znači ispis znaka % n Ništa se ne ispisuje, a odgovarajući argument mora biti pokazivač na int, u kojega se upisuje broj do tada ispisanih znakova Polje veličina_tipa može sadržavati samo jedan znak koji se upisuje neposredno ispred oznake tipa. h Pripadni argument tipa int tretira se kao short int ili unsigned short int. l Pripadni argument je long int ili unsigned long int. L Pripadni argument realnog tipa je long double. Funkcije printf() i fprintf() vraćaju broj ispisanih znakova. Ako nastupi greška, vraćaju negativan broj. scanf, fscanf int scanf(const char *format, ...); int fscanf(FILE *fp, const char *format, ...); Funkcijom fscanf() dobavljaju se vrijednosti iz tekstualnog toka fp po pravilima pretvorbe koji su određeni u stringu format. Te vrijednosti se zapisuju na memorijske lokacije određene listom pokazivačkih argumenata. Lista argumenata (...) je niz izraza odvojenih zarezom, čija vrijednost predstavlja adresu postojećeg memorijskog objekta. Tip objekta mora odgovarati specifikaciji tipa prethodno zapisanog u stringu format. scanf(format, …) je ekvivalentno fscanf(stdin, format, …). String format se formira od proizvoljnih znakova i specifikatora formata oblika: %[prefiks][širina_ispisa][veličina_tipa]tip_argumenta Ako se pored specifikatora formata navedu i proizvoljni znakovi tada se očekuje da i oni budu prisutni u ulaznom tekstu (osim tzv. bijelih znakova: razmak, tab i nova linija) . Osim u slučaju specifikatora %c, %n, %[, i %% , za sve pretvorbe se podrazumijeva da se iz ulaznog teksta odstrane bijeli znakovi. Jedini opcioni prefiks je znak '*' koji znači da se pretvorena vrijednost ne dodjeljuje ni jednom argumentu. U polju širina_ispisa zadaje se maksimalni broj kolona predviđenih za dobavu vrijednosti. 364
  • 365.
    Polje veličina_tipa možesadržavati samo jedan znak koji se upisuje neposredno ispred oznake tipa pokazivačkog argumenta. To su sljedeći znakovi: h Pripadni argument tipa int tretira se kao short int ili unsigned short int. l Pripadni argument je long int ili unsigned long int. L Pripadni argument realnog tipa je long double. Polje tip_argumenta može sadržavati samo jedan od sljedećih znakova: c Dobavlja se jedan znak, a argument je tipa char *. Ako je definirano polje širina_unosa, tada se unosi onoliko znakova kolika je vrijednost širina_unosa. d Dobavlja se cijeli broj, a argument je tipa int * ( ili short int * ili long int *, ako je zadana veličina tipa h ili l). i Dobavlja se cijeli broj, a argument je tipa int * ( ili short int * ili long int *, ako je zadana veličina tipa h ili l). Pretvorba se vrši i ako je broj zapisan oktalno (počinje znamenkom 0) ili heksadecimalno (počinje s 0x ili 0X), e, E, f, Dobavlja se realni broj. Argument je tipa float * ili double *. g, G o Dobavlja se cijeli broj zapisan oktalnim znamenkama. Argument je tipa unsigned int* (ili unsigned short int * ili unsigned long int *, ako je zadana veličina tipa h ili l). p Dobavlja se pokazivač, a argument mora biti tipa void **. s Dobavlja se string koji ne sadrži bijele znakove. Argument mora biti tipa char *. u Dobavlja se cijeli broj, a argument je tipa unsigned int * ( ili unsigned short int * ili unsigned long int *, ako je zadana veličina tipa h ili l). x, X Dobavlja se cijeli broj zapisan heksadecimalno. Argument je tipa unsigned int* Argument je tipa unsigned int* (ili unsigned short int * ili unsigned long int *, ako je zadana veličina tipa h ili l). % Ne odnosi se na argument, već znači dobavu znaka % n Ne vrši se pretvorba. Odgovarajući argument mora biti tipa int *. Upisuje se broj do tada učitanih znakova [ Za format oblika %[...] dobavlja se string koji sadrži znakove koji su unutar zagrada, prema sljedećem obrascu: %[abc] znači da se dobavlja string koji može sadržavati znakove a,b ili c. %[a-d] znači da se dobavlja string koji može sadržavati znakove a,b,c ili d. %[^abc] znači da se dobavlja string koji može sadržavati sve znakove osim a,b ili c. %[^a-d] znači da se dobavlja string koji može sadržavati znakove sve znakove osim a,b,c ili d. Argument je tipa char *. Funkcije scanf()i fscanf() vraćaju broj uspješno izvršenih pretvorbi (bez %n i %*). Ako se ne izvrši ni jedna konverzija, zbog dosegnutog kraja datoteke, vraća se vrijednost EOF. gets, fgets char *fgets(char *buf, int n, FILE *fp); char *gets(char *buf); Funkcije fgets() čita liniju teksta (koja završava znakom 'n') iz toka fp, i sprema taj tekst (uključujući 'n' i zaključni znak '0') u znakovni niz buf. Veličina tog niza je n znakova. Ako u liniji ima više od n-2 znakova, tada neće biti dobavljeni svi znakovi. Funkcije gets() je ekvivalentna funkciji fgets(), ali isključivo služi za dobavu stringa s tipkovnice. 365
  • 366.
    Ona ne prenosiznak nove linije i ne vrši kontrolu broja unesenih znakova. Obje funkcije vraćaju pokazivač na dobavljeni string ili NULL ako je greška ili kraj datoteke. puts, fputs int puts(char *str); int fputs(char *str, FILE *fp); Funkcija fputs() ispisuje string str u tok fp. Funkcija puts() ispisuje string str na stdout i dodaje znak nove linije. Obje funkcije vraćaju pozitivnu vrijednost ili EOF ako nastane greška. fread size_t fread(void *buf, size_t elsize, size_t n, FILE *fp); Funkcija fread() iz toka fp čita blok veličine n x elsize bajta i upisuje u spremnik buf. Za dobavu n elemenata nekog tipa, elsize treba biti jednak sizeof(tip elementa). Kod čitanja znakova je elsize = 1. Funkcija vraća broj dobavljenih elemenata ili EOF u slučaju greške. fwrite size_t fwrite(void *buf, size_t elsize, size_t n, FILE *fp); Funkcija fwrite() upisuje u tok fp blok veličine n x elsize bajta iz spremnika buf. Za upis n elemenata nekog tipa, elsize treba biti jednak sizeof(tip elementa). Kod upisa n znakova elsize = 1. Funkcija vraća broj upisanih elemenata ili EOF u slučaju greške. ungetc int ungetc(int c, FILE *fp); Funkcija ungetc() umeće u tok fp znak c, tako da će pri sljedećem čitanju taj znak biti prvi očitan. Ova operacija se može obaviti samo nakon operacije čitanja iz toka. Funkcija vraća znak c ili EOF u slučaju greške. Pozicioniranje toka Mjesto na kojem se vrši čitanje/pisanje u tok naziva se trenutna pozicija toka (eng. stream position). U radu s binarnim datotekama može se kontrolirati i postavljati trenutnu poziciju toka. ftell long int ftell(FILE *fp); Funkcija ftell() vraća long int vrijednost koja je jednaka trenutnoj poziciji toka fp (kod binarnih datoteka to je pomak u bajtima od početka datoteke). Ako se radi s velikom datotekama, u kojima pozicija može biti veće od long int, tada treba koristiti funkciju fgetpos(). fseek, rewind int fseek(FILE *fp, long int pos, int from); void rewind(FILE *fp); 366
  • 367.
    Funkcija fseek() postavljatrenutnu poziciju toka fp na poziciju pos, relativno prema vrijednosti from argumenta, koji može imati tri vrijednosti: SEEK_SET (početak datoteke) , SEEK_CUR (trenutna pozicija) i SEEK_END (kraj datoteke). Argument pos može biti negativna vrijednost. Funkcija vraća 0, ako je operacija uspješna. Ako je argument from jednak SEEK_SET, nova_pozicija = pos; Ako je argument from jednak SEEK_CUR, nova_pozicija = trenutna_pozicija + pos; Ako je argument from jednak SEEK_END nova_pozicija = pozicija_kraja_datoteke + pos; Nova pozicija mora biti veća ili jednaka nuli, i može biti veća od trenutne pozicije. Funkcija rewind(fp) postavlja poziciju na 0, što je ekvivalentno fseek(fp,0,SEEK_SET). Ovu funkciju se može koristiti i s tekstualnim datotekama. fgetpos, fsetpos int fgetpos(FILE *fp, fpos_t *pos); int fsetpos(FILE *fp, const fpos_t *pos); Funkcija fgetpos() zapisuje trenutnu poziciju toka fp u fpos_t objekt na kojega pokazuje pos. Funkcija fsetpos() postavlja trenutnu poziciju toka fp na vrijednost fpos_t objekta na kojeg pokazuje pos (to mora biti vrijednost prethodno dobivena funkcijom fgetpos). Obje funkcije vraćaju 0 ako je operacija uspješna. Kontrola ulazno/izlaznog spremnika setbuf, setvbuf void setbuf(FILE *fp, char *buf); void setvbuf(FILE *fp, char *buf, int mode, size_t size); Pomoću ovih funkcija postavlja se korisnički definirani spremnik buf kao spremnik ulazno/izlaznih operacija. Primjenjuju se prije poziva ulazno/izlaznih operacija na otvoreni tok fp. Kod funkcije setbuf() veličina spremnika mora biti jednaka BUFSIZE (definiran u stdio.h). Ako je buf==NULL tada se ne koristi spremnik. Kod funkcije setvbuf() veličina spremnika se postavlja argumentom size, a način korištenja spremnika se postavlja argumentom mode, koji može imati tri predefinirane vrijednosti: _IONBF Ne koristi se spremnik, već se vrši neposredan pristup datoteci. _IOFBF Spremnik se koristi potpuno, tj. spremnik se prazni tek kada je popunjen. _IOLBF Spremnik se koristi djelomično (uvijek se prazni kada se ispisuje znak nove linije 'n' ). Dojava i prihvat greške feof int feof(FILE *fp); Funkcija feof() vraća nenultu vrijednost ako je tok u poziciji kraj datoteke, inače vraća 0. 367
  • 368.
    ferror int ferror(FILE *fp); Funkcija ferror() vraća nenultu vrijednost ako je nastala greška u radu s tokom, inače vraća 0. clearerr void clearerr(FILE *fp); Funkcija clearerr() briše indikatore greške ili kraj datoteke za tok fp. perror void perror(const char *prefix); Funkcija perror() ispisuje poruku na stderr o trenutno nastaloj greški. Tip greške se bilježi u globalnoj varijabli errno. Poruka je ista kao i poruka koja se dobije pozivom funkcije strerror(errno). Drugim riječima, perror(p) je otprilike ekvivalentna pozivu fprintf(stderr, "%s: %s", p == NULL ? "" : p, strerror(errno)); Argument prefix je proizvoljni string koji se ispisuje ispred poruke. Operacije s formatiranim stringovima sprintf, sscanf int sprintf(char *buf, const char *format, ...); int sscanf(const char *buf, const char *format, ...); Funkcije sprintf() i sscanf() su varijante od printf() i scanf() koje umjesto ulazno/izlaznog toka koriste proizvoljno odabrani string buf. Znak kraja stringa '0' se tretira kao znak za kraj datoteke. Korisnik mora voditi računa o tome da veličina stringa buf bude dovoljno velika, da se može izvršiti sve pretvorbe formata u sprintf() funkciji. Operacije s promjenjljivom listom argumenata vprintf, vfprintf, vsprintf int vprintf(const char *format, va_list argp); int vfprintf(FILE *fp, const char *format, va_list argp); int vsprintf(char *buf, const char *format, va_list argp); Ove funkcije omogućuju definiranje funkcija s promjenljivim brojem argumenata, koje imaju funkcionalnost kao printf(), fprintf() i sprintf() funkcije. Posljednji argument ovih funkcija argp je pokazivač tipa va_list. To omogućuje rad s promjenljivim brojem argumenata. U sljedećem programu definirana je funkcija za dojavu greške, koju se može koristiti za dojavu greške pri analizi izvornog programa. Ona koristi dvije globalne varijable koje sadrže ime datoteke (filename) i broj linije izvornog programa (lineno). U dojavi greške se uvijek prije izvještaja o tipu greške ispisuje to ime i broj linije. #include <stdio.h> 368
  • 369.
    #include <stdarg.h> extern char *filename; /* current input file name */ extern int lineno; /* current input line number */ void error(char *msg,...) { va_list argp; va_start(argp, msg); fprintf(stderr, "%s:, line %d: ",filename, lineno); vfprintf(stderr, msg, argp); fprintf(stderr, "n"); va_end(argp); } Manipuliranje s datotekama rename int rename(const char *origname, const char *newname); Funkcija rename() vrši promjenu imena datoteke origname, u ime newname. Ako je operacija uspješna funkcija vraća 0, inaće vraća nenultu vrijednost. remove int remove(const char *name); Funkcija remove() briše datoteku imena name. Ako je operacija uspješna funkcija vraća 0, inaće vraća nenultu vrijednost. tmpfile, tmpnam FILE *tmpfile(void); char *tmpnam(char *buf); Funkcija tmpfile() stvara privremenu datoteku i otvara je u "wb+" modu. Po izlasku iz programa ova se datoteka automatski briše. Funkcija tmpnam() generira jedinstveno ime u string buf, koji mora biti duljine L_tmpnam (predefinirana konstanta). To se ime može koristiti za stvaranje datoteke. Ako je buf==0, ime se generira u internom statičkom spremniku. Funkcija vraća pokazivač na taj spremnik. C 2 <string.h> Funkcije koje su deklarirane u <string.h> uglavnom služe za rad s ASCIIZ stringovima. Pored njih, definirano je nekoliko funkcija, čije ime počinje s mem, za rad s memorijskim blokovima (nizovima bajta). Rad sa stringovima size_t strlen(const char *s) Vraća duljinu stringa s. char *strcpy(char *s, const char *t) Kopira string t u string s, uključujući '0'; vraća s. 369
  • 370.
    char *strncpy(char *s,const char *t, size_t n) Kopira najviše n znakova stringa t u s; vraća s. Dopunja string s sa znakovima '0', ako t ima manje od n znakova. Napomena: ako u stringu t ima n ili više znakova, tada string s neće biti zaključen s '0'. char *strcat(char *s, const char *t) Dodaje string t na kraj stringa s; vraća s. char *strncat(char *s, const char *t, size_t n) Dodaje najviše n znakova stringa t na string s, i znak '0'; vraća s. int strcmp(const char *s, const char *t) Uspoređuje string s sa stringom t, vraća <0 ako je s<t, 0 ako je s==t, ili >0 ako je s>t. Usporedba je leksikografska, prema ASCII rasporedu. int strncmp(const char *s, const char *t, size_t n) Uspoređuje najviše n znakova stringa s sa stringom t; vraća <0 ako je s<t, 0 ako je s==t, ili >0 ako je s>t. int strcoll(const char *s, const char *t); Uspoređuje dva stringa s1 and s2, poput strcmp(), ali se usporedba vrši prema multinacionalnom znakovnom rasporedu (koji je određen konstantom LC_COLLATE ). Vraća <0 ako je s<t, 0 ako je s==t, ili >0 ako je s>t. size_t strxfrm(char *s, const char *t, size_t n); Stvara modificiranu kopiju n znakova stringa t u stringu s (uključujući '0') , tako da strcmp(s,t) daje istu ocjenu kao i strcoll(s,n) na originalnom stringu. Vraća broj znakova u stringu s. char *strchr(const char *s, int c) Vraća pokazivač na prvu pojavnost znaka c u stringu s, ili NULL znak ako c nije sadržan u stringu s. char *strrchr(const char *s, int c) Vraća pokazivač na zadnju pojavnost znaka c u stringu s, ili NULL znak ako c nije sadržan u stringu s. char *strstr(const char *s, const char *t) Vraća pokazivač na prvu pojavu stringa t u stringu s, ili NULL ako string s ne sadrži string t. size_t strspn(const char *s, const char *t) Vraća duljinu prefiksa stringa s koji sadrži znakove koji čine string t. size_t strcspn(const char *s, const char *t) Vraća duljinu prefiksa stringa s koji sadrži znakove koji nisu prisutni u stringu t. 370
  • 371.
    char *strpbrk(const char*s, const char *t) Vraća pokazivač na prvu pojavu bilo kojeg znaka iz string t u stringu s, ili NULL ako nije prisutan ni jedan znak iz string t u stringu s. char *strerror(int n) Vraća pokazivač na string koji se interno generira, a služi za dojavu greške u nekim sistemskim operacijama. Argument je obično globalna varijabla errno, čiju vrijednost određuje izvršenje funkcija iz standardne biblioteke. char *strtok(char *s, const char *sep) strtok() je funkcija kojom se može izvršiti razlaganje stringa na niz leksema koji su razdvojeni znakovima-separatorima. Skup znakova-separatora se zadaje u stringu sep. Funkcija vraća pokazivač na leksem ili NULL ako nema leksema. Korištenje funkcije strtok() je specifično jer u stringu može biti više leksema, a ona vraća pokazivač na jedan leksem. Da bi se dobili slijedeći leksemi treba iznova pozvati ovu funkciju, ali s prvim argumentom jednakim NULL. Primjerice, za string char *s = "Prvi drugi,treci"; ako odaberemo znakove separatore: razmak, tab i zarez, tada sljedeći iskazi daju ispis tri leksema (Prvi drugi i treci): char *leksem = strtoken(s, " ,t"); /* dobavi prvi leksem */ while( leksem != NULL) { /* ukoliko postoji */ printf("", leksem); /* ispiši ga i */ lexem = strtok(NULL," ,t"); /* dobavi sljedeći leksem */ } /* pa ponovi postupak */ Operacije s memorijskim blokovima (nizovima) memcpy, memmove void *memcpy(void *dest, const void *src, size_t n); void *memmove(void *dest, const void *src, size_t n); Ove funkcije kopiraju točno n bajta s lokacije na koju pokazuje src na lokaciju koju pokazuje dest. Ukoliko se blokovi preklapaju tada treba koristiti funkciju memmove(). Funkcije vraćaju pokazivač dest. memcmp int memcmp(const void *p1, const void *p2, size_t n); Uspoređuje točno n znakova s lokacija na koje pokazuju p1 i p2, na isti način kao strcnmp(), ali se usporedba ne prekida ako je dostignut znak '0'. memchr void *memchr(const void *p, int c, size_t n); 371
  • 372.
    Traži prvu pojavuznaka c u n znakova bloka na koji pokazuje p. Vraća pokazivač na pronađeni znak ili NULL ako znak nije pronađen. memset void *memset(void *p, int c, size_t n); Postavlja n bajta bloka na koji pokazuje p na vrijednost znaka c, i vraća p. C 3 <ctype.h> Funkcije iz <ctype.h> omogućuju klasifikaciju znakova te pretvorbu velikih u mala slova i obratno. Klasifikacija znakova int isupper(int c); vraća vrijednost različitu od nule ako je znak c veliko slovo, inače vraća 0. int islower(int c); vraća vrijednost različitu od nule ako je znak c malo slovo, inače vraća 0. int isalpha(int c); vraća vrijednost različitu od nule ako je znak c veliko ili malo slovo, inače vraća 0. int iscntrl(int c); vraća vrijednost različitu od nule ako je znak c kontrolni znak, inače vraća 0. int isalnum(int c); vraća vrijednost različitu od nule ako je znak c slovo ili znamenka, inače vraća 0. int isdigit(int c); vraća vrijednost različitu od nule ako je znak c decimalna znamenka, inače vraća 0. int isxdigit(int c); vraća vrijednost različitu od nule ako je znak c heksadecimalna znamanka, inače vraća 0. int isgraph(int c); vraća vrijednost različitu od nule ako je znak c tiskani znak osim razmaka, inače vraća 0. int isprint(int c); vraća vrijednost različitu od nule ako je znak c tiskani znak uključujući razmak, inače vraća 0. int ispunct(int c); vraća vrijednost različitu od nule ako je znak c tiskani znak osim razmaka, slova ili znamanke, inače vraća 0. 372
  • 373.
    int isspace(int c); vraćavrijednost različitu od nule ako je znak c razmak, tab, vert. tab, nova linija, povrat ili nova stranica, inače vraća 0. Pretvorba znaka int toupper(int c); int tolower(int c); Funkcija toupper() pretvara malo slovo u ekvivalentno veliko slovo, ostala slova ostaju nepromijenjena. Slično, tolower() pretvara veliko slovo u ekvivalentno malo slovo. C 4 <stdlib.h> U zaglavlju <stdlib.h> definirano je nekoliko temeljnih funkcija za alokaciju memorije, pretvorbu stringa u brojeve, manipuliranje s multibajtnim znakovnim skupom, itd. Alokacija memorije malloc, calloc void *malloc(size_t n); void *calloc(size_t n, size_t elsize); Funkcija malloc() alocira se n bajta slobodne memorije. Ako je alociranje uspješno funkcija vraća pokazivač na tu memoriju, u suprotnom vraća NULL pokazivač. Primjerice, naredbom double *dp = malloc(10 * sizeof(double)); dobije se pokazivač dp, koji pokazuje na niz od 10 elemenata tipa double. Funkcija calloc(n, elsize) je ekvivalentna malloc(n * elsize), uz dodatni uvjet da calloc() inicijalizira sve bitove alocirane memorije na vrijednost nula. free void free(void *p); Funkcija free() prima kao argument pokazivač p. Uz pretpostavku da p pokazuje na memoriju koja je prethodno alocirana funkcijom malloc(), calloc() ili realloc(), ova funkcija dealocira tu memoriju. realloc void *realloc(void *oldptr, size_t newsize); Funkcija realloc() vrši promjenu veličine prethodno alocirane memorije, koja je pridijeljena pokazivaču ptr, na veličinu newsize. Funkcija realloc() vraća pokazivač na tu memoriju. Vrijednost toga pokazivača može biti ista kao i vrijednost od ptr, ako memorijski alokator može prilagoditi veličinu zahtijevanog području slobodne memorije veličini newsize. Ukoliko se to ne može ostvariti funkcija realloc() alocira novo područje memorije pa u njega kopira i zatim oslobađa dio memorije na koju pokazuje ptr. Ukoliko se ne može izvršiti alokacija memorije funkcija realloc() vraća NULL. Napomena: poziv realloc(p, 0) je ekvivalentan pozivu free(p), a poziv realloc(0, n) je ekvivalentan pozivu malloc(n). 373
  • 374.
    Pretvorba stringa unumeričku vrijednost atoi, atol, strtol, strtoul int atoi(const char *s); long int atol(const char *s); long int strtol(const char *s, char **endp, int baza); unsigned long int strtoul(const char *s, char **endp, int baza); Ove funkcije pretvaraju numerički string s u odgovarajuću numeričku vrijednost. Funkcija strtol() u stringu s preskače bijele znakove, pretvara s u broj i vraća vrijednost tipa long int. Ona omogućuje pretvorbu iz sustava različite baze. Ako je baza 10 tada se iz ulaznog stringa s prihvaćaju znakovi od 0 do 10, a ako je baza 16 prihvaćaju se i znakovi a-f, A- F. Ako je baza manja od 10 prihvaćaju se znakovi od 0 do baza-1. Ako je baza 0, tada se koristi pravilo da oktalni brojevi počinju s nulom, a heksadecimalni s 0x ili 0X. atoi(s) je ekvivalntno strtol(s,NULL,0); Argument endp ja pokazivač na pokazivač na char. Njega se koristi za tako da ako endp != NULL, tada strtol() sprema u *endp pokazivač na prvi znak koji nije obrađen. Ako je to znak '0', pretvorba je uspješna. Ako je jednak s, pretvorba uopće nije izvršena. Funkcija vraća vrijednost koja je pretvoreni broj ili 0 ako pretvorba nije izvršena ili konstanta LONG_MAX ili LONG_MIN ako se broj ne može predstaviti kao long int. U slučaju prekoračenja postavlja se errno = ERANGE. Funkcija strtoul() je slična strtol() osim što vraća tip unsigned long int, ili vrijednost ULONG_MAX kod prekoračenja. Poziv atol(s) je ekvivalentan pozivu strtoul(s,NULL,0). atof, strtod double atof(const char *s); double strtod(const char *s, char **endp); Ove funkcije pretvaraju numerički string s u odgovarajuću numeričku vrijednost realnog broja. Funkcija strtod() u stringu s preskače bijele znakove, pretvara s u broj i vraća vrijednost tipa double. Prihvaća prosti i eksponentni zapis realnog broja. Argument endp ja pokazivač na pokazivač na char. Njega se koristi za tako da ako endp != NULL, tada strtod() sprema u *endp pokazivač na prvi znak koji nije obrađen. Ako je to znak '0', pretvorba je uspješna. U slučaju prekoračenja vraća konstantu HUGE_VAL, i postavlja globalnu varijablu errno = ERANGE. Napomena: atof(s) je ekvivalentno strtod(s, NULL), osim što rezultat nije definiran u slučaju prekoračenja. Generator slučajnih brojeva rand int rand(void); Funkcija rand() vraća slučajni cijeli broj iz intervala 0 do RAND_MAX ( RAND_MAX je konstanta definirana u <stdlib.h>). srand 374
  • 375.
    void srand(unsigned intseed); Funkcija srand() postavlja početnu vrijednost seed generatora slučajnih brojeva. Sortiranje i traženje qsort void qsort(void *a, size_t n, size_t elsize, int (*cmpfunc)()); Funkcija qsort() sortira niz a, koji ima n elemenata veličine elsize (u bajtima), prema kriteriju koji je određen funkcijom na koju pokazuje cmpfunc. Ta funkcija mora biti deklarirana u obliku: int name(const void *p1, const void *p2); i mora vratiti cijeli broj koji je manji, veći ili jedanak nuli, ovosno o tome da li je objekt na kojeg pokazuje p1 manji, veći, ili jednak objektu na kojeg pokazuje p2. bsearch bsearch(const void *pObj, const void *a, size_t n, size_t elsize, int (*cmpfunc)()); Funkcija bsearch() vrši binarno traženje u sortiranom nizu a, koji ima n elemenata veličine elsize (u bajtima), tražeći element koji je jednak objektu na kojeg pokazuje pObj. Pri traženju se za usporedbu koristi funkcija na koju pokazuje cmpfunc. Deklaracija te funkcije je ista kao kod qsort(). Interakcija s operativnim sustavom getenv char *getenv(const char *name); "Environment" sadrži postavke operativnog sustava u sistemskim varijablama (pr. path). Funkcija getenv() traži "environment varijablu" imena name i vraća njenu vrijednost u obliku stringa. Ako ne može pronaći tu varijablu, tada vraća NULL. atexit int atexit(void (*func)(void)); Funkcija atexit() prima kao argument pokazivač na funkciju func, koja će biti pozvana pri normalnim završetku programa. Vraća 0 ako je funkcija uspješno registrirana. Može se registrirati do 32 funkcije, koje će biti pozvane u obrnutom redoslijedu od reda registriranja. exit void exit(int status); Funkcija exit() vrši normalni završetak programa, poziva sve funkcije koje su registrirane s atexit() funkcijom i zatvara sve tokove. Argument status se prosljeđuje operativnom sustavu. Mogu se koristiti i dvije simboličke konstante EXIT_SUCCESS i EXIT_FAILURE. Po dogovoru, status=0 (EXIT_SUCCESS) znači uspješan završetak programa. 375
  • 376.
    system int system(const char *s); Funkcija system() prima kao argument string koji sadrži komandu operativnog sustava. Funkcija vraća vrijednost koju vrati operativni sustav po završetku procesa. abort void abort(void); Funkcija abort() predstavlja zahtjev za neposrednim prekidom programa, na isti način kao da je izvršen poziv raise(SIGABRT). Cjelobrojne aritmetičke funkcije abs, labs int abs(int x); long int abs(long int x); Obje funkcije vraćaju apsolutnu vrijednost argumenta x.. div, ldiv div_t div(int num, int denom); ldiv_t div(long int num, long int denom); Ove funkcije vrše dijeljenje num/denum na način da se istovremeno dobije rezultat dijeljenja i ostatak cjelobrojnog dijeljenja. Rezultat se vraća u vrijednost koja je tipa sljedeće strukture: typedef struct { int quot; /* rezultat dijeljenja */ int rem; /* ostatak dijeljenja */ }div_t; Multibajt znakovne sekvence i stringovi Za zapis znakova po ASCII standardu koristi se tip char. Za zapis znakova se također može koristiti prošireni znakovni tip wchar_t, koji podržava 16-bitni Unicode standard, i multibajt znakovne sekvence MBCS (za jezike poput japanskog). mblen int mblen(const char *s, size_t n); Funkcija mblen() vraća broj znakova u stringu koji sadrži multibajt znakovne sekvence. Analizira se maksimalno n znakova. mbtowc, wctomb int mbtowc(wchar_t dest, const char *src, size_t n); int wctomb(char *dest, wchar_t src); 376
  • 377.
    Ove funkcije pretvarajumultibajt znakovnu sekvencu u tip wchar_t i obrnuto. Funkcija mbtowc() analizira maksimalno n bajta iz stringa src i pretvara ih u wchar_t i sprema u dest, te vraća broj bajta ili -1 ako pretvorba nije uspješna. Funkcija wctomb() pretvara prošireni znak src u multibajt sekvencu dest, i vraća broj bajta u toj sekvenci.. (Ovaj broj neće nikada biti veći od konstante MB_CUR_MAX, definirane u <stdlib.h>.) mbstowcs, wcstombs size_t mbstowcs(wchar_t *dest, const char *src, size_t n); size_t wcstombs(char *dest, wchar_t src); Ove funkcije vrše pretvorbu višestruke multibajt sequence i niza proširenih znakova. Funkcija mbtowcs() pretvara multibajt sekvencu src i u niz proširenih znakova i sprema u dest, ali maksimalno n znakova. Vraća broj pretvorenih znakova. Funkcija wcstombs() vrši obrnutu radnju. C 5 <math.h> Standardne matematičke funkcije su deklarirane u zaglavlju <math.h>. Njihov opis je dan sljedećoj tablici: Funkcija Vraća vrijednost double sin(double x); sin(x) , sinus kuta x u radijanima double cos(double x); cos(x) , kosinus kuta x u radijanima double tan(double x); tg(x) , tangens kuta x u radijanima double asin(double x); arcsin(x), vraća vrijednost [-π/2, π/2], za x ∈ [-1,1]. double acos(double x); arccos(x), vraća vrijednost [0, π], za x ∈ [-1,1]. double atan(double x); arctg(x), vraća vrijednost [-π/2, π/2]. double atan2(double y, arctan(y / x), vraća vrijednost [-π,π]. double x); double sinh(double x); sh(x), sinus hiperbolni kuta x double cosh(double x); ch(x) , kosinus hiperbolni kuta x double tanh(double x); th(x) , tangens hiperbolni kuta x double exp(double x); ex , x potencija broja e = 2,781 double log(double x); prirodni logaritam ln(x), x>0. double log10(double x); logaritam baze 10, log10(x), x>0. double pow(double x, xy, potenciranje x s eksponentom y. Nastaje greška ako je double y); x=0 i y <= 0, ili ako je x<0, a y nije cijeli broj. double sqrt(double x); √x, za x>=0, dugi korijen pozitivnog broja double ceil(double x); najmanji cijeli broj (tipa double) koji nije manji od x double floor(double x); najveći cijeli broj (tipa double) koji nije veći od x double fabs(double x); |x|, apsolutna vrijednost od x double ldexp(double x,int n); x*2n double frexp(double x, rastavlja x na frakcioni dio i eksponent broja 2. Vraća int *exp); normaliziranu frakciju od x (iz intervala [1/2,1)] i exponent od 2 u *exp. Ako je x jednak nuli, oba dijela su jednaka nuli. Primjerice, frexp(2.5, &i) vraća 0.625 (odnosno 0.101 baze 2) i postavlja i na 2, tako da ldexp(0.625, 2) ponovo daje vrijednost 2.5. double modf(double x, rastavlja x na integralni i frakcioni dio, tako da oba imaju double *ip); predznak od x. Vraća frakcioni dio, a integralni dio smješta u *ip. double fmod(double x, ostatak dijeljenja realnih brojeva x/y, s predznakom od x. double y); 377
  • 378.
    C 6 <time.h> Uzaglavlju <time.h> definirane su funkcije i strukture za rad s datumima i vremenom. Posebni typedef tip time_t, služi za bilježenje vremena. Obično se uzima da ovaj tip predstavlja broj sekundi počevši od 01.01.1970. godine. Za detaljniji zapis vremena i datuma koristi se struktura tm, koja je definirana na sljedeći način: struct tm /* opisuje vrijeme i datum */ { int tm_sec; /* sekunde 0..61 */ int tm_min; /* minute 0..59 */ int tm_hour; /* sat 0..23 */ int tm_mday; /* dan 1..31 */ int tm_mon; /* mjesec 0..11 */ int tm_year; /* broj godina nakon 1900 */ int tm_wday; /* dan u sedmici 0..6 */ int tm_yday; /* dan u godini 0..365 */ int tm_isdst; /* da li je dan promjene sata 0..1 */ }; Napomena: ako dan promjene sata nije implementiran, tada tm_isdst ima negativnu vrijednost. Broj sekundi može biti veći od 59 u slučaju prestupnog vremena. Mjeseci su kodiranu tako da 0 označava siječanj, 1 veljaču itd. Dani u sedmici su kodirani tako da 0 označava nedjelju, 1 ponedjeljak itd. Stvarna godina se dobije tako da se članu tm_year doda vrijednost 1900 (primjerice u godini 2002. godini član tm_year sadrži vrijednost 102). time time_t time(time_t *tp); Funkcija time() vraća time_t vrijednost – kardinalni broj koji predstavlja trenutno vrijeme (obično je to broj sekundi od 1.1.1970.). Parametar tp, ako nije NULL, također prihvaća trenutno vrijeme u *tp. localtime, gmtime struct tm *localtime(const time_t *t); struct tm *gmtime(const time_t *t); Pretvorbu vremena iz formata time_t u struct tm vrši se funkcijom localtime(), kada se želi dobiti lokalno vrijeme, ili funkcijom gmtime() za univerzalno vrijeme u nultom meridijanu. Obje funkcije primaju adresu varijable koja sadrži vrijeme u formatu time_t, a vraćaju pokazivač na statičku strukturu tipa tm (sadržaj se obnavlja pri svakom pozivu ovih funkcija) . ctime, asctime Ako se želi dobiti zapis vremena u obliku stringa, mogu se koristiti funkcije char *ctime(const time_t *t); char *asctime(const struct tm *tp); Funkcija ctime() za argument koristi adresu varijable koja sadrži vrijeme u formatu time_t, a funkcija asctime() za argument koristi pokazivač na strukturu tm. Obje funkcije vraćaju 378
  • 379.
    pokazivač statičkog stringakoji sadrži zapis vremena u standardnom formatu. Primjerice, sekvenca naredbi time_t t = time(NULL); char *s = ctime(&t); puts(s); generira ispis: Sat May 11 14:21:20 2002 Uočite da je rezultat poziva ctime(&t) ekvivalentan pozivu asctime(localtime(&t)) . Standardna verzija je prilagođena američkim standardima. Ako se želi napisati vrijeme u formatu 11.05.2002 14:21 tada se može koristiti sljedeće iskaze: /* ispisuje datum i vrijeme u formatu 11.05.2002 14:21 */ time_t t = time(NULL); struct tm *p = localtime(&t); printf("%.2d.%.2d.%.2d %2d:%.2dn", p->tm_mday, p->tm_mon + 1, p->tm_year +1900, p->tm_hour, p->tm_min); strftime size_t strftime(char *buf, size_t bufsize, const char *fmt, const struct tm *tp); Funkcija strftime() se koristi za formatirani ispis vremena. Format se zadaje kao kod printf() funkcije. Prvi argument je string str u koji se vrši formatirani zapis. Drugi argument (bufsize) ograničava broj znakova stringa. Treći parametar je string u kojem se zapisuje format ispisa nizom specifikatora oblika %x (kao kod printf() funkcije). Posljednji argument je pokazivač strukture tm. Funkcija vraća broj znakova u stringu ili 0 ako nije moguće generirati formatirani string. Specifikatori formata su: %a kratica od tri slova za ime dana u sedmici (eng. Sun, Mon, Tue,..) %A puno ime dana u sedmici (eng.) %b kratica od tri slova za ime mjeseca (eng. Jan, Feb, Mar,...) %B puno ime mjeseca (eng.) %c kompletni zapis vremena i datuma %d dan u mjesecu (1..31) %H sat u formatu (1..24) %I sat u formatu (1..12) %j dan u godini (1..365) %m mjesec u godini (1..12) %M minute %p AM/PM (eng.) string koji označava jutro ili popodne %S sekunde %U broj za sedmicu u godini (1..52) - 1 određen prvom nedjeljom %w broj za dan u sedmici (0-nedjelja) %W broj za sedmicu u godini (1..52) - 1 određen prvim ponedjeljkom 379
  • 380.
    %x kompletni zapis datuma %X kompletni zapis vremena %y zadnje dvije znamenke godine %Y godina u 4-znamenkastom formatu %Z ime vremenske zone (ako postoji ) %% znak % mktime time_t mktime(struct tm *tp); Funkcija mktime() pretvara zapisa iz strukture tm u time_t format. Korisna je u tzv. kalendarskim proračunima. Kada je potrebno dodati nekom datumu n dana, tada se može upisati datum u tm strukturu, povećati član tm_mday za n, zatim pozivom mktime() se dobije time_t vrijednost koja odgovara novom datumu. difftime double difftime(time_t t1, time_t t2); Funkcija difftime() vraća realnu vrijednost koja je jednaka razlici vremena t1 i t1 u sekundama. clock clock_t clock(void); Funkcija clock() služi za preciznije mjerenje vremena nego je to moguće sa prethodnim funkcijama. Ona vraća vrijednost procesorskog mjerača vremena, koji starta na početku programa, u jedinicama koje su znatno manje od sekunde (nekoliko milisekundi). Koliko je tih jedinica u jednoj sekundi određeno je konstantom CLOCKS_PER_SEC. To znači da izraz: (double)clock()/CLOCKS_PER_SEC daje vrijednost koja je jednaka vremenu (u sekundama) od pokretanja programa. C 7 <signal.h> U zaglavlju <signal.h> deklarirane su dvije funkcije (signal() i raise()) za prihvat i generiranje asinkronih prekida programa ili "signala". Za identificiranje nekoliko mogućih signala, u ovom zaglavlju su definirane simboličke cjelobrojne konstante sa sljedećim imenima: SIGABRT Signal kojeg generira funkcija abort(). SIGFPE Signal koji se generira kad nastane greška kod matematičkih operacija primjerice pri dijeljenju s nulom. SIGILL Signal koji se generira ako se pokušava izvršiti nepostojeća ili nedozvoljena instrukcija procesora. SIGINT Signal koji se generira s tipkovnice (primjerice, Ctrl-C tipkom). SIGSEGV Signal koji se generira ako se pristupa zaštićenoj ili nepostojećoj memoriji ("segmentation violations"). SIGTERM Signal koji se generira kada je proces prekinut nekim vanjskim događajem. Ovisno o implementaciji kompilatora, moguće su i dodatne definicije identifikatora signala. 380
  • 381.
    raise int raise(int sig); Funkcija raise() šalje izvršnom programu signal sig. signal void (*signal(int sig, void (*func)(int)))(int); Funkcija signal() se koristi za definiranje akcije koja se treba izvršiti kada se pojavi neki signal. Ukoliko nije definirana radnja koja se vrši nakon pojave signala, prekida se program. Argument sig je signal kojeg treba prihvatiti (jedna od SIGxxx konstanti). Argument func je ili konstanta SIG_IGN (kojom se zahtijeva ignoriranje signala) ili konstanta SIG_DFL (kojom se postavlja predodređeni postupak prihvata signala) ili pokazivač na korisnički definiranu funkciju koja će se izvršiti pojavom signala. Ta funkcija mora imati prototip oblika void signalhandler(int sig); Argument ove funkcije tipa int je broj signala koji se prihvaća. Funkcija signal() vraća prethodni oblik prihvata signala; SIG_DFL, SIG_IGN, ili pokazivač na funkciju. Zbog navedenih svojstava, deklaracija funkcije signal() je kompleksna. To je funkcija koja vraća pokazivač na funkciju koja prima jedan argument tipa int i vraća void. Prvi argument je tipa int, a drugi argument je pokazivač na funkciju koja prima jedan argument tipa int i vraća void. Primjer: u sljedećem programskom odsječku pokazano je kako se postavlja poziv funkcije exithandler() u slučaju pojave prekida (interrupt signal - SIGINT), ali samo ako taj signal nije prethodno ignoriran: extern void exithandler(int); if(signal(SIGINT, SIG_IGN) != SIG_IGN) signal(SIGINT, exithandler); C 8 <setjmp.h> U zaglavlju <setjmp.h> deklarirane su funkcije setjmp() i longjmp(), pomoću kojih se može izvršiti skok u program i izvan funkcije. Mjesto u programu, na koju se vrši skok, označava se funkcijom setjmp(), koja pamti stanje stoga, registre procesora i trenutnu adresu programa u objektu tipa jmp_buf. Kasnije se s bilo kojeg mjesta u programu može skočiti na ovu poziciju pozivom funkcije longjmp(). setjmp int setjmp(jmp_buf context); Funkcija setjmp() sprema trenutnu programsku adresu i stanje procesora u objekt context, koji je tipa jmp_buf, i vraća vrijednost 0. Kasnije, nakon poziva funkcije longjmp(), povratna vrijednost se može promijeniti. longjmp 381
  • 382.
    void longjmp(jmp_buf context,int retval) Funkcija longjmp() vrši skok na stanje opisano u objektu context, koji je prethodno spremljen pozivom funkcija setjmp(). Skok se vrši na mjesto gdje je prethodno pozvana funkcija setjmp(), pa se sada vrši povrat iz funkcije setjmp()s vrijednošću retval. C 9 <locale.h> U zaglavlju <locale.h> deklarirane su dvije funkcije za lokalizirane postavke. Početno, program počinje u "C" lokalizaciji, koja se zatim može promijeniti sa setlocale() funkcijom. Lokalno-specifične informacije se dijele u nekoliko kategorija, koje se označavaju sljedećim konstantama: LC_COLLATE Usporedba stringova se vrši pomoću funkcija strcoll() i strxfrm() LC_CTYPE Klasiofikacija znakova pomoću funkcija iz <ctype.h> LC_MONETARY Monetarne postavke se dobiju funkcijom localeconv() LC_NUMERIC Koristi decimalnu točku u funkcijama printf(), scanf(), strtod(), itd. LC_TIME Lokalizirani format strftime() funkcije LC_ALL Unija prethodnih postavki setlocale char *setlocale(int cat, const char *locale); Funkcija setlocale() ima dva argumenta. Prvi argument je oznaka kategorije koja se postavlja, a drugi parametar locale je string za oznaku lokalizacije. Ako taj string je jednak "C", tada se koristi predodređena lokalizacija. Prazni string "" također označava predodređenu lokalizaciju. Sve ostale oznake su specifične za pojedinu implementaciju kompilatora. Funkcija vraća pokazivač na string koji sadrži prethodnu locale postavku. Ako se setlocale() pozove s locale == NULL tada funkcija vraća trenutnu postavku. localeconv struct lconv *localeconv(void); Funkcija localeconv() vraća pokazivač na strukturu lconv koja sadrži lokalno-specifične informacije. Ta struktura je otprilike definirana ovako: struct lconv { char *decimal_point; char *thousands_sep; char *grouping; char *int_curr_symbol; char *currency_symbol; char *mon_decimal_point; char *mon_thousands_sep; char *mon_grouping; char *positive_sign, *negative_sign; char int_frac_digits; char frac_digits; char p_cs_precedes, p_sep_by_space; char n_cs_precedes, n_sep_by_space; char p_sign_posn, n_sign_posn; 382
  • 383.
    }; decimal_point je oznaka koja se koristi za decimalni zarez. thousands_sep je separator koji se koristi između grupe znamenki grouping je string koji definira veličinu grupe (primjerice "3" označava da se ponavlje grupa od 3 znaka ). Ostali članovi opisuju monetarno-specifične informacije. Ukratko, int_curr_symbol i currency_symbol su verzije (internacionalne i lokalne) za lokalnu valutu, mon_decimal_point je decimalna točka, mon_thousands_sep i mon_grouping dopisuju grupiranje znamenki (analogno s thousands_sep i grouping), positive_sign i negative_sign su znakovi pozitivnog i negativnog predznaka, int_frac_digits i frac_digits opisuju broj decimalnih znamenki koje se prikazuju. Ostali članovi opisuju oznaku valute i indikatore predznaka. C 10 <stdarg.h> U zaglavlju <stdarg.h> su definirani makro naredbe pomoću kojih se omogućuje definiranje funkcija s promjenjljivim brojem parametara. Koristi se ideja da se argumentima neke funkcije pridijeli lista pokazivača koja ima apstraktni tip va_list. U tu svrhu koristi se makro va_start. Zatim se iz ove liste mogu dobiti svi argumenti pomoću makroa va_arg. Na kraju rada, unutar iste funkcije, treba pozvati makro va_end. va_start va_start(va_list argp, Lastarg); va_start inicijalizira argp tako da se njime mogu dohvatiti argumenti. Lastarg je ime posljednjeg fiksnog argumenta funkcije. va_arg argtype va_arg(va_list argp, argtype); va_arg dobavlja vrijednost sljedećeg argumenta koji je tipa argtype. Tip argtype se specificira na isti način kako se definira argument sizeof operatora. Tip mora odgovarati tipu sljedećeg argumenta. va_end va_end(va_list argp); va_end označava da je završen pristup promjenjljivoj listi argumenata. Primjer: u sljedećem programu definirana je funkcija miniprintf(), kojom je pokazano kako je implementirana printf() funkcija. #include <stdio.h> #include <stdarg.h> void miniprintf(const char *format, ...) { va_list argp; 383
  • 384.
    const char *p; char tmpbuf[25]; int i; va_start(argp, format); for(p = format; *p != '0'; p++) { if(*p != '%') { putchar(*p); continue; } switch(*++p) { case 'c': i = va_arg(argp, int); putchar(i); break; case 'd': i = va_arg(argp, int); sprintf(tmpbuf, "%d", i); fputs(tmpbuf, stdout); break; case 'o': i = va_arg(argp, int); sprintf(tmpbuf, "%o", i); fputs(tmpbuf, stdout); break; case 's': fputs(va_arg(argp, char *), stdout); break; case 'x': i = va_arg(argp, int); sprintf(tmpbuf, "%x", i); fputs(tmpbuf, stdout); break; case '%': putchar('%'); break; } } va_end(argp); } C 11 <stddef.h> U zaglavlju <stddef.h> definirano je nekoliko tipova i makro naredbi. NULL Makro koji označava konstantu za nul pokazivač (vrijednost mu je 0 ili (void *)0). size_t Cjelobrojni unsigned tip koji se koristi za označavanje veličine memorijskog objekta. ptrdiff_t Cjelobrojni tip koji označava vrijednosti koji nastaju kod oduzimanja pokazivača. wchar_t Tip “wide character” koji može imati znatno veći interval vrijednosti od tipa char. Koristi se za multinacionalni skup znakova (Unicode). offsetof() Makro kojim se računa pomak (eng. offset) u bajtima nekog elementa strukture, primjerice offsetof(struct tm, tm_year). Korištenje ovih tipova osigurava prenosivost programa. C 12 <assert.h> U zaglavlju <assert.h> definar je makro assert, koji omogućuje testiranje pograma. 384
  • 385.
    void assert(int test_izraz) Akoje test_izraz jednak nuli, tada assert(test_izraz) šalje na stdderr poruku, poput ove: Assertion failed: test_izraz, file filename, line nnn i vrši poziv funkcije abort(), čime se prekida izvršenje programa. Ime izvorne datoteke (filename) i broj linije u kojoj je assert, su dobijeni pretprocesorskih makroa: __FILE__ i __LINE__. Ako se pri kompiliranju definira makro NDEBUG (s bilo kojom vrijednošću) tada se ignorira makro assert. C 13 <errno.h> U zaglavlju <errno.h> deklarirana je globalna varijable errno u kojoj se bilježi kôd greške koja nastaje pri korištenju funkcija standardne biblioteke. Također, definirane su simboličke konstante EDOM and ERANGE koje označavaju kôd pogreške kod matematičkih operacija. C 14 <limits.h> U zaglavlju <limits.h> definirane su simboličke konstante koje označavaju interval standardnih tipova. To su: CHAR_BIT broj bitova u tipu char CHAR_MAX maksimalna vrijednost char tipa CHAR_MIN maksimalna vrijednost char tipa INT_MAX maksimalna vrijednost int tipa INT_MIN minimalna vrijednost int tipa LONG_MAX maksimalna vrijednost long tipa LONG_MIN minimalna vrijednost long tipa SCHAR_MAX maksimalna vrijednost signed char tipa SCHAR_MIN minimalna vrijednost signed char tipa SHRT_MAX maksimalna vrijednost short tipa SHRT_MIN minimalna vrijednost short tipa UCHAR_MAX maksimalna vrijednost unsigned char tipa UINT_MAX maksimalna vrijednost unsigned int tipa ULONG_MAX maksimalna vrijednost unsigned long tipa USHRT_MAX maksimalna vrijednost unsigned short tipa MB_LEN_MAX broj bajta u multibajt znakovnoj sekvenci C 15 <float.h> U zaglavlju <float.h> definirane su simboličke konstante koje označavaju implementaciju realnih brojeva s pomičnom točkom. To su: FLT_RADIX FLT_ROUNDS FLT_MANT_DIG DBL_MANT_DIG LDBL_MANT_DIG FLT_DIG DBL_DIG LDBL_DIG FLT_MIN_EXP DBL_MIN_EXP LDBL_MIN_EXP FLT_MIN_10_EXP DBL_MIN_10_EXP LDBL_MIN_10_EXP FLT_MAX_EXP DBL_MAX_EXP LDBL_MAX_EXP 385
  • 386.
    FLT_MAX_10_EXP DBL_MAX_10_EXP LDBL_MAX_10_EXP FLT_MAX DBL_MAX LDBL_MAX FLT_EPSILON DBL_EPSILON LDBL_EPSILON FLT_MIN DBL_MIN LDBL_MIN FLT_RADIX je baza “floating-point” modela (pr. 2, 16). FLT_ROUNDS je konstanta koja pokazuje kako se zaokružuje rezultat pri zbrajanju: 0 ako je prema 0, 1 ako je prema najbližoj vrijednosti, 2 ako je prema +∞, 3 ako je –∞, i -1 znači da nije definirano. Ostali makroi daju svojstva tipova: float (FLT_), double (DBL_), i long double (LDBL_). MANT_DIG je broj znamenki (baze FLT_RADIX) u mantisi. DIG daje približnu preciznost u ekvivalentnoj bazi 10. MIN_EXP i MAX_EXP daju maksimalni i minimalni eksponent (MIN_10_EXP i MAX_10_EXP daju njihov ekvivalent u bazi 10). MIN i MAX daju minimalnu i maksimalnu vrijednost realnog broja. EPSILON je razlika između 1.0 i sljedećeg većeg broja. C 16 <iso646.h> U zaglavlju <iso646.h> definirani su makroi za zamjenu operatora koji možda nisu implementirani na nekom mikro računalima. To se sljedeće definicije: #define and && #define and_eq &= #define bitand & #define bitor | #define compl ~ #define not ! #define not_eq != #define or || #define or_eq |= #define xor ^ #define xor_eq ^= C 17 <wchar.h> U zaglavlju <wchar.h> definirane su gotovo sve funkcije za rad s znakovima i stringovima koji su tipa wchar_t. Obično se ASCII znakove naziva prostim znakovima, a znakove tipa wchar_t proširenim znakovima (eng. wide character). Evo kako se inicijalizira prošireni znakovni tip i string: wchar_t c = L'A'; wchar_t *s = L"Hello"; Pored wchar_t tipa definiran wint_t, integralni tip koji može sadržavati vrijednost wchar_t tipa, te makro WEOF kao oznaka za kraj datoteke. Operacije sa stringovima proširenih znakova size_t wcslen(const wchar_t *s); wchar_t *wcscpy(wchar_t *dest, const wchar_t *src); wchar_t *wcscat(wchar_t *dest, const wchar_t *src); wchar_t *wcsncpy(wchar_t *dest, const wchar_t *src, size_t n); wchar_t *wcsncat(wchar_t *dest, const wchar_t *src, size_t n); int wcscmp(const wchar_t *s1, const wchar_t *s2); int wcsncmp(const wchar_t *s1, const wchar_t *s2, size_t n); int wcscoll(const wchar_t *s1, const wchar_t *s2); size_t wcsxfrm(wchar_t *dest, const wchar_t *src, size_t n); 386
  • 387.
    wchar_t *wcschr(const wchar_t*s, wchar_t c); wchar_t *wcsrchr(const wchar_t *s, wchar_t c); wchar_t *wcsstr(const wchar_t *s, const wchar_t *pat); size_t wcsspn(const wchar_t *s, const wchar_t *set); size_t wcscspn(const wchar_t *s, const wchar_t *set); wchar_t *wcspbrk(const wchar_t *s, const wchar_t *set); Ove funkcije (wcsxxx) su ekvivalentne funkcijama za rad s ASCII stringovima (strxxx), osim što se umjesto pokazivača na char koristi pokazivač na wchar_t, a broj n se intepretira kao broj wchar_t znakova. Operacije s nizovima proširenih znakova wchar_t *wmemcpy(wchar_t *dest, const wchar_t *src, size_t n); wchar_t *wmemmove(wchar_t *dest, const wchar_t *src, size_t n); int wmemcmp(const wchar_t *p1, const wchar_t *p2, size_t n); wchar_t *wmemchr(const wchar_t *p, wchar_t c, size_t n); wchar_t *wmemset(wchar_t *p, wchar_t c, size_t n); Ove funkcije (wmemxxx) su ekvivalentne funkcijama za rad s ASCII nizovima (memxxx), osim što se umjesto pokazivača na char koristi pokazivač na wchar_t, a broj n se intepretira kao broj wchar_t znakova. Pretvorba stringa proširenih znakova u numeričku vrijednost long int wcstol(const wchar_t *s, wchar_t **endp, int base) unsigned long int wcstoul(const wchar_t *s, wchar_t **endp, int base); double wcstod(const wchar_t *s, wchar_t **endp); Ove funkcije (wcsxxx) su ekvivalentne funkcijama za rad s ASCII stringovima (strxxx), osim što se umjesto pokazivača na char koristi pokazivač na wchar_t, Pretvorba vremena size_t wcsftime(wchar_t *buf, size_t bufsize, const wchar_t *format, const struct tm *tp); wcsftime() izvršava operaciju analognu izvršenju strftime(). Rastav stringa proširenih znakova na lekseme wchar_t *wcstok(wchar_t *s, const wchar_t *sep, wchar_t **state); Funkcija wcstok() vrši rastav stringa s ne lekseme koji su odvojeni znacima separatora (sep) analogno funkciji strtok(), osim što je temeljni tip wchar_t, i dodan je treći argument state, koji je pokazivač na objekt tipa wchar_t *; wcstok() koristi ovaj objekt za pohranu stanja između uzastopnih poziva funkcije. Ulazno izlazne operacije s proširenim znakovima getwchar, getwc, fgetwc 387
  • 388.
    wint_t getwchar(void); wint_t getwc(FILE *fp); wint_t fgetwc(FILE *fp); Ove funkcije čitaju znakove iz toka fp ili stdin (implicitno se vrši pretvorba multibajt- znakovnih sekvenci, kao da je pozvana funkcija mbrtowc).Ako je kraj datoteke funkcije vraćaju WEOF. Funkcionalnost im je ista kao kod funkcija getchar(), getc(), i fgetc(). putwchar, putwc, fputwc wint_t putwchar(wchar_t c); wint_t putwc(wchar_t c, FILE *fp); wint_t fputwc(wchar_t c, FILE *fp); Ove funkcije upisuju wchar_t znakove u toka fp ili stdin (implicitno se vrši pretvorba multibajt-znakovnih sekvenci, kao da je pozvana funkcija mbrtowc). Funkcionalnost im je ista kao kod funkcija putchar(), putc(), i fputc(). wprintf, fwprintf int wprintf(const wchar_t *, ...); int fwprintf(FILE *fp, const wchar_t *, ...); Ove funkcije su ekvivalentne funkcijama printf() i fprintf(), osim što se u tok zapisuje multibajt znakovna sekvenca, kao da je pozvan fputwc(). U format stringu specifikatori %c i %s i dalje znače da se očekuje prosti znakovi, a da bi se ispisali prošireni znakovi treba koristiti specifikatore %lc i %ls. wscanf, fwscanf int wscanf(const wchar_t *, ...); int fwscanf(FILE *fp, const wchar_t *, ...); Ove funkcije su ekvivalentne funkcijama scanf() i fscanf(), osim što se format string tretira kao niz proširenih znakova, a tok koji se očitava tretira se kao multibajt znakovni niz. U format stringu specifikatori %c, %s i %[ znače da se očekuje prosti znakovi, a da bi se unijeli prošireni znakovi treba koristiti specifikatore %lc , %ls i %l[. fgetws, fputws wchar_t *fgetws(wchar_t *, int, FILE *fp); int fputws(const wchar_t *, FILE *fp); Ove funkcije služe čitanju ili zapisu linije teksta analogno funkcijama fgets() i fputs(). ungetwc wint_t ungetwc(wint_t c, FILE *fp); Funkcija ungetwc() vraća prošireni znak c natrag u ulazni tok fp, analogno ungetc() funkciji. swprintf, swscanf 388
  • 389.
    int swprintf(wchar_t *buf,size_t bufsize, const wchar_t *format, ...); int swscanf(const wchar_t *buf, const wchar_t *format, ...); Funkcija swprintf() generira string buf, maksimalne veličine bufsize, a funkcija swscanf() dobavlja podatke iz stringa buf, prema zadanom formatu, analogno funkcijama sprintf() i sscanf(). vwprintf, vfwprintf, vswprintf int vwprintf(const wchar_t *format, va_list argp); int vfwprintf(FILE *fp, const wchar_t *format, va_list argp); int vswprintf(wchar_t *buf, size_t bufsize, const wchar_t *format, va_list argp); Ove funkcije su analogne funkcijama vprintf(), vfprintf(), and vsprintf(). Argument vswprint() bufsize omogućuje kontrolu maksimalne duljine stringa kao kod swprintf(). fwide int fwide(FILE *fp, int mode); Svaki tok ima "orijentaciju" koja pokazuje da li se on koristi s normalnim ili multibajt znakovima (pomoću funkcija iz ove sekcije) Početno je tok “neorijentiran”, ali se nakon prve upotrebe prebacuje u "bajt-orijentirani" ili "prošireno-orijentirani" mod obrade teksta. Funkcijom fwide() može se postaviti orijentacija toka fp, tako da se argument mode postavi na vrijednost veću od nule za "prošireno-orijentirani" mod, ili na vrijednost manju od nule za "bajt-orijentirani" mod. Funkcija vraća vrijednost trenutne orijentacije (0 znači da je tok neorijentiran). Dodatne pretvorbe btowc, wctob wint_t btowc(int c); int wctob(wint_t wc); Funkcija btowc() pretvara normalni znak c u prošireni znak. Funkcija wctob() pretvara prošireni znak wc u normalni znak. Vraća znak ili EOF ako pretvorba nije moguća. mbrlen size_t mbrlen(const char *s, size_t n, mbstate_t *state); Funkcija mbrlen() je slična funkciji mblen(). Njome se može dobiti i duljinu prekinute multibajt sekvence. Početni se dio referira u objektu state, a na preostali dio pokazuje pokazivač s. Vraća vrijednost kao funkcija mbrtowc(). mbrtowc, wcrtomb size_t mbrtowc(wchar_t *dest, const char *src, size_t n, mbstate_t *state); size_t wcrtomb(char *dest, wchar_t src, mbstate_t *state); 389
  • 390.
    Ove su funkcijeslične funkcijama mbtowc() i wctomb(), osim što mogu obraditi i dio multibajt sekvence koja je prekinuta, uz uvjet da je stanje pretvorbe zabilježeno u objektu na kojeg pokazuje state. Funkcija mbrtowc() pretvara multibajt sekvencu src u proširen znak u *dest i vraća broj bajta na koje pokazuje src koji tvore ispravnu multibajt sekvencu, ili 0 ako src==NULL, ili -1 ako nastane greška, ili -2 ako nije pronađena kompletna multbajt sekvenca (upotrebljena za *state). Funkcija wcrtomb() pretvara prošireni znak src u multibajt sekvencu dest i vraća broj bajta zapisanih u dest, ili -1 ako nastane greška. mbsrtowcs, wcsrtombs size_t mbsrtowcs(wchar_t *dest, const char **srcp, size_t n, mbstate_t *state); size_t wcsrtombs(char *dest, const wchar_t **srcp, size_t n, mbstate_t *state); Ove su funkcije slične funkcijama mbtowcs() i wctombs(), osim što mogu obraditi i dio multibajt sekvence koja je prekinuta, uz uvjet da je stanje pretvorbe zabilježeno u objektu na kojeg pokazuje state. String koji se pretvara prenosi se po referenci *srcp, kako bi se mogao ažurirati da pokazuje na preostali dio nepretvorenog stringa. Ako je broj n nedovoljan (kao broj proširenih znakova za mbsrtowcs() ili bajta za wcsrtombs()) za kapacitet odredišnog stringa rezultata, *srcp se postavlja tako da pokazuje na nepretvoreni ulaz, a *state se ažurira da odrazi prekinuto stanje pretvorbe. mbsinit int mbsinit(const mbstate_t *p); Funkcija mbsinit() vraća nenultu vrijednost ako je objekt stanja na kojeg pokazuje p u početnom stanju, ili ako je p==NULL. C 18 <wctype.h> U zaglavlju <wctype.h> deklarirano je nekoliko funkcija za klasificiranje i pretvorbu znakova tipa wchar_t, analognih funkcijama iz <ctype.h>. Klasifikacija proširenih znakova int iswupper(wint_t c); int iswlower(wint_t c); int iswalpha(wint_t c); int iswdigit(wint_t c); int iswalnum(wint_t c); int iswxdigit(wint_t c); int iswspace(wint_t c); int iswpunct(wint_t c); int iswprint(wint_t c); int iswgraph(wint_t c); int iswcntrl(wint_t c); Funkcije (iswxxx) su analogne funkcijana (isxxx), osim što je argument ovih funkcija tipa wint_t. 390
  • 391.
    Dodatne funkcije zaklasifikacija proširenih znakova wctype_t wctype(const char *classname); int iswctype(wint_t c, wctype_t classtok); Funkcija wctype() prihvaća argument classname u kojem se indicira klasifikacija i vraća token tipa wctype_t (definiran je u <wctype.h>). Funkcija wctype() prihvaća stringove: "upper", "lower", "alpha", "digit", "alnum", "xdigit", "space", "punct", "print", "graph", i "cntrl" (koji odgovaraju predefiniranoj klasifikaciji ), plus korisnički definirani string za klasifikaciju. Funkcija iswctype() prihvaća argumente znak c i token classtok koji je prethodno dobiven funkcijom wctype(), te vraće nenultu vrijednost ako znak ne pripada klasifikaciji Funkcije za pretvorbu proširenih znakova i stringova wint_t towupper(wint_t c); wint_t towlower(wint_t c); Ove funkcije su ekvivalentne funkcijana toupper() i tolower(), za normalne znakove. wctrans_t wctrans(const char *convname); wint_t towctrans(wint_t c, wctrans_t convtok); Funkcija wctrans() prihvaća argument convname u kojem se indicira znakovna pretvorba i vraća token tipa wctrans_t koji se koristi za izvršenje pretvorbe (wctrans_t je definiran u <wctype.h>). Funkcija towctrans() vrši pretvorbu proširenog znaka c prema tokenu convtok koji je prethodno dobiven funkcijom wctrans(), i vraća pretvoreni prošireni znak. Funkcija wctrans() prihvaća stringove "toupper" i "tolower" (koji označavaju predefinirani način pretvorbe), plus korisnički definirani string za pretvorbu. 391
  • 392.
    Index #define, 47, 186 biblioteke funkcija, 120 #elif, 190 binarne i tekstualne datoteke, 193 #else, 190 binarni brojevni sustav, 18 #endif, 190 binarno pretraživanje, 229 #if, 190 binarno stablo, 278, 279 #ifdef, 190 bit, 7 #ifndef, 190 bit polja strukture, 179 #include, 186 bitznačajni operatori, 75 #pragma pack, 177 BNF notacija, 289 #undef, 189 Booleova logika, 16 <assert.h>, 380 break, 100 <ctype.h>, 368 brojevni sustavi, 17 <errno.h>, 381 bsearch, 371 <float.h>, 381 bsearch(), 232 <iso646.h>, 382 calloc(), 369 <limits.h>, 381 centralna procesorska jedinica, 8 <locale.h>, 378 char, 43 <math.h>, 373 clearerr(), 364 <setjmp.h>, 377 clock(), 376 <signal.h>, 376 continue, 100 <stdarg.h>, 379 COUNTER, 209 <stddef.h>, 380 ctime(), 374 <stdio.h>, 357 De Morganov teorem, 16 <stdlib.h>, 369 decimalni brojevni sustav, 17 <string.h>, 365 deklaracija funkcije, 27 <time.h>, 374 deklaracija varijable, 44 <wchar.h>, 382 dereferencirani pokazivač, 53 <wctype.h>, 386 difftime(), 376 abort(), 372 digitalna logika, 14 abs(), labs(), 372 dinamičke matrice, 167 adresni operator &, 50 dinamičko alociranje memorije, 159 ADT, 208 disjunkcija, 15 apstrakcija, 12 div(), ldiv(), 372 apstraktni dinamički tipovi, 208 doseg identifikatora, 113 argumenti komandne linije, 157 double, 43 aritmetički izrazi, 72 do-while petlja, 99 ASCII, 39 dvostruko vezana lista, 266 asctime(), 374 egzistencijalni kvantifikator, 14 asocijativnost i prioritet operatora, 73 eksterne varijable, 117 assert, 210, 380 ekvivalencija, 15 atexit(), 371 enum, 180 atof(), 370 errno, 381 atoi(), atol(), 370 exit(), 371 automatske varijable, 114 fclose(), 195, 358 bajt, 7 feof(), 363 392
  • 393.
    ferror(), 364 komplement dvojke, 35 fflush(), 195, 358 komplement jedinice, 35 fgetc(), 358 konjunkcija, 15 fgetpos(), fsetpos(), 363 korijen stabla, 279 fgets(), 361 kule Hanoia, 226 FIFO, 218 leksemi, 84 FILE, 192 LIFO, 213 float, 43 linker, 22 fopen(), 194, 357 lista stringova, 258 for petlja, 99 listovi stabla, 279 fprintf(), 359 literalne konstante, 82 fputc(), 358 LL(1) analiza, 293 fputs(), 362 localeconv(), 378 fread(), 201, 362 localtime(), 374 free(), 369 lokalne varijable, 114 freopen(), 358 long, 43 fscanf(), 360 longjmp(), 377 fseek(), 204, 362 LSB, 7 ftell(), 203, 362 makefile, 33 funkcije, 64 makro, 187 fwrite(), 201, 362 malloc(), 369 generator slučajnih brojeva, 156 memchr(), 367 getchar(), getc(), 358 memcmp(), 367 getenv, 371 memcpy(), 367 gets(), 361 memmove(), 367 globalne varijable, 116 memset(), 368 gmtime(), 374 merge sort, 240 goto, 90 metajezik, 84 hash tablica, 316 mktime(), 376 heksadecimalni brojevni sustav, 20 MSB, 7 identifikatori, 82 multibajt znakovne sekvence, 372 if, 91 naredbe, 88 if-else, 92 negacija, 15 implikacija, 15 neterminalni simboli, 85 inicijalizacija varijable, 55 nibl, 7 INORDER, 284 nizovi, 103 int, 43 nizovi znakova, 146 Integrirana razvojna okolina, 22, 28 obilazak stabla, 283 intepreter, 22 oktalni brojevnim sustav, 20 interpreter prefiksnih izraza, 288 općenito stablo, 278 invertor, 16 operativni sustav, 9 izjavna logika, 14 operator indirekcije, 51 izlazne jedinice, 8 parser, 294 izrazi, 72 perror(), 364 izvedivi program, 22 petlje, 97 izvorni program, 6 pobrojani tip, 180 jednodimenzionalni nizovi, 103 podijeli pa vladaj, 228 klasifikacija znakova, 368 pointeri, 51 kodiranje, 34 pokazivači, 51 kodiranje realnih brojeva, 37 postfiksna notacija izraza, 215 kodiranje znakova, 39 POSTORDER, 284 komentar, 83 povezivač, 22 kompilator, 21 poziv funkcije, 26 393
  • 394.
    pozivna funkcija, 26 složeni operatori, 77 pozvana funkcija, 26 složenost algoritama, 233 predikat, 14 sortirane liste, 258 PREORDER, 284 sortiranje, 237 pretprocesor, 186 sortiranje umetanjem, 239 pretvorba tipova, 79 sprintf(), 364 pretvorba znaka, 369 srand(), 156, 370 printf(), 48, 359 sscanf(), 364 privremene datoteke, 206 stablo (tree), 278 produkcije C-jezika, 85 statičke globalne varijable, 118 program, 6 stderr, 192 proste naredbe, 88 stdin, 192 prototip funkcije, 27 stdout, 192 punjač, 22 Stog ADT (stack), 213 putchar(), putc(), 358 strcmp(), 150 puts(), 362 strcpy(), 149 qsort(), 371 strdup(), 162 quicksort, 243 strftime(), 375 računalo, 6 string, 146 Računarski algoritam, 9 string operator, 188 radna memorija, 8, 41 strlen(), 147 raise(), 376 strojni jezik, 21 RAM, 8 strtod(), 370 rand(), 156, 370 strtol(), strtoul(), 370 realloc(), 369 struktura, 171 red (queue), 218 switch-case, 95 referenca, 44 system(), 372 rekurzivne funkcije, 223 tablice simbola, 314 rekurzivno silazni parser, 294 terminalni simboli, 84 relacijski i logički izrazi, 74 ternarni uvjetni izraz, 78 remove(), 206, 365 Tic-Tac-Toe, 121 rename(), 206, 365 tijelo funkcije, 26, 66 repna rekurzija, 231 time(), 374 rewind(), 203, 362 tipovi podataka, 43 rezidentni programi, 9 tmpfile(), 365 riječ, 7 tmpnam(), 365 rječnici, 314 točka operator, 172 ROM, 8 tokeni, 84 samoreferentne strukture, 247 tokovi, 192 scanf(), 53, 360 typedef, 81 selekcijsko sortiranje, 237 ulazne jedinice, 8 semantičke akcije, 297 ungetc(), 362 separatori, 82 union, 178 setbuf(), setvbuf(), 363 univerzalni kvantifikator, 14 setjmp(), 377 unsigned, 43 setlocale(), 378 unutarnji čvorovi - podstabla, 279 short, 43 uvlačenje redova, 93 signal(), 377 va_arg, 379 sintaksa i leksika, 82 va_end, 379 sintaktički analizator, 294 va_start, 379 sizeof, 46 vanjska memorija, 8 sklop-I, 16 vanjski čvorovi, 279 sklop-ILI, 16 veliki-O notacija, 233 394
  • 395.
    vezana lista, 248 while petlja, 97 Visual Studio, 28 zaglavlje funkcije, 26, 66 višedimenzionalni nizovi, 111 zakon asocijacije, 16 void, 53 zakon distribucije, 16 void funkcije, 68 zakon idempotentnosti, 16 vprintf(), vfprintf(), vsprintf(), 364 zakon komutacije, 15 vrijeme i datum, 181 znakovni ulaz/izlaz, 198 395