Robert Logožar
Uvod u programiranje i
jezik C/C++
Uvod u programski jezik C/C++ Predgovor Uvod u programski jezik C/C++ je udžbenik koji pokriva gradivo uobičajenih uvodnih kolegija programiranja na visokoškolskim učilištima. Iako su C i C++ relativno zahtjevni jezici, danas su upravo oni standardan izbor nastavnih kurikuluma iz područja računarstava. Većina studenata se u svom prethodnom školovanju već trebala susresti s programiranjem u nekim drugim, konceptualno jednostavnijim ili jasnijim jezicima, kao što su npr. BASIC, LOGO, Pascal, i sl. Stoga je danas uvriježeno mišljenje da je na studiju najbolje odmah krenuti s usvajanjem jezika C i C++, kao standardnih jezika struke. Dodatni argument je vrlo elegantna i učinkovita sintaksa ovih jezika, koja je postala uzor i drugim, danas popularnim računalnim jezicima, kao što su npr. Java, Perl, PHP… Svi ovi razlozi dovoljni su da, i one čitatelje koji su početnici u programiranju, potaknu na učenje upravo ovih jezika. S druge strane, nužno je istaći da savladavanje C++ jezika u cjelini, uz punu objektno orijentiranu funkcionalnost, zahtijeva poznavanje temelja računarstva i posjedovanje osnovnih vještina u pisanju programa. Da se tome udovolji, najčešće se u okviru jednosemestralnog kolegija utvrđuje znanje klasičnog programiranja, kao i usvajanje sintakse jezika. To i jest cilj prvog dijela ovog teksta: savladavanje „tradicionalnog“, proceduralnog programiranja, koje se u C-miljeu naziva i funkcijsko programiranje. Za ovakvo bi programiranje u potpunosti bio dostatan jezik C , koji je i formalni podskup jezika C++. Programi u prvom dijelu ovog teksta napisani su upravo u duhu funkcijskog programiranja jezika C. Međutim, kao što je to danas uobičajeno, odmah se koriste sintaksne i formalne specifičnosti nasljednika C++. Dakle, usporedo s izlaganjem osnovna programiranja u proceduralnom stilu, ukratko se obrazlažu posebnosti jezika C++ , i glede njegove dodatne objektne prirode, i glede sitnih sintaktičkih različitosti u odnosu na njegovog prethodnika. Time se vrši prirema za usvajanje objektne paradigme programiranja za koju je jezik C++ i pisan, i koja u punoj mjeri iskorištava njegove potencijale. To će biti izloženo u kasnijim dijelovima ovog teksta.* Gradivo je izloženo tako da se nakon kratkog teorijskog izlaganja određene teme, rano prelazi na primjere i zadatke. Time se studenti potiču da odmah provjeravaju uvedene koncepte i zamisli programskog jezika na konkretnim primjerima, te da kroz samostalan rad i istraživanje odgonetavaju detalje jezika. Programski jezik je alat koji programeru služi da riješi probleme i ostvare svoje zamisli, i nema boljeg načina da se taj alat upozna nego kroz njegovu uporabu. No, s druge strane, potrebno je kod čitatelja razviti apstraktno promišljanje programiranja. Stoga svako poglavlje počinje izlaganjem motiva i svrhe pojednih tipova tvrdnji i svojstava jezika, te njihovom „općenitom definicijom“. Definicije nisu formalno stroge, jer se radi o uvodnom tekstu programiranja, ali su dovoljno precizne da se iz njih mogu iščitati opća pravila jezika. S druge strane, za čitatelje kojemu su i ovako postavljene definicije previše apstraktne, dodana su pojašnjenja i primjeri. Usvajanje gore spomenutog apstraktnog znanja će zasigurno biti puno lakše nakon što čitatelj riješi primjere i zadatke, uspješno odgovori na postavljena pitanja, i potom ponovo promotri osnovnu ideju izloženu u teorijskom uvodu. Uspješno programiranje nije moguće bez poznavanja osnova računarstva, a upravo je jezik C/C++ idealan da se to dvoje upoznaje usporedno. Također, iako se radi o uvodnom kolegiju, nužno je da se kod studenata odmah razvija osjećaj za formalno korektno programiranje, za izbor ispravnih programskih i podatkovnih struktura, te za pisanje preglednog i jasnog izvornog koda. *
Nisu dodani u ovoj inačici.
i
ii
Prilikom odabira imena varijabli i funkcija uglavnom su poštovana načela tzv. mađarske notacije (engl. Hungarian notation), koja je danas široko usvojena, a pogotovo u razvojnoj okolini Microsoft Visual C++ koju ovdje koristimo. Tendencija uporabe engleskih imena je opravdana činjenicom da se radi o standardnom jeziku struke. I sam jezik C/C++ koristi engleski jezik kao osnovu za svoje ključne riječi. Studente je stoga potrebno navikavati na uporabu profesionalne literature i pomoćnih izbornika upravo na tom standardnom jeziku struke. Imajući na umu da pred početnicima leži veliki zadatak usvajanja mnogobrojnih znanja, većina komentara je, osim u nekoliko izuzetaka, pisana na hrvatskom. Jezik C/C++ često se karakterizira i kao «nisko postavljeni» viši programski jezik. Njegova fleksibilna sintaksa, velike mogućnosti s jedne i suštinska jednostavnost i elegancija s druge strane, namijenjene su profesionalnim programerima. To zasigurno otežava njegovo učenje početnicima, ali ih zato i potiče da prodube svoje znanje u svim aspektima računarstva. «Slobodu» koje prevodilac dopušta treba dobro razumjeti, i tek onda će se otkriti neosporna učinkovitost i ljepota ovog jezika. Ujedno, ta činjenica zahtijeva i opravdava usputna obrazloženja o radu računala i prevodioca. Već smo spomenuli da je obim izloženog gradiva i zadataka nešto veći od onog što mogu svladati početnici bez iskustva u programiranju. Naime, prejednostavan pristup može biti nezanimljiv i nemotivirajući za naprednije studente. Za njih su dodani teži dijelovi gradiva i složeniji zadaci koji su označeni zvjezdicom (*). Ovi dijelovi se u pravilu mogu preskočiti u prvom čitanju. S druge strane, oni često razmatraju važne i nezaobilazne teme nužne za temeljitije razumijevanje jezika C/C++, i bolje svladavanje znanja i vještina programiranja.
R. Logožar, ožujak 2009.
iii
Sadržaj Uvod u programski jezik C/C++ .......................................................................i Predgovor ..............................................................................................................i Poglavlje 1.
Uvod u programiranje. Algoritmi ............................................ 1
1.1
Programiranje kao znanost i vještina...................................................................... 1
1.2
Algoritmi .................................................................................................................... 2
1.3
Temeljni pojmovi programiranja ............................................................................ 2
Poglavlje 2.
Jezici C i C++. Prvi programi u C++. ................................. 4
Poglavlje 3.
Osnovnih tipovi podataka u C / C++ .................................... 13
Poglavlje 4.
Osnovni operatori u jeziku C / C++ ....................................... 31
Poglavlje 5. Organizacija izvornog koda, programske strukture, i kontrola toka 39 Poglavlje 6. Osnovna struktura podatka – poredak. Jedno i više dimenzionalni poredci ...................................................................................... 63 Poglavlje 7.
Kazaljke – pokazivački tip. Navodi (reference) ................... 72
Poglavlje 8.
C (C++) funkcije .................................................................... 101
Poglavlje 9.
Znakovni poredci i C-nizovi. Primjer uporabe funkcija .. 122
Poglavlje 10. Strukturirano programiranje ............................................... 133 Poglavlje 11. Pretraživanje i sređivanje ..................................................... 136 Poglavlje 12. Dodatna poglavlja funkcijskog programiranja ................. 155 Poglavlje 13. Dodatni zadaci ........................................................................ 162 Literatura ......................................................................................................... 178
Poglavlje 1.
Uvod u programiranje. Algoritmi
Programiranje računala je djelatnost stara koliko i sami računalni strojevi. Današnji, viši programski jezici, uvelike olakšavaju pisanje programa jer su orijentirani prema problemu (engl. problem oriented languages), za razliku od strojnih jezika koji su orijentirani prema računalu. (engl. machine oriented languages). * Dakle, dok su strojni jezici izravno povezani sa strojnim instrukcijama i načinima adresiranja memorije koje određeni procesor može izvoditi, viši programski jezici apsthrahiraju tehničke detalje rada računaula kroz primjenu izbora simbola, ključnih riječi, i načina pisanja programa koji su bliži ljudskom načinu izražavanja i matematičkom jeziku i logici. Danas postoji mnošto viših programskih jezika od opće do specijalizirane uporabe. Takvi jezici olakšavaju stručnjacima iz nekog područja da sami pišu svoje programe. Pri tom je poznavanje svojstava jezika i načina pisanja programa nužno, ali ne i dovoljno za uspješno programiranje. Jasno je da samom pisanju programskog koda mora prethoditi analiza problema koji želimo riješti na računalu. U ovom početnom poglavlju dajemo kratak osvrt upravo na taj dio programiranja, koji je neovisan o rabljenom programskom jeziku, a tiče se izražavanja rješenja problema u algoritamskom obliku.
1.1 Programiranje kao znanost i vještina. Da bismo uspješno izradili program, potrebno je potpuno razumijevanje problema ili zadatka koji želimo riješiti, u smislu da njegovo rješenje možemo precizno razraditi „na papiru“. Za uspješno programiranje nepohodno je: i. razumijevanje problema ili zadatka koji želimo riješti; ii. osmišljavanje rješenja i njegovo izražavanje u algoritamskom obliku; iii. poznavanje programskog jezika (alata) u kojem se to rješenje piše. Iz ovoga je jasno da dobri programeri moraju imati široko predznanje mnogih temeljnih disciplina. Tu u prvom redu ističemo matematiku, za koju je tipično apstrahiranje problema, tj. izvlačenje njegovih najbitnijih i općih odrednica koje su primjenjive na sve konkretne instance. Također, u matematici se redovito primjenjuju precizno formulirani postupci, jednako kao i u računarstvu. Za programiranje u jeziku kao što je C/C++ nužno je i poznavanje osnova o radu računala. Konačno, ako se radi o problemu iz nekog specijalnog područja, potrebno je usvojiti i osnovna znanja iz njega. Uvriježeno je mišljenje da programiranje ne predstavlja samo znanost, nego i umijeće. Isti se problem često može riješiti na nekoliko različitih i podjednako dobrih načina. Pogotovo se rješenja i pristupi razilaze u izvedbi vrlo opsežnih programa, iako oni često rabe slične sastavne dijelove. No slično kao i u umjetnosti, iako postoje različiti autorski pristupi istim temama, kritičari će lako razlučiti dobra djela od loših. Istina je da i loš program može davati zadovoljavajuće, ili barem donekle prihvatljivo rješenje. Međutim, tu računarska znanost postavlja jasne kriterije glede korektnosti, brzine rada programa (algoritma), količine utrošene memorije, robustnosti, mogućnosti nadogradnje, i sl.. Nadalje, računarstvo propisuje podatkovne strukture i algoritme s njima u svezi koje treba rabiti. To je nezaobilazna osnovica znanja na kojem se dalje izgrađuje vještina. *
Strojni jezici se kod nas u računarskom žargonu nazivaju asemblerski jezici (od engl. izvornika assembly languages) što se često krati u naziv asembleri (engl. assembler). Ovaj kraći naziv preklapa se s programom za prevođenje programskog koda napisanog u strojnom jeziku u formi čitkoj i razumljivoj čovjeku, u objektni binarni kôd (sastavljen od 0 i 1) „razumljiv“ računalu.
1
2
Od samog početka važno je istaći činjenicu da je bit programiranja u rješavanju zadanog problema za najširi i najopćenitiji broj slučajeva. Programi koji rade samo uz vrlo ograničene kriterije, i koji se stalno moraju mijenjati i usklađivati i za najmanje promjene ulaznih podataka, su nevaljali i besmisleni. Njih treba izbjegavati čak i kao školske primjere. Loše navike u programiranju lako se stječu i kasnije mogu predstavljati ozbiljnu kočnicu pri nailasku na teže probleme. Programiranje je tehnička disciplina koja zahtijeva jasnoću, preglednost i uporabu «elegantnih rješenja». Da bi programer razvio osjećaj za to, mora dobro razumjeti sintaksu i semantiku programskog jezika, te detaljno proučiti mnogobrojne primjere. Zadovoljenje lošim programom pod izlikom da su današnja računala brza i da količina utrošene memorije nije problem, je nedopustivo. Mnogi studenti ignoriraju potrebu za usvajanjem novih znanja i čvrstih računarskih principa, te nastavljaju rješavati probleme na svoj stari, «intuitivan», način. Takav pristup onemogućava napredak i rezultira neuspjehom pri prvim ozbiljnijim programskim zadacima. Nadalje, neke naizgled nevažne stvari kod jednostavnih programa, postaju presudne kod kompleksnih. Učestale loše navike kod programiranja su nemarnost u programskoj formi, loš i nekonzistentan stil, ignoriranje potrebe za uvlačenjima linija koda (tzv. identacija) prema njihovoj funkcionalnoj razini, izostanak odgovarajućih komentara, itd. Gotovo da nema poznatog autora na području programiranja koji ne ističe važnost programskog stila i važnost preglednosti napisanog koda (vidi npr. [3]). Bez poštivanja tih načela će ono što u trenutku pisanja izgleda kao jasno, već sutra biti potpuno nerazumljivo i samom autoru.
1.2 Algoritmi Programeri moraju razvijati svoje sposobnosti za algoritamski pristup rješavanju problema. Algoritam ukratko možemo definirati kao precizno definirani postupak koji na temelju nekih zadanih veličina, u konačnom broju koraka rješava određenu zadaću i daje odgovarajući rezultat. Razumijevanje algoritama i njihovo programiranje iziskuje razvijeno apstraktno razmišljanje, i analitičko i sintetičko. Potrebna je i kreativnost, od osmišljavanja potrebnih varijabli i struktura, davanja prikladnih imena, korištenja prikladnih podatkovnih i programskih struktura, do razmatranja različitih rješenja i odabira najpovoljnijih. Rješenje problema iz nekog područja možemo predočiti u algoritmom u nekom prikladnom obliku, te ga zatim preformulirati u računarski algoritam u vidu dijagrama toka ili pseudo-koda. Ostatak posla je prevođenje takvog rješenja u programski kod specifičan za rabljeni jezik. Za jednostavnije probleme vješti programeri mogu tih nekoliko faza odmah razriješiti u glavi i izravno pisati programe. Za početnike je preporučljivo da isprva na papiru skiciraju rješenje i tek onda pređu pretvorbi tog rješenja u kompjuterski program.
1.3 Temeljni pojmovi programiranja Četiri su ključne faze u izradi i primjeni programa: 1. 2. 3. 4.
Pisanje izvornog koda; Prevođenje izvornog koda; Povezivanje u izvedbeni kôd; Testiranje programa.
Pisanje izvornog koda možemo realizirati u bilo kojem programu za uređivanje teksta (engl. text editor). Danas postoje integrirane razvojne okoline (npr. Microsoft© Visual Studio), koje uz prevodioca i povezivača imaju ugrađene programe za upis i ispravljanje izvornog koda. U C++ jeziku, datoteku izvornog koda pohranjujemo pod fizičkim imenom ime_programa.cpp (u jeziku C to je bila datoteka ime_programa.c )
3
Prevođenje izvornog koda je postupak u kojem prevodilac (engl. compiler) provjerava sintaksu (gramatičku ispravnost, vidi kasnije) napisanog izvornog koda uz javljanje mogućih pogrešaka za vrijeme prevođenja (engl. compile-time errors), te prevodi tvrdnje (engl. statements) ili naredbe višeg programskog jezika u strojni kôd, koji se sastoji od binarno kodiranih strojnih instrukcija (engl. instructions), razumljivih datom procesoru kao osnovnoj sastavnici računarske platforme (engl. computing platform). Stvara se datoteka objektnog koda pod imenom ime_programa.obj. Povezivanje (engl. linking) u izvedbeni kôd je postupak u kojem povezivač (engl. linker) obavlja povezivanje objektnog koda u datoteci ime_programa.obj, s objektnim datotekama već gotovih funkcija koje smo koristili u programu, sadržanih u bibliotekama (engl. libraries) funkcija. Greške koje se pri tome mogu javiti su tzv. greške u vrijeme povezivanja (engl. link-time errors). Nakon uspješnog povezivanja stvara se datoteka izvedbenog koda: ime_programa.exe. Ovu datotkeu operacijski sustav prepoznaje kao program koji je moguće izvršiti na datoj računarskoj platformi. Testiranje. Izvedbeni kôd još uvijek ne jamči da će program uspješno raditi. Program treba testirati. Preostale greške spadaju u tzv. logičke greške, koje se još nazivaju i greške pri izvođenju (engl. run-time errors). Za testiranje programa služi dodatni program, kojeg bismo mogli nazvati uklanjač grešaka (engl. debugger), kojim testiramo program tvrdnju po tvrdnju, uz moguću analizu vrijednosti pojedinih varijabli u programu, stanja registra procesora, memorije itd. Također, programer po potrebi sam kreira programe koji služe za provjeru drugih programa za karakteristične i kritične vrijednosti ulaznih podataka. Osim pogrešaka (engl. errors), prevoditelj i povezivač mogu javiti i upozorenja (engl. warnings). Ta upozorenja ne onemogućavaju prevođenje, povezivanje i izvršavanje programa, ali predstavljaju opasnost. Iz tih razloga, najbolje je da se uklone.
4
Poglavlje 2.
Jezici C i C++. Prvi programi u C++.
U ovom poglavlju dajemo kratak pregled nastanka programskih jezika C i C++, te njihovih osnovnih značajki. Objektno orijentirani programski jezik C++ konstruirao je Bjarne Stroustrup 1983.-1985. iz Bell Laboratories u SAD. On predstavlja de-facto standard suvremenog računarstva, i jednako je prisutan i u znanstvenoj i industrijskoj praksi. Nastao je na temelju jezika C (Dennis Ritchie 1972, Bell Laboratories) kao najzastupljenijeg klasičnog programskog jezika, poznatog između ostalog i po tome što je u njemu bio napisan operacijski sustav Unix, te mnogi drugi, računarski najsloženiji primjenski (aplikativni) programi. Jezik C++ u sebi kao podskup sadrži jezik C i dosljedno nastavlja njegovu sintaksu. Pošto je za objektno programiranje potrebno imati vještinu i razumijevanje uobičajenog, tzv. proceduralnog programiranja, uvodno izučavanje se svodi na jezik C. Kako što je već napomenuto u predgovoru, jezik C je u sebi sažeo i unaprijedio mnogobrojna rješenja iz drugih viših programskih jezika, jednako kao što je uveo i najnovija dostignuća, te odgovorio zahtjevima suvremenog računarstva. Istovremeno, to je jezik stvoren da u prvom redu služi za profesionalnu uporabu. Zato C zasigurno nije jezik za početnike koji to namjeravaju i ostati. Također, teorijski gledano, postoje formalno elegantniji i konzistentniji jezici (npr. Pascal, Modula 2, i drugi), koji su, s vremenom izašli iz profesionalne uporabe. C je izgrađen na temeljima koji se oslanjanju na standardna arhitekturalna rješenja procesora, kao npr. tretiranje nizova ili poredaka, inkrementirajući i dekrementirajući operatori, rad s pokazivačkim tipom, itd., a istovremeno omogućuje pisanje kratkog i elegantnog programskog koda. U tom smislu je C/C++ najsnažniji programski jezik današnjice, ali istovremeno i alat kojim nije lako potpuno ovladati. Sve to iziskuje povećani trud koji početnici moraju uložiti u njegovo razumijevanje. Funkcijsko programiranje. Pisanje C programa svodi se na pisanje programskih funkcija, pa govorimo o funkcijskom programiranju (engl. functional programming). Čak i ono što se u drugim jezicima zvalo «glavni program», u jeziku C je elegantno svedeno na funkciju. To je posebna glavna funkcija, engl. main() funkcija, sa ili bez argumenata (za funkcije vidi pogl. 6, za uporabu glavne funkcije main vidi primjere niže). Glavna funkcija može pozivati druge funkcije koje je napisao programer ili koje su sadržane u standardnim bibliotekama funkcija. Postoji potpuna analogija između funkcijskog programiranja i proceduralnog programiranja, pa se jezik C ubraja u skupinu proceduralnih programskih jezika gdje spadaju npr. Pascal, Modula 2, Ada.
Prvi programi i kratak pregled sintakse jezika C / C++ Sav rad računala svodi se na izvršavanje računalnih programa, što se opet svodi na izvršavanje niza strojnih instrukcija razumljivih računalnom procesoru. Cilj programa je da na način propisan određenim programskim jezikom omogući izvršenje željenog zadatka. Kao uobičajeni početni primjer navodimo program «Zdravo svijete!», koji na ekran računala ispisuje prvu poruku budućih programera. Primjer 1. Program „Zdravo svijete“. Napišite program u C++ kojim ćete iz C++ programa pozdraviti svijet. #include
// Pretprocesorska direktiva (uputa).
5 int main() // Glavna funkcija, pod nazivom "main". { // Program ispisuje znakovni niz (string): std::cout << "Zdravo svijete, iz C++ programa!" << std::endl; return 0;
// Povratak iz glavne funkcije uz slanje rezultata 0.
}
Primjer 2. Program „Zdravo svijete“ uz drugačiji stil pisanja blokova tvrdnji. Udrugom primjeru dajemo stil pisanja kod kojeg se prva vitičasta zagrada piše u retku tvrdnje (tvrdnje) ili funkcije na koji se odnosi blok tvrdnji koji slijedi. Zatvorena vitičasta zagrada piše se na isti način kao i gore, tj. poravnava se u kolonu u kojoj je početno slovo retka pripadne otvorene vitičaste zagrade. #include
// Pretprocesorska direktiva (uputa)
int main() { // Glavna funkcija pod nazivom "main". // Program ispisuje znakovni niz (string): std::cout << "Zdravo svijete, iz C++ programa!" << std::endl; return 0;
// Povratak iz glavne funkcije uz slanje rezultata 0.
} Tipovi izričaja u jeziku C / C++. Prilikom imenovanja pojedinih sastavnica programa koristimo «identifikatore» (engl. identifiers). Oni predstavljaju imena (simbole) kojima nazivamo varijable, tipove podataka, funkcije i oznake (engl. label). Osnovno je načelo da svaki kompjuterski program predstavlja potpuno određen skup: – uputa ili direktiva (engl. directives); – izraza (engl. expressions) i – tvrdnji ili naredbi (engl. statements). Način njihovog pisanja propisan je sintaksom (engl. syntax) jezika. Sintaksa općenito predstavlja skup pravila i obrazaca za tvorbu gramatički korektnih rečenica nekog jezika. Osnove sintakse jezika C ćemo ukratko predstaviti kroz objašnjenja prethodnog programskog primjera. Direktive. Izričaj #include na početku ispisa je pretprocesorska tvrdnja (engl. preprocessor statement), ili, kao što se češće naziva pretprocesorska direktiva (smjernica, naputak, engl. directive). Sve pretprocesorske direktive dio su zasebne cjeline koju obrađuje „pretprocesor“, program koji je danas standardno ugrađen u razvojne okoline. C/C++ prevodilac te iste direktive ne raspoznaje, odnosno ignorira, i stoga je je pretprocesiranje u suštini odvojeno od samog jezika C / C++. Sintaktička je posebnost pretprocesorskih direktiva uporaba znaka povisilice ispred ključne riječi. Cilj pisanja direktiva i rada pretprocesora je priprema izvornog koda za prevođenje automatizacijom mnogih radnji, od kojih su najčešće upravo uključenje zaglavlja često korištenih datoteka s objavama detalja o funkcijama koje koristimo. Tako za spomenuti slučaj uključenja zaglavnih datoteka koristimo spomenutu direktivu #include (od engl. to include = hrv. uključiti ), kao u našem primjeru, gdje je direktivom: #include uključena, tj. na mjesto navoda direktive upisana u cijelosti datoteka pod nazivom iostream. Radi potpunosti, navedimo da su često korištene direktive još i #define , koja služi za definiranje tzv. makro tvrdnji (engl. macro statements), te uvjetne direktive tipa #if .
6
Posve ukratko, ideja makro tvrdnji je da posluže kao kratice za pisanje programskog koda. Svugdje gdje programer u programu napiše naziv macroName neke makro tvrdnje, ugradit će se cijeli programski kôd definiran direktivom #define macroName. Uvjetne direktive ( #if, #endif, #else, #elif, * #ifdef, #ifndef ) služe da se u ovisnosti o vrsti prevodioca definiraju uvjeti kompiliranje te uključe prikladne zaglavne datoteke. U jeziku C , kao i u starijim inačicama MS© Visual C++ kompilatora (do inačice VS 6.0), ove su se objave smještale u zasebne datotke s ekstenzijom .h , pa se ova datoteka nazivala iostream.h . Ekstenzija je upućivala da se radi o datoteci zaglavlja (engl. header file), odnosno da se radi o dijelu koda koji se piše u zaglavljima programa. Pošto C++ ISO standard ne propisuje da se te objave moraju vršiti u zasebnim datotekama, bolje je koristiti standardnu, gore navedenu, sintaksu, kojoj su se prilagodili i novije inačice Microsoftovih razvojnih alata. Unutar svake razvojne okoline postoji mapa pod nazivom include (npr. unutar MS Visual Studia 2005 staza do mape je: C:\Program Files\Microsoft Visual Studio 8\VC\include), unutar koje se može pronaći datoteka pod nazivom iostream . Pregledom u nekom uređivaču teksta (npr. u Windows OSu s pomoću Notepada), vidljivo je da se radi o relativno kratkoj datoteci izvornog koda, veličine 1KB (što je i minimalna velična datoteke u ovom operacijskom sustavu), u kojoj se nalaze samo objave, bez funkcionalnih programskih dijelova. Pri vrhu ove datoteke lako je uočiti direktivu: #include
koja prevodiocu nalaže uključenje datoteke istream , smještene u istoj mapi. Otvaranjem datoteke istream , veličine 32KB, bez ulaženja u detalje, možemo uočiti da se u njoj nalazi funkcionalni dio izvornog koda potrebnog za ostvarenje unosa podataka s tastature. Također, u njoj se nalaže uključenje datoteke ostream , s pomoću direktive: #include
Upravo datoteka ostream sadrži funkcionalni dio potreban za ostvarenje ispisa na ekran, što je najvažniji dio našeg uvodnog programa. Unutar iste mape možemo vidjeti i veći broj zaglavnih datoteka s ekstenzijom iostream.h u skladu sa starim stilom organizacije jezika C. Za svaku zaglavnu datoteku iz jezika C postoji i odgovarajuća datoteka u C++ kojoj je ime kreirano tako da je na početku dodano slovo c , i ispuštena ekstenzija .h . Tako zaglavnim datotekama: assert.h , string.h i wctype.h iz jezika C, odgovaraju datoteke: cassert , cstring i cwctype u jeziku C++ , i sve se one mogu pronaći unutar navedene include mape. Naravno, nove C++ datoteke, ne samo da imaju različita imena, nego su i pisane u duhu objektnog programiranja koji podupire ovaj jezik. Pored navedenih, u jeziku C++ dodane su i mnoge druge korisne datoteke što će biti diskutirano u pasusu niže. U mapama naziva src, smještenima unutar podmapa \VC\ce\crt i \VC\crt , nalaze se datoteke izvornog koda za jezik C (ekstenzija .c ) i C++ (ekstenzija .cpp ). Slična struktura mapa i način smještaja datoteka vrijedi i za druge razvojne okoline. Npr. za DevC++ su C zaglavne datoteke smještene u mapi C:\Dev-Cpp\include , a odgovarajuće C++ datoteke u C:\Dev-Cpp\include\c++\3.4.2 . C standardna biblioteka. Još u jeziku C navedene datoteke sa srodnim funkcijama i strukturama (struct, od engl. structure) objedinjavale su se pod nazivom biblioteka (engl. library) funkcija. Standard ISO C propisuje 24 zaglavne datoteke koje, zajedno s odgovarjućim izvornim datotekama, čine C standardnu bibiloteku (engl. C Standard Library).
*
Usporediti s tvrdnjama selekcije if,
else, else if,
u pogl. 5.
7
C++ standardna biblioteka. Da bi se osigurala kompatibilnost, u jezik C++ uključena je C standardna biblioteka u cijelosti. Dakle, programski kôd pisan u jeziku C i može se i dalje uspješno kompilirati s pomoću C++ prevodioca. Konkretno, ako neki C program na svom početku uključuje gore spomenute zaglavne datoteke: assert.h , string.h i wctype.h , možemo ga prevesti s pomoću C++ kompilatora bez da unosimo ikakve promjene. S druge strane, pišemo li novi program u C++ razvojnoj okolini od početka, uputno je odmah koristiti zamjenske C++ datoteke, imena kojih su dobivena gore opisanim preinakama. Time se osigurava da će novi programski kôd biti u cijelosti pisan prema formalno strožim načelima objektnog programiranja. U C++ standardnu biblioteku ulaze i dodatne datoteke: ios , iostream , iomanip ,
kao i mnoge druge, od kojih nam upravo funkcionalnost ostvarena u datoteci iostream omogućuje izvršavanje našeg programa. Pored standardnih datoteka, u bibliotekama pojedinih razvojnih okolinima mogu se naći i dodatne. Ako je potrebno osigurati prenosivosti izvornog koda, uputno je držati se standardnih na koje programer može uvijek računati. Za detaljnije razmatranje C i C++ biblioteka čitatelj se upućuje na [7xx], članci C (C++) Standard Library. Rezimirajmo, u skladu sa starom stilom, u zaglavnim datotekama s ekstenzijom .h objavljuju se potrebne funkcije i podatkovne strukture jezika C. Izvorni kôd, u kojima su te deklarirane C funkcije implementirane, nalazi se u istoimenim datotekama s ekstenzijom .c . U stilu jezika C++, objave se vrše na početku datoteka bez ekstenzije .h , a njihov izvorni kôd se nalazi bilo u istim, bilo u datotekama s ekstenzijom .cpp . U duhu objektnog programiranja, ovdje se objavljuju i implementiraju klase, u sklopu kojih djeluju članske funkcije. Pored funkcija, C++ dozvoljava i definiranje operatora, čije je djelovanje istovjetno kao i funkcija, ali je način pisanja u mnogim slučajevima pregledniji i praktičniji. U našem prvom programu odmah ćemo imati primjer uporabe operatora kopiranja na izlazni objekt cout klase ostream (vidi niže), deklarirane u datoteci iostream , te implementrane u datoteci ostream. Komentari. U jeziku C / C++ postoje dva načina pisanja komentara. Dvostruka kosa crta // je znak za komentar do kraja retka. Za komentar u više redaka, ili npr. za isključivanje niza tvrdnji iz programa, koristi se komentar-blok kojem se početak označuje s /*, a kraj s */. Znakovi za komentar, jednako kao i sam komentar, dijelovi su isključivo izvornog koda, namijenjeni kao podsjetnik programeru i korisniku napisanih funkcija. Kao takvi, jasno je da se komentari ne prevode u strojni kôd. Pisanje komentara je nužno da bi se uz izvorni kôd podastrijela dokumentacija o njegovoj zadaći, ulaznim i izlaznim veličinama, korištenim varijablama itd. Programi (funkcije) bez komentara su manjkavi, teže ih je razumjeti, ispravljati i primjenjivati. Glavna funkcija. Već je rečeno da se koncept glavnog programa u jezicima C i C++ ostvaruje funkcijom pod nazivom main() . Svaki C++ program sadrži jednu i samo jednu main() funkciju. To je glavna funkcija u programu i ujedno onaj dio programa koji se počinje prvi izvoditi. Ispred funkcije je oznaka njenog tipa, tj. tipa rezultata koji funkcija vraća (u našem primjeru cjelobrojni tip int). U našem primjeru koristimo uobičajenu praksu u jeziku C da tvrdnjom return 0 glavna funkcija vraća operacijskom sustavu cjelobrojnu vrijednost 0, i to je znak da je program uspješno završen. U protivnom se vraća neka druga vrijednost, ovisno o ishodu programa i postavkama operacijskog sustava. Ukoliko funkcija ne treba vraćati nikakav rezultat, označujemo je posebnim tipom void (engl. void = ništavan, nevažeći, prazan), i u tom slučaju se na kraju funkcije ne koristi tvrdnja return. Iza imena funkcije stoji par okruglih zagrada u koje dolaze argumenti (engl. arguments) ili varijable (ulazni podaci) funkcije, ukoliko oni postoje. Ako ne postoje zagrada je prazna, kao u našem
8
primjeru. U slučaju da pokrećemo program u komadnoj liniji (engl. command prompt), navodimo ime datoteke izvedbenog koda, te nakon toga argumente glavne funkcije, npr.: c:\c_examples> opseg .exe 1200
(u našim primjerima za sada nećemo koristiti argumente u glavnoj funkciji main ). Blokovi. Sve tvrdnje ili tvrdnje u C / C++ pišu se unutar bloka tvrdnji (engl. statement block) ili prema učestalom hrvatskom nazivu, bloka naredbi. Blok započinje otvorenom vitičastom zagradom ‘{’ , a završava pripadnom zatvorenom zagradom ‘}’ . Prevodilac kontrolira uparenost svih otvorenih i zatvorenih zagrada, pa i vitičastih za definiranje bloka. Blokovi se po potrebi mogu gnijezditi jedan unutar drugog do proizvoljne razine (vidi poglavlje 5). Tvrdnje. Tvrdnje (engl. statement), u hrvatskom se uobičajeno nazivaju i naredbe,* po uzoru na nazivlje preuzeto iz drugih programskih jezika. Izraz tvrdnja bolje opisuje deklarativni koncept proceduralnih i funkcijskih jezika, u koje spadaju i C / C++. Ukratko, kod funkcijskog programiranja se na temelju opisa, odnosno definicije funkcije izračunava njen rezultat, slično kao u matematici (vidjeti također pogl. 8). Nasuprot tome, u imperativnom programiranju se govori o imperativnim tvrdnjama (engl. imperative statements) ili naredbama (engl. commands) kojima se mijenjaju stanja (varijabli) programa. Navedene razlike su formalne naravi, te se uglavnom tiču stila pisanja i načina tumačenja programskih izričaja. Jasno je da će i u slučaju funkcijskog, i imperativnog programiranja, izvorni programski kôd biti preveden u skup sličnih strojnih instrukcija koje su raspoložive na datom procesoru, odnosno računarskoj platformi. Tvrdnje predstavljaju osnovne, funkcionalno povezane cjeline koje se koriste za pisanje programa, napisane kao jedan ili više izraza, nalik matematičkim ili sličnim, u skladu sa sintaksom jezika C / C++. Tvrdnje možemo usporediti s rečenicama koje su osnovni sastavni dijelovi nekog teksta. Svaka tvrdnja u bloku završava znakom točka-zarez ‘;’ , kojeg promatramo kao terminator (konačni znak) programske rečenice.† U labavoj definiciji pod tvrdnjama podrazumijevamo jednostavne izraze koji obavljaju neki zaokruženi zadatak. U našem je primjeru to upravo ispis poruke na ekran ostvaren s pomoću tvrdnje: std::cout << "Zdravo svijete, iz C++ programa!" << std::endl; Drugim riječima, u našem programu postoji samo jedna tvrdnja koja u na prikaznik računala ispisuje tekstualnu poruku unutar dvostrukih navodnika, i potom prebacuje ispis u novi redak konzolne aplikacije. Primijetimo da tvrdnja nije elementarna, u smislu da se ista zadaća mogla obaviti i s pomoću dvije tvrdnje oblika: std::cout << "Zdravo svijete, iz C++ programa!" std::cout << endl; gdje prva vrši ispis poruke na ekran, a druga prebacuje daljnji ispis (nakon završetka našeg programa) u novi redak. Dakle, slično kao što pisanjem složenih rečenica možemo preciznije i jasnije izraziti naše misli, tako i pisanjem složenih tvrdnji u C / C++ možemo pisati bolje i preglednije programe. Naglasimo još jednom da prevodilac prepoznaje kraj tvrdnje u znaku ‘;’ . To omogućuje pisanje jedne tvrdnje u više redova i njenu preglednu organizaciju prema želji programera (bjeline, tabulatori, i
*
Naziv naredba ušao je u hrvatski računarski jezik, iako nema odgovarajući izvorni pojam u engleskom jeziku. U jeziku C / C++ osnovni tip izričaja su tvrdnje (engl. statements). U strojnim jezicima govori se o instrukcijama ili uputama (engl. instructions). †
Pošto se točka u računarstvu rabi za zapis decimalnih razlomljenih brojeva, točka-zarez je prirodan izbor za označavanje kraja osnovne programske cjeline. Isti znak koristi i jezik Pascal uz malu sintaktičku razliku: tamo je to oznaka granice između dviju „programskih rečenica“, pa ga nije potrebno stavljati iza zadnje.
9
znakovi za novi red ― kao nevidljivi znakovi za kontrolu ispisa, se jednostavno ignoriraju). Suvišna točka zarez neće biti interpretirana kao greška, već će „nepostojeća tvrdnja“ jednostavno biti ignorirana. S druge strane, nedostajuća točka-zarez može uzrokovati neugodnu sintaktičku pogrešku. Naravno, uvijek nastojimo da znak točka-zarez pišemo upravo tamo gdje treba ― na kraju tvrdnje.* Na tvrdnje ćemo se još osvrnuti i u pogl. 5xx. Tijelo funkcije. Tijelo funkcije (engl. function body) je njezin dio u kojem su sadržane tvrdnje kojima se ostvaruje željena zadaća. Tijelo funkcije se standardno realizira kao blok, što, naravno, vrijedi i za glavnu funkciju main . U našem primjeru glavna funkcija je definirana jednim blokom. Iznimka su slučajevi kad postoji samo jedna tvrdnja, u kom slučaju se ne treba otvarati blok s pomoću vitičastih zagrada. Više detalja o blokovima dano je u pogl. 5xx. Preostali dio programa. Iako su, u ovom uvodnom izlaganju, mnogi pojmovi tek dotaknuti, programeri moraju stvarati naviku razumijevanja svih dijelova programskog koda koji koriste, nastojeći dobro razlučiti o kojoj tvorbi programskog jezika se radi, čemu ona u načelu služi, koji je način njene uporabe, koji su njeni ulazni parametri, te koji rezultate polučuje. U mnogim slučajevima to će biti dostatno. Stoga, radi potpunosti, slijede kratka objašnjenja preostalog dijela uvodnog programa. je objekt klase ostream , kojom se ostvaruje funkcionalnost tzv. izlaznog toka ili struje (engl. stream) nizova znakova. Ime objekta cout (čitaj: „si aut“)† je važeće, tj. ima dogled unutar imenskog prostora (engl. name space) naziva std . Klasa objekta je objavljena u zaglavnoj (engl. header) datoteci iostream , a implementirana u datoteci ostream . std::cout
Spomenimo tek ukratko da je klasa programska tvorba koja služi za općeniti opis nekog entiteta, pojma, svojstva i sl. Klasa uključuje uporabu proizvoljnog broj varijabli za pohranu podataka, kao i tzv. članskih funkcija za rad s tim podacima. Objekt predstavlja konkretan primjerak (engl. instantiation) neke klase. Ovi ključni pojmovi objektnog programiranja bit će povremeno dotaknuti u poglavljima koja slijede, a detaljnlije objašnjeni u drugom dijelu ovog udžbenika. je tzv. ispisni manipulator koji prebacuje ispis u novi redak i prazni izlazni tok (vidjeti također odjeljak nakon primjera 2.20). Definiran je na sličan način i u istom imenskom prostoru kao i objekt cout. Pražnjenje izlaznog toka je važna radnja, pogotovo kod po završetku ispisa većeg broja znakova. Operatori kopiranja mogu se nizati jedan iza drugoga, kao u našem primjeru, i ispis će teći s lijeva na desno. U programskim odsječcima koji slijede, trebat ćemo ostvariti i unos podataka. Zato će nam poslužiti objekt cin i operator kopiranja >> . std::endl
(čitaj: „si in“)‡ je objekt klase istream kojom se ostvaruje funkcionalnost tzv. ulaznog toka ili struje nizova znakova. Ova je klasa objavljena u datoteci iostream.h . std::cin
>> je operator kopiranja niza znakova sa (iz) objekta cin u određenu memorijsku varijablu. Podaci se iz ulaznog toka, prihvaćenog s tastature, prenose preko memorijskog međuspremnika (engl. buffer), objavljenog i ostvarenog u datotekama iostream.h i iostream.cpp .
Imenski prostor. Gore spomenuti imenski prostor je važan dodatak jeziku C++ u odnosu na jezik C . Naime, zbog ogromnog broja imena koja se pridjeljuju varijablama, objektima, klasama i funkcijama, česta je pojava konflikta identifikatora istog naziva. Uvođenje imenskog prostora produljuje i *
Iza zatvorene vitičaste zagrade bloka, koja i sama služi kao terminator skupa tvrdnji, nije potrebno pisati znak točka-zarez. †
Ime objekta je tvoreno na uobičajeni način dodavanjem prefiksa koji ilustrira funkciju objekta.
‡
Od engl. in = hrv. u, unutar, „ulaz“.
c
nazivu
out
(engl. out = hrv. van, izlaz),
10
precizira identifikator, specificirajući koji je njegov dogled. Sintaksa za navođenje punog imena je sljedećeg oblika: namespaceIdentifier::entityIdentifier
gdje je namespaceIdentifier naziv, odnosno identifikator (vidi niže) imenskog prostora, a entityIdentifier je naziv programskog entiteta, npr. varijable ili funkcije. Dvostruka dvotočka :: je dogledni operator (engl. scope operator). On kaže da navedeni identifikator ima dogled u imenskom prostoru navedenom lijevo od njega. Ilustriramo li ovo na našem primjeru programa, jasno je da objekt cout i manipulator endl mi nismo definirali, već da oni moraju biti definirani negdje drugdje. Kao što je rečeno ranije, oni su definirani i kreirani u datoteci ostream. Njihovi pak su nazivi definirani unutar imenskog prostora std (od engl. standard), u kojem se objavljuju entiteti svih datoteka iz C++ standardne biblioteke, uključujući i nama važnu iostream datoteku. Ukoliko se često koriste entiteti iz nekog imenskog prostora, možemo uporabom ključne riječi skratiti pisanje njihovih imena ispuštanjem naziva imenskog prostora, kao što je ilustrirano u sljedećem primjeru. using
Primjer 3. Program „Zdravo svijete“ uz skraćeno pisanje vanjskih entiteta. Na početku programa u tvrdnji koja započinje ključnom riječi using specificiramo puno ime jezične tvorbe. Nju ćemo kasnije imenovati skraćeno, navođenjem samo imena entietata, ispuštajući naziv imenskog prostora. #include using std::cout; using std::endl;
// Pretprocesorska direktiva (uputa) // Navođenje punog imena objekta: std::cout // Navođenje punog imena manipulatora: std::endl
int main() // Glavna funkcija "main" { // program ispisuje znakovni niz (string): cout << "Zdravo svijete, iz C++ programa!" << endl; // Skraćeni zapis: // cout, endl return 0;
} Primjer 4. Program „Zdravo svijete“ uz korištenje cijelog imenskog prostora std. U praksi, pogotovo kod manjih programa, kad su i manji izgledi da će biti problema s konflikitima imena, uobičajeno je da se rabi cijeli imenski prostor povezan uz standardne biblioteke. Jasno je da se uključenjem svih varijabli nekog imenskog prostora umanjuje funkcionalnost cijelog koncepta, pa bi se prethodno rješenje trebalo favorizirati naspram ovoga. Međutim, zbog jednostavnosti uporabe, ovaj način pisanja se često nalazi u praksi. #include using namespace std;
// Pretprocesorska direktiva (uputa) // Rabi se cijeli imenski prostor! Svi njegovi enti// teti imaju dogled u svim niže navedenim funkcijama
int main() { // program ispisuje znakovni niz (string): cout << "Zdravo svijete, iz C++ programa!" << endl; // Skraćeni zapis: // cout, endl return 0;
}
11
Identifikatori. Prilikom pisanja programa koristimo tzv. «identifikator» (engl. identifier), koji predstavljaju imena (simbole) kojima nazivamo varijable, tipove podataka, funkcije i oznake (engl. label). Identifikator u C / C++ je proizvoljno dugački niz slova latinske abecede i brojevnih znamenaka, s time da prvi znak ne smije biti brojka. Prevodilac razaznaje znakove u identifikatoru kao znakove ASCII koda, i razlikuje velika i mala slova (engl. case sensitive, vidi primjere koji slijede). Identifikator mora biti jedinstven, dakle, različit od svih ostalih identifikatora korištenih u bloku tvrdnji, te od identifikatora globalnih varijabli (vidi kasnije), u uključenim bibliotekama. Donju crticu (engl. underscore) _ možemo koristiti kao slovo, dakle i na prvom mjestu. Napomenimo da se identifikatori koji počinju s jednom ili dvije donje crtice standardno koriste u bibliotečnim datotekama, pa ih je najbolje izbjegavati. Identifikator mora biti različit i od ključnih riječi rezerviranih za potrebe samog programskog jezika, kao npr: int, char, double, …, if, else, for, do, while, break (vidjeti na klizovima koji slijede). Primjeri ispravnih i međusobno različitih identifikatora: – OvoJeIdentifikator0, ovoJeIdentifikator0, ovoJeIdentifikator1 ; –
x1 , X1, y1, Y1, x_1, X_1, ….
Identifikatori ispravnog oblika čiju uporabu izbjegavamo zbog moguće kolizije s identifikatorima funkcija u standardnim C / C++ bibliotekama: 1. _x1 , __x1 , _y1 , __y1 , _123 , __123 , ... Neispravni identifikatori: 2. 0_Identifikator , 1xxx, 123__ , int , break , … Osnovno je pravilo da ime identifikatora odabiremo tako da sadrži čim više informacija o onom što imenuje. Prisjetite se latinske izreke: Nomen_est_omen ! Ako je bilo koji identifikator u programu nepoznat, tj. nije određen u samom programu i ne predstavlja ključnu riječ, prevodilac će provjeriti je li on odgovarajuće specificiran u datotekama uključenim direktivom #include. Ako jest, definicija tog identifikatora, odnosno njegova funkcional-nost, bit će uključena u izvršnu verziju programa u fazi povezivanja. Ako nije, prevodilac će dojaviti grešku: undeclared identifier. Ključne riječi. Ključne riječi (engl. keywords) su identifikatori rezervirani za potrebe samog programskog jezika C / C++ (u drugim jezicima koristi se i naziv rezervirane riječi, engl. reserved words). C ima 30-tak ključnih riječi, dok C++ ima 60-tak ključnih riječi, uključujući i one iz C. Tablica 2.1. Ključne riječi jezika C. auto
break
case
char
const
continue
default
do
double
else
enum
extern
float
for
goto
if
int
long
register
return
short
signed
(sizeof)
static
struct
switch
typedef
union
unsigned
void
volatile
while
12
Tablica 2.2. Dodatne ključne riječi jezika C++ . asm
bool
catch
class
const _cast
default
dynamic _cast
explicit
export
false
friend
inline
mutable
namespace
new
operator
private
protected
public
reinterpret _cast
static _cast
template
this
throw
true
try
typeid
typename
using
virtual
wchar_t
Stil pisanja programa. Kao što je već istaknuto u uvodnom odjeljku, stil pisanja programa je od velike važnosti. Programski kod mora biti pisan pregledno, s detaljnim i preciznim komentarima. Pošto se i sam jezik C / C++ zasniva na engleskom jeziku kao de-facto jeziku struke, profesionalni programeri pisat će komentare i davati nazive identifikatorima na tom istom jeziku. Strogo se mora voditi računa o uočljivosti pojedinih dijelova programa, što se postiže odgovarajućim uvlačenjem teksta (engl indentation). Vitičaste zagrade kojima se otvara i zatvara blok moraju biti jasno vidljive, a svaki novi, ugniježđeni blok jasno uvučen. Programerima početnicima se sugerira da pažljivo usvajaju dobre načine pisanja programskog koda na primjerima, te da razvijaju konzistentnost u skladu s uvriježenim, internacionalnim stilom. Suvremene programske okoline će u znatno pomoći u tome. U ovom tekstu sustavno se koristi tzv. mađarska notacija (engl. Hungarian Notation) kojom se identifikatori pišu na sustavan način te u sebi uključuju i podatak o svojem tipu, jednako kao i o svojem značenju, odnosno zadaći, što će biti objašnjeno na primjerima. Ova notacija uvriježena je u mnogim standardnim knjižnicama funkcija, i znatan je doprinos dobrom stilu programiranja. Na primjer nazovemo li neku funkciju slovom O , ne znamo puno o njoj, a ako je, u skladu s mađarskom notacijom, nazovemo fOpsegKruga,* odmah je jasno što ona radi, dok istovremeno slovo f sugerira da ona vraća realnu vrijednost tipa float (vidi kasnije).
*
Profesionalni programeri bi imena tvorili prema engleskom jeziku, kao de-facto jeziku struke.
13
Poglavlje 3.
Osnovnih tipovi podataka u C / C++
Uvod. Deklaracija i inicijalizacija varijabli. Da bi se podaci pohranili, odnosno kodirali u računalu na odgovarajući način, te da bi se s njima mogle vršiti odgovarajuće operacije, programski jezici posjeduju tipove podataka. Sve veličine s kojima radimo u programima, tzv. varijable, moraju imati pridijeljen tip podataka. Pored tipova koji su definirani u samom jeziku (u C/C++ npr. char, short int, int, unsigned int, float, double, …), programer može definirati i svoje tipove podataka. Poznavanje raspoloživih tipova i razumijevanje njihove valjane uporabe predstavlja osnovu programerskog znanja. U sljedećem primjeru uvodimo osnovne pojmove o tipovima podataka. Primjer 3.1 Napišite program u C++ koji učitava i ispisuje sljedeće tipove podataka: jedan cijeli broj, jedan realan broj i jedan znak. Također, program ispisuje veličinu korištene memorije u bajtovima (B) za svaki od tipova podataka, uporabom operatora sizeof . Na početku programa uporabom ključne riječi using , specificirana su puna imena objekata cin i cout, te ispisnog manipulatora endl , koja sadrže i naziv njihovog imenskog prostora std (od i primijetite da je, #include using std::cin; using std::cout; using std::endl;
// Navodi puno ime koje uključuje i tzv. imenski // prostor, i ime entiteta unutar njega, po ključu: // namespaceIdentifier::entityIdentifier . // Time se u daljnjem tekstu omogućuje kraći zapis, tj. // dovoljno je pisati samo ime jezičnog entiteta, u našem // slučaju: cin, cout, endl.
int main() { // Program učitava i ispisuje brojeve i pojedine znakove // Obavezna objava (deklaracija) varijabli: int a; float b; char c; // Unos vrijednosti varijabli: cout << "Unesite cijeli broj: a = "; cin >> a; cout << "Unesite realni broj: b = "; cin >> b; cout << "Unesite neki znak: c = "; cin >> c; // Novi redak u ispisu cout << endl; // Ispis cout << << <<
unešenih varijabli: "Cijeli broj ( int ) a = " << a << endl "Realan broj ( float ) b = " << b << endl " Znak ( char ) c = " << c << endl;
// Veličina podataka, odnosno količina memorije potrebna // za njihovu pohranu, dobiva se uporabom operatora sizeof . // Duljina tipa izražena je brojem bajtova (B), 1B = 8 bit.
14 // Novi redak, s pomoću kontrolnog znaka "\n" = '\n' = new line: cout << "\n"; // Duljine uporabljenih tipova : cout << "Duljina tipa int = " << sizeof a << "B" << "\n" << "Duljina tipa float = " << sizeof b << "B" << "\n" << "Duljina tipa char = " << sizeof c << "B" << "\n" << endl; }
return 0;
Umjesto prefiksnog operatora sizeof možemo koristiti i ekvivalentnu funkciju sizeof(varType). * Razlika je u tome što iza operatora mora stajati objavljena varijabla, a kod funkcije unutar okruglih zagrada može stajati bilo objavljena varijabla, bilo naziv standardnog C/C++ tipa podataka. U primjeru gornjeg programa deklarirane su tri varijable različitih tipova: cijeli broj ( int ), realan broj ( float ), i standardni C (ASCII) znak ( char ). Programski jezik C++ obiluje mnoštvom drugih tipova, a korisnik može uvoditi i vlastite tipove podataka. Deklaracijom varijable, ili objekta kao instance (predstavnika) klase, imenuje se i pridjeljuje (alocira) memorijski prostor u koji će biti pohranjena neka vrijednost. Tip varijable određuje kako će se podaci memorirati, raspon njenih dozvoljenih vrijednosti, te dozvoljene operacije nad tom varijablom (objektom). U jeziku C++ (C) tipiziranje podataka je obavezno i provodi se kroz deklaraciju (objavu) varijabli. Usklađenost varijabli, u smislu da se operacije vrše samo s istovjetnim tipovima, općeniti je programerski princip koji se treba poštivati neovisno od rabljenog jezika. Jezik C++ (C) zbog praktičnosti dozvoljava i neka «prirodna» odstupanja od tog pravila, tj. implicitno pridjeljivanje tipova (engl. implicit type-casting), o čemu će biti riječi kasnije. Implicitno pridjeljivanje treba svoditi na minimum, te stalno voditi računa o uporabi onih tipova koji najbolje odgovaraju podacima i rezultatima koje želimo prikazati.
Cjelobrojni tip podataka Zadatak 3.2 Objasnite značenje sljedeće tvrdnje, te navedite okvirno što prevodilac čini prilikom njenog prevođenja u zbirni jezik: int i, j, k;
Zadatak 3.3 U čemu je pogreška kod sljedećeg isječka programa: int i, j, k; // ... ... // int i = 0;
...
Zadatak 3.4 Komentirajte ispravnost, odnosno neispravnost sljedećih linija programskog koda. Objasnite što je sve obavljeno u istom retku. int int int int
*
i1 i2 i3 i4
= = = =
1; i1; i1 + i4; i2;
O C/C++ operatorima vidi odjeljak 2.8, a o funkcijama pogl. 6.
15
Zadatak 3.5 Provjerite kako vaš prevodilac interpretira cjelobrojne tipove short int i long int u odnosu na standardni tip int (tip long int se može i kratko pisati kao long). Kako ćete to učiniti? Naputak: sastavite mali program prema gornjim uzorima, te odgonetnite duljine s pomoću operatora ili funkcije sizeof. Napišite rezultate: Duljina tipa short int = Duljina tipa long int (long) =
B, B.
Komentar o interpretaciji cjelobrojnih tipova. Izvorno su u jeziku C postojala tri cjelobrojna tipa int, short int, long int ( long ). Interpretacija ovih tipova ovisila je o bitnosti računarske platforme koju predstavljaju procesor i korišteni operacijski sustav. Tako je za 16-bitnu platformu tip int bio 16-bitni cjelobrojni tip (dakle duljine 2B), short int je bio 8-bitni (1B), a long int 32-bitni cjelobrojni tip (nije se mogao obraditi u „jednom koraku“ rada procesora jer je ALU bila 16-bitna). Tip char je standardni tip za pohranu jednog ASCII znaka (prisjetite se značenja kratice ASCII i odgovorite što je to). Izvorno je ASCII 7-bitni kôd, no zbog toga što je bajt (oktet) standardna osnovna memorijska lokacija računala, proširen je u 8-bitni zapis uvođenjem vodeće nule. Drugim riječima, znakovi se pohranjuju kao 8-bitni cijeli brojevi koji se na razini zbirnog jezika (asemblera) najčešće označuju jednostavno kao tip Byte (B). Zaključujemo da za 16-bitne platforme short int i char predstavlja tip iste duljine — dakle istog načina pohrane u memoriji, a jedina je razlika bila formalne razlika u nazivima. Za danas standardne 32-bitne platforme interpretaciju tipova pokazali su rezultati prethodnog zadatka. Tipovi int i long int sada su istovjetni, a short int odgovara 16-bitnom cjelobrojnom podatku koji se na razini asemblera najčešće označava kao Word (W). Želimo li 8-bitni cjelobrojni podatak, preostaje nam da koristimo tip char (1B). Dosada navedeni cjelobrojni tipovi su tzv. predznačeni cjelobrojni tipovi, i s njima se vrši uobičajena predznačena aritmetika koja uključuje račun u podskupu skupa cijelih brojeva. Dakle svaki cjelobrojni tip podataka s n bitova predstavlja određeni podskup skupa Z cijelih brojeva: 53 Današnji procesori standardno kodiraju negativne brojeve s pomoću potpunog (dvojnog) komplementa (podsjetite se iz osnova informatike o čemu se radi). Za n–bitni cjelobrojni predznačeni tip, elementarna kombinatorika pokazuje da vrijedi:
N min = −2 n −1 , N max = 2 n −1 − 1 . Poznavanje opsega pojedinih tipova predstavlja temelj za njihovu svrsishodnu i racionalnu uporabu. Zadatak 3.6 Najprije pokušajte izvesti, ili barem objasniti gornju formulu, a potom je ilustrirati na jednostavnom cjelobrojnom tipu. Zatim sastavite tablicu svih predznačenih cjelobrojnih tipova i navedite najmanji i najveći broj u svakom od njih. Zadatak 3.7 Napišite kratke programe s kojima ćete testirati opseg brojeva za svaki pojedini cjelobrojni tip redom: char, short int i long int i pokazati valjanost gornjih formula. Poslužite se metodom «probe i pogreške», tj. isprobavajte i «griješite» sve dok niste sigurni u odgovor! Skica rješenja. Umjesto s najkraćim tipom char (kojeg će ulazno-izlazni objekti cin i cout uvijek interpretirati kao znak a ne kao broj, odnosno kao niz znamenaka broja u pozicionom brojevnom sustavu), počnimo razmatranje na tipu short int. U glavnu funkciju testnog programa umetnite sljedeće tvrdnje:
16 short int sX; cout << endl << "Input: cin >> sX ; cout <<
short int sX = ";
"Output: short int sX = " << sX << endl;
Upišite za ulazni podatak vrijednosti npr. 1000, 10000, 10000, –10000, –100000, pa zatim +32767, +32768, +32769, te –32767, –32768, –32769, i provjerite koji su rezultati ispravno zabilježeni u memoriju, te ispravno ispisani. Ponovite sličan program za tip int, te unašajte vrijednosti reda stotinu milijuna i milijardi: npr. 100000000, 1000000000, 2000000000, 4000000000, –2000000000, –4000000000, i zaključite koji je traženi opseg brojeva. Za slučaj tipa char, već smo rekli da će postojati problem interpretacije (predstavljanja) ovog tipa, iako je u memoriji on uskladišten kao 8-bitni broj. Operatori kopiranja >> i << će znakovnom tipu char jednostavno pridružiti odgovarajuću kodnu zamjenu prvog nailazećeg znaka ulaznoizlazno niza iz objekata cin, odnosno cout. Izričito pridjeljivanje tipova. Da prisilimo operatore da interpretiraju niz znakova kao niz znamenaka pozicionog brojevnog sustava, poslužit ćemo se svojstvom jezika C++ (C) da vrši izričito (prisilno) pridjeljivanje tipova (engl. explicit type casting). Za to se rabi operator prilagodbe ili nabačaja tipa (engl. type casting operator), koji se sastoji od imena tipa typeName napisanog unutar okruglih zagrada, ispred izraza kojem želimo promijeniti tip: typeName tVar1 = (typeName) expression;
Ovdje je izrazu expression nekog proizvoljnog tipa expressionType pridjeljen tip typeName, da ga uskladimo s tipom varijable tVar1. Funkcionalnost operatora prilagodbe tipova ostvarena je za sve parove standardnih C/C++ tipova podataka i za sve smjerove prilagodbe. Tako npr. možemo vršiti prilagodbu iz int u float (tj. uz expressionType = int, i typeName = float), te iz float u int (expressionType = float, i typeName = int). Za korisnički definirane tipove i klase (C++), korisnik mora funkcionalnost pretvorbe ostvariti sam. U sljedećem primjeru dan je koristan primjer izričite prilagodbe tipova. Primjer 3.8** Tip char želimo iskoristiti kao 8-bitni cjelobrojni tip. Da bismo ostvarili ispravan unos brojevnih vrijednosti u ovaj tip preko ulaznog objekta cin, poslužili se smo se cjelobrojnim tipom short int. Promotrite donji primjer i odgovorite na sljedeća pitanja: a) Koja je linija programa problematična? Objasnite! b) Pogledajte kako je ostvaren ispis sadržaja varijable, da dobijemo brojevnu vrijednost preko izlaznog objekta cout. c) Testirajte program za brojeve: 48d ( 30h ), 49d ( 31h ), … … , 57d ( 39h ), zatim za 64d ( 40h ), 65d ( 41h ), 66d ( 42h ), pa za 96d ( 60h ), 97d ( 61h ), 97d ( 63h ). Ovdje indeks (podskript) d označava broj zapisan u dekadskom sustavu, a podskript h broj zapisan u heksadekadskom sustavu. d) Testirajte program za brojeve: 100, –100, 200, –200, 127, –127, 128, –128, 127, 128, 129, itd… Izvedite zaključke i ponovite kako se zapisuju (kodiraju) pozitivni i negativni u memoriji (i registrima) računala. char i8bitX; // znakovni tip kao 8-bitna cjelobrojna varijabla short int isX; // 16-bitni cjelobrojni tip
*
Zadaci označeni zvjedicom nisu obavezni i namijenjeni su naprednijim studentima (ocjene 4 i 5). Studenti koji se prvi put susreću s programiranjem, mogu ove zadatke preskočiti u prvom čitanju, te se vratiti na njih kasnije.
17 cout << endl << "Input: i8bit = "; // Od korisnika tražimo unos dekadskog broja koji će biti // pospremljen u 8 bita cin >> isX ; // Da bi se unos ispravno «dekodirao» u binarni broj // koristimo varijablu tip short int i8bitX = isX; // vrijednost 16-bitne varijabla se pridjeljuje varijabli // manjeg 8-bitnog tipa, tzv. implicitna prilagodba. // Što se prilikom ovakve konverzije može desiti, te // što se zaista i dešava prilikom našeg testiranja? // Proizvoljna cjelobrojna aritmetika s tipom char i8bit = i8bit + 10 ; i8bit = i8bit – 10 ; // Kad varijablu i8bitX izravno ispišemo preko cout objekta, tada // umjesto interpretacije binarnog zapisa kao dekadskog broja, // dobivamo prikaz ASCII znaka odgovarajuće vrijednosti! // Još jedna konverzija: isX = i8bitX; cout << cout <<
"Output: i8bit = " << isX << endl; "Output: char = " << i8bitX << endl;
// Dodajte što je potrebno da se ispravno završi glavna funkcija.
Nakon što kompilirate gornji program, uočite što javlja prevodilac! Ako programer nije svjestan što radi, očito da je prevodilac dobro upozorio na moguću grešku. Objasnite to. Zatim umjesto problematične linije ubacite: i8bitX = (char) isX;
Na ovaj način se vrši kontrolirano pridjeljivanje tipa short int na tip char. Nakon što je prevodilac uočio da je programer namjerno izvršio «rizično» pridjeljivanje većeg tipa na manji, navedite kakva je poruka nakon kompilacije? Unašajte sada pozitivne brojeve npr. 10, 100, 200, pa –10, –100, –200, zatim 125, 127, 128, 129, –125, –127, –128, –129, itd… Pošto nam ovaj program ujedno omogućuje da vidimo kodne brojeve za znakove ASCI koda, odgonetnite kodne zamjene za neke ključne znakove, kao što su razmak ili praznina (engl. blank ili space), za slovo A, a, broj 0, 1, itd… Učeći jezik C++ (C), možemo mnogo naučiti o kodiranju brojeva i znakova, te o radu računala.
Nepredznačeni (unsigned) cjelobrojni tipovi Mnoge cjelobrojne veličine normalno ne poprimaju negativne vrijednosti, kao npr. indeksi u strukturi poretka (pogl. 6), adrese, itd… Za njih koristimo nepredznačeni cjelobrojni tip (engl. unsigned). Nep-
redznačeni cjelobrojni tip predstavlja podskup skupa N 0+ = N ∪ {0} . Uključenje nule je uvjetovano
prirodnim hardverskim razlozima (razmislite koji su to razlozi!), odnosno načinom kodiranja brojeva. Posljedica pak je brojanje koje kreće od 0, koje je uobičajeno za računarstvo. Stoga, sve veličine koje u matematici služe za pobrojavanje i indeksiranje, dakle one iz skupa prirodnih brojeva N, najbolje prikazujemo upravo nepredznačenim tipom, uz mogući posmak za jedan broj manje. Zbog nepostojanja negativnih brojeva, s istim brojem n bitova, nepredznačeni tip omogućuje prikaz otprilike dvostruko veće maksimalne vrijednosti od predznačenog tipa. Uz uobičajeni prirodni binarni kôd, skup brojeva prikazan nepredznačenim tipom je:
18
Punsigned = { 0 , 1, 2 , ... ... , N max − 1, N max } ⊂ N 0+ . gdje za n–bitni cjelobrojni nepredznačeni tip očito vrijedi:
N min = 0 , N max = 2 n − 1 . Nepredznačeni tip objavljujemo umetanjem rezervirane riječi unsigned ispred željenog cjelobrojnog tipa. Zadatak 3.9 U sljedećem programskom odsječku deklarirane su i inicijalizirane nepredznačene varijable. Odgonetnite njihovo stanje nakon naznačenih operacija. Prije nego što odgovorite na pitanja, potrebno je znati raspon predznačenog tipa, razumjeti kodiranje s pomoću dvojnog komplementa, odnosno svođenje oduzimanja na zbrajanje komplementa. Na kraju odsječka pojavljuje se pretvorba tipa unsigned int u int. Zaključite kako se ovi tipovi pohranjuju u memoriji tj. postoji li razlika u kodiranju? Postoji li razlika u interpretaciji sadržaja memorije? Tko obavlja kodiranje broja kao pripremu za pohranu u memoriju. Tko interpretira sadržaj memorije i na temelju čega? Dodatna pitanja: Što javlja prevodilac nakon prvog prevođenja programskog odsječka? Što bi trebalo učiniti? Napomena: mnogi korisni tipovi podataka dodani su u datoteci windows.h. Uključite ovu datoteku u program direktivom #include "windows.h" , i ponovite kompilaciju. unsigned unsigned unsigned unsigned
char uCh1 = 0; char uCh2 = +255; short int uSInt1 = 0; short int uSInt2 = 65535;
// UINT == skraćeno za 'unsigned int', // nije ključna riječ standardnih C/C++ kompajlera! UINT uInt1 = 0; UINT uInt2 = 4294967295; uCh1 uCh2 uSInt1 uSInt2 uInt1 uInt2
= = = = = =
// Prijelaz int iInt1 = int iInt2 = int iInt3 = int iInt4 = //
uCh1 – 1; uCh2 + 1; uSInt1 – 1; uSInt2 + 1; uInt1 – 1; uInt2 + 1;
// // // // // //
s UINT na int 4294967295; // 4294967294; // uInt1; // uInt2; //
uCh1 = ? uCh2 = ? uSInt1 = ? uSInt2 = ? uInt1 = ? uInt2 = ? iInt1 iInt2 iInt3 iInt4
= = = =
? ? ? ?
// Tvrdnje za ispis: // Ispis unsigned char varijabli cout << endl; cout << "uCh1 = " << ((unsigned short) uCh1) << "\n" << "uCh2 = " << ((unsigned short) uCh2) << "\n" << endl; // Osigurajte ispis preostalih varijabli: uSInt1, uSInt2, // uInt1, uInt2, iInt1, iInt2, iInt3, iInt4 : //////////////////////////////////////
Ukoliko niste sigurni u odgovore, umetnite gornji odsječak u glavnu funkciju testnog programa i provjerite što se događa.
19
Zadatak 3.10 Ispunite sljedeću tablicu za cjelobrojne tipove. Navedite njihov broj bitova i točan raspon brojeva. Iskoristite teorijske formule i provjere rješenja koje ste napravili na način opisan u zadatku 2.6. Zatim dovršite tablicu 2.1 raspona cjelobrojnih tipova. Tablica 2.1. Opsezi cjelobrojnih tipova podataka. Cjelobrojni Tip
Broj bitova
Predznačeni (signed)
Nepredznačeni (unsigned)
Nmin
Nmin
Nmax
Nmax
char short int int (long) long long
Konstante cjelobrojnih tipova.. Uobičajeno je da prevodioci imaju definirane bitne konstante koje se tiču npr. tipova podataka i drugih sustavskih odrednica. Tako se i granične vrijednosti cjelobrojnih tipova mogu pronaći u razvojnoj okolini Visual C++. Pronađite u izborniku Pomoć objašnjenje za «integer limits». Ili ako nemate instaliranu MSDN knjižnicu*, potražite u mapi gdje se nalazi Visual C++ (staza: Microsoft Visual Studio\VC98\Include) datoteku LIMITS.H i otvorite je. Ukoliko ste temeljito odradili prethodne vježbe, prepoznat ćete sve brojeve iz gornje tablice pod nazivima konstanti npr. SHORT_MIN, SHORT_MAX, INT_MIN, INT_MAX, UINT_MAX, itd…
Zapis cjelobrojnih tipova u različitim brojevnim sustavima Brojevi se u izvornom kodu mogu prikazati u dekadskom (podrazumijevajući odabir), te u oktalnom i heksadekadskom brojevnom sustavu. Zadatak 3.11 Napišite vrijednosti koje će biti ispisane na ekranu, a potom ih provjerite u konzolnoj aplikaciji. Obratite pažnju na brojevne sustave u kojima je varijabla zadana. Najprije zaključite općenito pravilo kako se zapisuje baza brojevnog sustava u istom tom sustavu. Objasnite potom kako je varijabla pohranjena u memoriji, te koji je standardni brojevni sustav za prikaz na ekranu. #include int main() { // Zapis brojeva u dekadskom, oktalnom i heksadekadskom // sustavu int iBDec = 10; // Što predstavlja broj iBDec? int iBOct = 010; // Što predstavlja broj iBOct? int iBHex = 0x10; // Što predstavlja broj iBHex? // Ispis vrijednosti baza sustava (u dekadskoj formi): cout << "iBDec = " << iBDec << endl;
*
MSDN knjižnica, od engl. Microsoft Developer Network Library®, je opsežna zbirka tekstova za poduku (engl. tutorials) , stručnih naputaka i komentara za razvoj programa u jeziku C++ (inačica Visual C++), Visual Basic-u, itd…
20 cout << "iBOct = " << iBOct << endl; cout << "iBHex = " << iBHex << endl; // Pridruživanje brojeva, N = 100 u raznim bazama: int iDec = 100; int iOct = 0100; int iHex = 0x100; / Ispis cout << cout << cout <<
dekadskog, oktalnog i "(100)Dec = " << iDec "(100)Oct = " << iOct "(100)Hex = " << iHex
heksadekadskog broja: << endl; << endl; << endl;
// Pridruživanje istog niza znamenaka: "777" u raznim bazama: iDec = 777; iOct = 0777; iHex = 0x777; // Ispis vrijednosti dekadskog, oktalnog i heksadekadskog broja // u dekadskoj formi: cout << "(777)Dec = " << iDec << endl; cout << "(777)Oct = " << iOct << endl; cout << "(777)Hex = " << iHex << endl; // Ispis istog broja // (uporaba izlaznog cout << "0x100 (Hex) cout << "0x100 (Oct) cout << "0x100 (Dec)
u različitim sustavima manipulatora, vidi kasnije): = " << hex << 0x100 << endl; = " << oct << 0x100 << endl; = " << dec << 0x100 << endl;
return 0; }
Zadatak 3.12* Zadatak u svezi s radom prevodioca. Prilikom deklaracije varijable bez njezine inicijalizacije, prevodilac mora rezervirati potreban broj bajtova ovisan o duljini tipa varijable (a i drugim detaljima vezanim uz rad prevodioca i platforme računala za koju je prevodilac namijenjen). Postavlja se pitanje ostavlja li prevodilac rezervirane bajtove s vrijednostima kakve su bile (slučajne, neodređene vrijednsti), ili ih možda postavlja na neku specifičnu vrijednost.. Sljedeći programski odsječak nudi mogućnost odgovora na to pitanje. Zabilježite rezultate ispisa. a) Pođite od dobivene vrijednosti za 16-bitnu nepredznačenu varijablu uS. Prevedite dobiveni broj u heksadekadsku formu. Zatim to isto ponovite i za varijablu uI. Što primjećujete? b) Kako ćete objasniti rezultat za varijablu c tipa char, koja je interno predstavljena kao 8-bitni cijeli broj? Podsjetite se da su u negativni brojevi u memoriji prikazani kao potpuni (u binarnom sustavu to je tzv. «dvojni») komplement apsolutne vrijednsoti broja. Prevedite apsolutni iznos dobivenog broja u binarni oblik, komplementirajte ga i prevedite u heksadekadsku formu. Ili, dobiveni negativni broj dodajte modulu 8-bitne aritmetike, tj. broju 28 = 256, pa njega prevedite u heksadekadski oblik. Potvrdite zaključak naveden u komentaru na dnu programskog odsječka! c) Ponovite razmatranje za varijable predznačenog tipa. d) Razmislite zašto konstruktori prevodioca nisu odlučili da se prilikom rezervacije bajtova za lokalne varijable svi bajtovi postave na vrijednost 00h . Napomena: ovaj zaključak ne vrijedi za polja (nizove)! Elementi polja ostaju u potpunosti nespecificirani ako nije provedena barem djelomična inicijalizacija (vidi pogl. 6). *
Primjeri i zadaci, te odjeljci teksta koji su označeni zvjezdicom (*) iza naslova i podnaslova, predstavljaju nešto teže gradivo namijenjeno naprednijim i ambicioznijim studentima. Ovi dijelovi se u pravilu mogu preskočiti u prvom čitanju.
21 // Deklaracija varijabli bez inicijalizacije: char c; unsigned short int uS; unsigned int uI; // Ispis: cout << "Tip char, 1B, c = " << (short int) c <<"\n" << "Tip short int, 2B, uS = " << uS << "\n" << "Tip int, 4B, uI = " << uI << "\n" << endl; ////////////////////////////////////////////////////////////////// // Zaključak: bajtovi rezervirani od strane prevodioca // // postavljaju se na vrijednost 0xCC = 11001100b // //////////////////////////////////////////////////////////////////
Znakovni tip (char), pridruživanje znakovnih vrijednosti Znakovni tip smo već razmotrili kao 8-bitni cjelobrojni tip. Jedina njegova posebnost u odnosu na uobičajeni cjelobrojni tip je način njegovog kodiranja (prevođenja) u binarni oblik prilikom unosa, te interpretacije njegovog cjelobrojnog sadržaja kao standardnog ASCII znaka prilikom ispisa od strane ulazno-izlaznih objekata cin, cout, te drugih ulazno-izlaznih tvrdnji. Inicijalizacija znakovne varijable se vrši pridruživanjem znaka z unutar jednostrukih navodnika (apostrofa), npr. char c = 'z'. Pošto se u osnovi radi o cjelobrojnom tipu, možemo primjenjivati sve operacije kao i na ostalim cjelobrojnim tipovima. Zadatak 3.12 Razmotrite sljedeći programski odsječak. Predvidite izlaz ako je kodna zamjena za znak praznine: ASCII(' ') = 20h , znamenka 0: ASCII ('0') = 30h , znak @: ASCII ('@') = 40h , slovo A: ASCII ('A') = 41h , slovo a: ASCII ('a') = 61h , itd. (vidjeti tablicu ASCII koda u dodatku). Ovdje smo indeksom h označili da se radi o heksadekadskom prikazu broja. char cSpace = ' ', cA = 'A', cX1, cX2, cX3, cX4; unsigned short int uInt1, uInt2, uInt3; // Račun s tipom char (8-bitni): cX1 cX2 cX3 cX4
= = = =
cSpace cSpace cSpace cSpace
+ 1; + 10; + 0x10; + 0x20;
// // // //
cX1 cX2 cX3 cX3
= = = =
? ? ? ?
// Ispis (znakovi ili njihove kodne zamjene?): cout cout cout cout
<< << << <<
"' "' "' "'
' ' ' '
+ 1 = + 10 = + 10h = + 20h =
" " " "
<< << << <<
cX1 cX2 cX3 cX4
<< << << <<
endl; endl; endl; endl;
// Podrazumijevajuća prilagodba (promocija) sa char (8-bitni) // na short int (16 bitni): uInt1 = cSpace + 1; // uInt1 = ? uInt2 = cSpace + 10; // uInt2 = ? uInt3 = cSpace + 0x10; // uInt3 = ? // Ispis (znakovi ili njihove kodne zamjene?):
22 cout << "ASCII( ' ' + 1) = " << uInt1 << endl; cout << "ASCII( ' ' + 10) = " << uInt2 << endl; cout << "ASCII( ' ' + 10h) = " << uInt3 << endl; // Izravan izlaz na objekt cout! // Ispis – znakovi ili njihove kodne zamjene? cout cout cout cout
<< << << <<
"' "' "' "'
' ' ' '
+ + + +
10h 11h 12h 19h
= = = =
" " " "
<< << << <<
cSpace cSpace cSpace cSpace
+ + + +
0x10 0x11 0x12 0x19
<< << << <<
endl; endl; endl; endl;
// Izravan izlaz na objekt cout, uz prisilno pridjeljivanje // (down-casting) na niži tip char. // ASCII brojke: cout cout cout cout
<< << << <<
"' "' "' "'
' ' ' '
+ + + +
10h 11h 12h 19h
= = = =
" " " "
<< << << <<
(char) (char) (char) (char)
(' (' (' ('
' ' ' '
+ + + +
0x10) 0x11) 0x12) 0x19)
<< << << <<
endl; endl; endl; endl;
// Izravan izlaz na objekt cout, uz odgovarajuće pridjeljivanje tipa. // ASCII slova: cout cout cout cout
<< << << <<
"'A' "'A' "'A' "'A'
+ 1 + 25 + 20h + 57
= = = =
" " " "
<< << << <<
(char) (char) (char) (char)
(cA (cA (cA (cA
+ 1) << + 25) << + 0x20) << + 57) <<
endl; endl; endl; endl;
////////////////////////////////////////////////////
Široki znakovni tip WCHAR (engl. wide character). Potreba za višejezičnim tekstom u računarstvu inicirala je uvođenje novog 16-bitnog koda s 216 = 65536 kodnih zamjena, pod nazivom UNICODE (akronim od engl. Universal Code). Namjena ovog koda je da obuhvati većinu znakova koji su u uporabi u aktualnim svjetskim pismima, od latinice, ćirilice, hebrejskog i arapskog pisma, kineskog i japanskog pisma, obilja posebnih znakova itd. Unicode je konstruiran tako da mu je ASCII podskup, i da predstavlja prvih 128 kodnih riječi. Znakovni tip WCHAR je uveden na Win32 platformi* kao podrška UNICODE-u. Za rad s ovim tipom postoji niz C/C++ funkcija, analognih onima za rad s tipom char (vidi pogl. XX).
Tipovi podataka s pomičnom točkom: float i double Kod cjelobrojnih tipova podataka, položaj decimalne (racionalne) točke† podrazumijeva se da je fiksan, tj. zamišljena točka je desno od najmanje značajne znamenke cijelog broja i ne piše se. Zbog čvrstog položaja racionalne točke se cjelobrojni tipovi ubrajaju u općenitiji tip s tzv. čvrstom točkom (engl. fixed point type).
*
Win32 = Windows 32-bitna platforma. Računarska platforma općenito predstavlja spoj procesora, računala u cijelosti, i operacijskog sustava kao osnovice za učinkovito izvođenje korisničkih programa. U ovom slučaju se radi o platformi baziranoj na naprednim x86 32-bitnim Intelovim procesorima (Pentium), uz uporabu Microsoft© Windows operacijskog sustava. †
Kod nas i u većem dijelu Europe se kod zapisa «decimalnih», odnosno, općenito razlomljenih brojeva (za bazu sustava koja nije dekadska), koristi zarez. U anglo-američkim zemljama se umjesto zareza koristi točka, što je postalo standard i u mnogim prirodnim i tehničkim područjima, pa i u računarstvu. Engl. naziv za racionalnu točku (znak) je radix point (character).
23
Za prikaz brojeva koji, matematički gledano, pripadaju skupu R realnih brojeva, a u praksi prikazujemo u obličju decimalnih brojeva sa zarezom (u anglo-američkom zapisu s točkom, kao što je napomenuto u fusnoti), koristimo tip podataka s pomičnom točkom (engl. floating point type). Prirodno je da sve veličine koje odražavaju realne fizikalne dimenzije, i sve one veličine koje zamišljamo da mogu poprimiti «kontinuirane» vrijednosti, prikazujemo ovim tipom. Kod ovih tipova se, po analogiji sa znanstvenom notacijom brojeva, posebno zapisuje mantisa (odnosno tzv. signifikand) broja, i posebno eksponent brojevne baze prema propisanom formatu. U tipove podataka s pomičnom točkom za zapis realnih brojeva spadaju tipovi s pomičnom točkom (zarezom) float (od engl. naziva: single precision floating point type) kodiran u 32 bita, i double (od engl. naziva: double precision floating point) kodiran u 64 bita (standard IEEE 754). Tip float omogućuje preciznost od 7 dekadskih znamenaka (tzv. signifikantne, ili značajne znamenke), uz standardni raspon apsolutnih vrijednosti od 1.175494 × 10-38 do 3.402824× 1038. U znanstvenoj uporabi standardni je tip double, koji garantira točnost mantise od 15 dekadskih znamenaka i standardni raspon apsolutnih vrijednosti od 2.2250739 × 10-308 do 1.7976931 × 10308. Tako se i u jeziku C++ konstanta zapisana kao broj s decimalnom točkom podrazumijeva kao tip double. Ukoliko želimo zapisati konstantu tipa float tada iza broja stavljamo dometak (sufiks) f (ili F). Primjer 3.13 U sljedećem primjeru vidljiva je sintaksa zapisa realnih brojeva u tipu double i float. float float float float
fR1 = 1.0E-10f ; // = 0.0000000001 fR2 = -123.4567E6f ; // = -1.234567 x 108 = 123 456 700 fRazlomak_1kroz16 = 6.25E-2f ; // 1/16 u točnom zapisu fRazlomak_1kroz3 = 0.333333 ; // 1/3 , točno ili približno?
//Matematičke konstante const double dPi = 3.14159265358979 ; const double
// Ludolfov broj Pi s 15 // značajnih znamenaka de = 2.71828182845904 ; // Baza prirodnog // logaritma
//Neke prirodne konstante: const double dEl = -1.6E-19; // Naboj elektrona const double dAn = 6.023e23; // Avogadrov broj const double dG = 6.67e-11; // Gravitaciona konstanta
Ovdje smo s ključnom riječi const modificirali objavu varijabli i odredili da se radi o konstantnoj veličini koja se ne može mijenjati. Zadatak 3.14 Napišite program koji gore objavljene i određene varijable ispisuje na pregledan način. Zadatak 3.15 Proanalizirajte sljedeći programski odsječak i diskutirajte formalnu korektnost. Hoće li će prevođenje proći? Što će javiti prevodilac, te hoće li se program izvršiti? Provjerite! float float float double float cout
fA fB fC dD fE
<< << << << <<
= = = = =
0.123456 ; 0.123456f ; 1. ; fA + fB ; fC + dD ;
"fA "fB "fC "dD "fE
= = = = =
" << " << " << fA + fC +
fA fB fC fB dD
<< "\t" << "\t" << "\t" << "\n\n" = " << dD << "\n" = " << fE << "\n"
24 << endl;
Zadatak 3.16 Provjera nužnog i dovoljnog broja znamenaka za potpuno i precizno definiranje tipa float. Promatramo decimalni zapis broja 1/3. Pokušajte okvirno predvidjeti rezultate ispisa najprije za prve četiri varijable. Provjerite rezultate ispisa i diskutirajte ih, te izvedite zaključke o potrebnom i dovoljnom broju znamenaka za potpuno i precizno definiranje varijable tipa float. Na temelju ispisa druge skupine od po četiri varijable izvedite zaključak o tome koje dekadske znamenke tretiramo kao signifikantne u slučaju brojeva manjih od 1. Ima li to veze s nazivom ”floating point” tipa? float float float float
f0point3_6z f0point3_7z f0point3_8z f0point3_9z
= = = =
0.333333f; 0.3333333f; 0.33333333f; 0.333333333f;
cout << "0.333 333 3f f0point3_7z cout << "0.333 333 33f f0point3_8z cout << "0.333 333 333f f0point3_9z cout << "0.333 333 333f f0point3_9z float float float float
f0point0003_6z f0point0003_7z f0point0003_8z f0point0003_9z
= = = =
0.333 333 = " - f0point3_6z << 0.333 333 3 = " - f0point3_7z << 0.333 333 33 = " - f0point3_8z << 0.333 333 3 = " - f0point3_7z <<
<< endl; << endl; << endl; << "\n\n";
0.000333333f; 0.0003333333f; 0.00033333333f; 0.000333333333f;
cout << "0.000 333 333 3f - 0.333 333 = f0point0003_7z - f0point0003_6z << cout << "0.000 333 333 33f - 0.000 333 333 3 = f0point0003_8z - f0point0003_7z << cout << "0.000 333 333 333f - 0.000 333 333 33 = f0point0003_9z - f0point0003_8z << cout << "0.000 333 333 333f - 0.000 333 333 3 = f0point0003_9z - f0point0003_7z <<
" << endl; " << endl; " << endl; " << "\n\n";
Zadatak 3.17 Po uzoru na prethodni zadatak, izvedite zaključke o potrebnom i dovoljnom broju znamenaka za potpuno precizno definiranje tipa double. Odgovorite na sva pitanja kao u prethodnom zadatku. Zadatak 3.18* Na temelju sljedećeg programskog odsječka izvedite zaključke o broju signifikantnih (značajnih, odnosno točnih) znamenaka tipa float. Pored ispisa preko objekta cout koristimo i funkciju printf za ispis kontroliranog formata (obličja) preko objekta stdout. a) Nakon prve provjere ispisa obratite pažnju na redoslijed ispisanih poruka i varijabli. Što je pogrešno? Razmislite što bi moglo biti uzrok tome? Zatim zamijenite zadnji red ispisa varijabli preko cout (ispred komentara «// Drugi ispis… ») s dijelom koda ispod njega koji je «iskomentiran». Što zaključujete? Jesu li prema tome tzv. manipulator izlaza endl i specijalni znak \n istovjetni ili nisu? Pokušajte objasniti i izvesti pravilo o uporabi ispisa preko objekta cout. Napomena: poslužite se pomoćnim izbornikom («help») iz MSDN knjižnice u programskoj okolini Visual Studio 6.0 da doznate više o tome. b) Izvedite zaključke o ispisu tipa float preko izlaznog objekta cout i njemu pridruženog operatora <<, te preko izlaznog objekta stdout i funkcije printf.
25
Napomena o printf(): Format se za funkciju printf uvijek specificira unutar niza znakova (stringa), a neposredno poslije oznake % za mjesto na kojem će se pojaviti varijabla. Prije znaka % može se navesti željena poruka koja će prethoditi ispisu varijable. Varijable se navode u listi iza završenog niza kojim se specificira format. Obratite pažnju — smije li se niz znakova (C-string) unutar znakova "" prelamati u novi red? Objasnite! Napomena o ispisu preko cout: ovaj ispis se također može formatirati, tj. njegov oblik prilagoditi željenom obliku i tipu podataka, ali uz nešto manju fleksibilnost (vidi komentar iza zadatka 2.20). c) Konačno, izvedite zaključke o broju signifikantnih znamenaka. Početna vrijednost konstante fR1 neka bude kao što je napisano: 1.0. Zatim je promijenite da bude 2.0. Što primjećujete u odnosu na početnu vrijednost. Zatim provjerite rezultate za fR1 = 4.0 i 8.0. Usporedite konačne rezultate s teorijskim vrijednostima za točnost ovog tipa. // Tip float: float fR1
= 1.0f;
float float float float
fR1_1Eminus5 fR1_1Eminus6 fR1_1Eminus7 fR1_1Eminus8
= = = =
fR1 fR1 fR1 fR1
-
1.0e-5f; 1.0e-6f; 1.0e-7f; 1.0e-8f;
float float float float
fR1_1Eplus5 fR1_1Eplus6 fR1_1Eplus7 fR1_1Eplus8
= = = =
fR1 fR1 fR1 fR1
+ + + +
1.0e-5f; 1.0e-6f; 1.0e-7f; 1.0e-8f;
// Prvi ispis (provjera toènosti) preko "cout": cout << "Tip float\n" << "===============================\n\n" << "Ispis preko cout:\n\n"; cout << fR1 << << fR1 << "\t << fR1 << "\t << fR1 << "\t
"\t - 1.0e-5f = - 1.0e-6f = " << - 1.0e-7f = " << - 1.0e-8f = " <<
" << fR1_1Eminus5 << "\n" fR1_1Eminus6 << "\n" fR1_1Eminus7 << "\n" fR1_1Eminus8 << "\n\n";
cout << fR1 << << fR1 << "\t << fR1 << "\t << fR1 << "\t << "\n"; // << endl;
"\t + 1.0e-5f = + 1.0e-6f = " << + 1.0e-7f = " << + 1.0e-8f = " <<
" << fR1_1Eplus5 << "\n" fR1_1Eplus6 << "\n" fR1_1Eplus7 << "\n" fR1_1Eplus8 << "\n"
// Drugi ispis funkcijom printf() preko "stdout" -printf ("Ispis preko stdout:\n"); printf("\n%.7f printf("\n%.7f printf("\n%.7f printf("\n%.7f
-
1.0e-5f 1.0e-6f 1.0e-7f 1.0e-8f
= = = =
%.7f", %.7f", %.7f", %.7f",
fR1, fR1, fR1, fR1,
fR1_1Eminus5); fR1_1Eminus6); fR1_1Eminus7); fR1_1Eminus8);
+ + + +
1.0e-5f 1.0e-6f 1.0e-7f 1.0e-8f
= = = =
%.7f", %.7f", %.7f", %.7f",
fR1, fR1, fR1, fR1,
fR1_1Eplus5); fR1_1Eplus6); fR1_1Eplus7); fR1_1Eplus8);
cout << endl; printf("\n%.7f printf("\n%.7f printf("\n%.7f printf("\n%.7f
26 cout << "\n" << endl; // Kraj programskog odsječka
Zadatak 3.19* Ponovite gornji zadatak i izvedite zaključke o broju signifikantnih znamenaka tipa double. a) Što sada uočavate za ispisa preko objekta cout u odnosu na ispis uz pomoć funkcije printf preko objekta stdout? b) Usporedite način ispisa formata i pripadnih varijabli ovdje i u prethodnom primjeru.. c) Po uzoru na prethodni zadatak, provedite razmatranje za različite vrijednosti dR1, u to ovaj put redom od 1.0, 2.0, 3.0 do 9.0. Izvedite zaključke i usporedite ih s teorijskim vrijednostima. // Tip double: double dR1
= 1.0;
double double double double
dR1_1Eminus13 dR1_1Eminus14 dR1_1Eminus15 dR1_1Eminus16
= = = =
dR1 dR1 dR1 dR1
-
1.0e-13; 1.0e-14; 1.0e-15; 1.0e-16;
double double double double
dR1_1Eplus13 dR1_1Eplus14 dR1_1Eplus15 dR1_1Eplus16
= = = =
dR1 dR1 dR1 dR1
+ + + +
1.0e-13; 1.0e-14; 1.0e-15; 1.0e-16;
// Ispis (provjera točnosti) preko "cout": cout << "Tip double\n" << "===============================\n\n" << "Ispis preko cout:\n\n"; cout << dR1 << "\t - 1.0e-13 = << dR1 << "\t - 1.0e-14 = " << << dR1 << "\t - 1.0e-15 = " << << dR1 << "\t - 1.0e-16 = " <<
" << dR1_1Eminus13 << "\n" dR1_1Eminus14 << "\n" dR1_1Eminus15 << "\n" dR1_1Eminus16 << "\n\n";
cout << dR1 << "\t + 1.0e-13 = << dR1 << "\t + 1.0e-14 = " << << dR1 << "\t + 1.0e-15 = " << << dR1 << "\t + 1.0e-16 = " << << endl;
" << dR1_1Eplus13 << "\n" dR1_1Eplus14 << "\n" dR1_1Eplus15 << "\n" dR1_1Eplus16 << "\n"
cout << "Gdje je očekivanih 15 signifikantnih znamenaka?):" << endl; // Drugi ispis funkcijom printf() preko "stdout": printf ("\nIspis preko stdout:\n\n"); printf("\n%.15f - 1.0e-13f = %.15f", dR1, dR1_1Eminus13); printf("\n%.15f - 1.0e-14f = %.15f", dR1, dR1_1Eminus14); printf("\n%.15f - 1.0e-15f = %.15f", dR1, dR1_1Eminus15); printf("\n%.15f - 1.0e-16f = %.15f", dR1, dR1_1Eminus16); cout << endl; printf("\n%.15f printf("\n%.15f printf("\n%.15f printf("\n%.15f
+ + + +
1.0e-13f 1.0e-14f 1.0e-15f 1.0e-16f
cout << "\n\n" << endl;
= = = =
%.15f", %.15f", %.15f", %.15f",
dR1, dR1, dR1, dR1,
dR1_1Eplus13); dR1_1Eplus14); dR1_1Eplus15); dR1_1Eplus16);
27 // Kraj programskog odsječka
Zadatak 3.20* Provjerite programski odsječak u kojem se razlomak prikazuje varijablom tipa float. Odgovorite je li to matematički egzaktan zapis razlomka? Može li se uopće vrijednost 1/3 točno zapisati, strogo uzevši? Predvidite rezultate kompilacije i ispisa, te detaljno objasnite svaki od tri slučaja prikaza razlomka. Koji će biti rezultat zadnjeg ispisa? Možete li odgovoriti na pitanje odakle proizlazi ispravno zaokruživanje? Što biste morali bolje znati da precizno odgovorite na to pitanje? Provjerite. float fRazlomak_1kroz3; // Razlomak 1/3 , I. put: fRazlomak_1kroz3 = 1 / 3 ; // Kakvo je ovo dijeljenje? cout << " 1/3 = " << fRazlomak_1kroz3 << endl; cout << "(1/3)*3 = " << fRazlomak_1kroz3 * 3.0f << "\n\n"; // Razlomak 1/3 , II. put: fRazlomak_1kroz3 = 1 / 3. ; cout << "1/3. = " << fRazlomak_1kroz3 << endl; cout << "(1/3)*3 = " << fRazlomak_1kroz3 * 3.0f << "\n\n"; // Razlomak 1/3 III put: fRazlomak_1kroz3 = 1.f / 3.f ; cout << "1.f/3.f = " << fRazlomak_1kroz3 << endl; cout << "(1/3)*3 = " << fRazlomak_1kroz3 * 3.0f << "\n\n"; // Provjera ispravnog zaokruživanja: cout << "Je li rezultat (1/3)*3 = 1 mat. egzaktan:\n\n"; cout << " 1.0 0.3333333*3.0 = " << 1.0 - 0.3333333f*3.0f << endl; cout << "(1/3.)*3. - 0.3333333*3.0 = " << fRazlomak_1kroz3*3.0f - 0.3333333f*3.0f << "\n\n";
Rukovanje ulazno-izlaznim tokovima (strujama).. Ulazno izlazne objekte cin i cout možemo modificirati s pomoću posebnih funkcija koje se nazivaju rukovaoci ili manipulatori (engl. manipulators), deklarirani u zaglavnoj datoteci iomanip.h. Kao koristan primjer navodimo rukovaoce za promjenu brojevne baze ispisa cjelobrojnih tipova preko cout objekta. S pomoću manipulatora hex i oct podrazumijevajuću dekadsku bazu mijenjamo u heksadekadsku, odnosno oktalnu, a za povratak na dekadsku bazu koristimo manipulator dec: #include ... ... cout << hex << var1 ; cout << oct << var1 ; cout << dec << var1 ;
// ispis var1 u heksadekadskoj formi // ispis var1 u oktalnoj formi // ispis var1 u dekadskoj formi
Potrebno je naglasiti da rukovaoci hex i oct daju doslovni ispis memorije (engl. memory dump), pa su npr. negativni brojevi prikazani u formi potpunog komplementa. Očito je da će upravo heksadekadski ispi biti pogodan za pregled sadržaja memorije. S pomoću rukovaoca setprecision(int n) možemo odabrati točnost ispisa tipova s pomičnom točkom na n značajnih znamenaka na sljedeći način: cout << setprecision(n);
Postavljena preciznost vrijedi sve dok se ne promijeni. Podrazumijevajuća preciznost je 6 značajnih znamenaka. Za detalje i opis ostalih ostalim rukovaoca iostream klasa pogledajte MSDN knjižnicu, unoseći za indeks ključne riječi iostream, te iomanip.
28
Zadatak 3.21 Neka dva takmičara trče po zamišljenoj atletskoj stazi koja se poklapa za Zemljinom stazom oko Sunca koja je približno kružnica radijusa r = 150 milijuna km. Trkači trče bok uz bok, tako da onaj u vanjskoj stazi trči po kružnici koja ima radijus za 1m veći od r. Napišite program u C++ koji će izračunati i ispisati kolika je razlika u duljini staze koju pretrče trkači, ukoliko vanjski trkač trči u stazi radijusa n metar većoj od onog u unutarnjoj. Odgovorite redom na sljedeća pitanja: a) Pokušajte najprije bez računa odrediti, je li ta razlika «velika» ili «mala»? Objasnite! b) Analizom podataka, odredite koji ćete tip podataka rabiti za radijus i za opseg? c) Koju, i koliko točno definiranu matematičku konstantu ćete rabiti? d) Može li se točan rezultat dobiti uporabom tipa float. c) Gornji zadatak uzet je iz poznatog ruskog astronomskog udžbenika za 1. razred gimnazije (autor Perelman). Razmislite što treba prethoditi svakom pisanju programa. Specifično, što treba prethoditi pisanju svakog programa koji se odnosi na matematički problem? Napomena: Rješenje može biti točno do na granicu proizvoljno odabranog tipa.
Logički tip Pošto se sve operacije na računalu svode ili mogu svesti na logičke operacije, možemo reći da je logički tip bazični tip podataka (prisjetite se logike sudova koje učite u matematičkim kolegijima, i logičkih funkcija koje ostvaruju digitalni sklopovi u računalima). Za pohranu logičkog tip podataka dovoljan je jedan bit, čime će se zabilježiti vrijednost FALSE (laž) i TRUE (istina). S druge strane, pošto se iz memorije računala standardno ne dohvaća bit po bit, u programskim jezicima se logički tip realizira uglavnom korištenjem (kratkih) cjelobrojnih tipova. Tako izvorni jezik C ne poznaje strogi logički tip u smislu drugih, formalno strožih jezika (kao npr. Pascal). Njegov logički tip svodi se na cjelobrojni tip, tako da se vrijednost 0 interpretira kao FALSE, a sve ostale cjelobrojne vrijednosti kao TRUE (dakle ne samo 1, kao što bismo očekivali). Jezik C++ uvodi tip bool i definiraju se logičke konstante false i true, ali je rad s ovim tipom u suštini nepromijenjen u odnosu na jezik C (vidi donji primjer). Za logičke konstante vrijedi tzv. cjelobrojna promocija (engl. integral promotion) po kojoj se vrijednost false promovira u 0, a true u 1. Veličina cjelobrojnog tipa na koji se svodi tip bool ovisi o prevodiocu. Ranije inačice Microsoftovih prevodioca poistovjećivale su ga s tipom int, a rješavanjem primjera koji slijedi odgonetnut ćete veličinu na Visual Studiu 6.0 (vrijedi od inačice 5.0). Nadalje, u sklopu MFC biblioteke (engl. Microsoft Foundation Class Library), definiran je tip BOOL (pisano velikim slovima) kao formalno strogi logički tip sa samo dvije vrijednosti (FALSE i TRUE), pa ga je uputno koristiti uvijek kad se koristi MFC biblioteka (vrijedi i za Windows Software Development Kit (SDK)). Primjer 3.22 U sljedećem programskom odsječku deklarirane su dvije logičke varijable. Varijablama se pridjeljuje vrijednost na nekoliko načina (pridruživanjem vrijednosti logičkih konstanti false i true, te cijelih brojeva). Također, provjeravamo duljinu logičkog tipa. a) Predvidite koje će vrijednosti biti ispisane na ekranu. b) Ustanovite koje pravilo vrijedi za pridjeljivanje cjelobrojnog tipa na tip bool. c) Je li opravdano ignorirati upozorenja prevodioca? Predstavlja li strogo vođenje računa o tipu podataka kao što je logički, tek puki formalizam? Ako jezik dopušta određene slobode u radu s tipovima, na kome preostaje da vodi računa o njihovoj pravilnoj uporabi? Napomena: ukoliko vam komentari na engleskom jeziku nisu razumljivi, prevedite ključne riječi s pomoću rječnika, i zatim dopišite komentare u hrvatskoj inačici.
29 // Logic Variables declaration bool bLogic0; bool bLogic1; // Size cout << << << <<
of bool type: "Size of bLogic0 = " << sizeof bLogic0 << "B\n" "Size of bLogic1 = " << sizeof bLogic1 << "B\n" "Size of (bool) = " << sizeof(bool) << "B\n" endl;
// Formally correct initialization: bLogic0 = false; bLogic1 = true; // Output values: cout << "bLogic0 [= true] = " << bLogic0 << "\n" << "bLogic1 [= false] = " << bLogic1 << "\n" << endl; // Formally incorrect initialization (automatic casting). // Will there be compile-time warnings? bLogic0 = 0; bLogic1 = 1; // Output values: cout << "bLogic0 [= 0 ] = " << bLogic0 << "\n" << "bLogic1 [= 1 ] = " << bLogic1 << "\n" << endl; // Formally incorrect initialization (truncation from // int to bool). Will there be compile-time warnings? bLogic0 = 256; bLogic1 = 65536; // Output values through bool type: cout << "bLogic0 [= -256] = " << bLogic0 << "\n" << "bLogic1 [= 65536] = " << bLogic1 << "\n" << endl; // Formally incorrect initialization (truncation from // int to bool). Will there be compile-time warnings? bLogic0 = 0; bLogic1 = 65535; // Output values through short int type: cout << "(short) bLogic0 [= 0] = " << (short) bLogic0 << "\n" << "(short) bLogic1 [= 65535] = " << (short) bLogic1 << "\n" << endl; // ... ... ...
Pobrojani tipovi (enumerated types) Jednostavni tip podataka u C/C++ kojeg korisnik prilagođava svojoj potrebi je pobrojani tip (engl. enumerated data type). U sljedećem primjeru je definirani pobrojani tipovi mjesec i month: enum mjesec {Sij, Velj, Ozu, Trv, Svb, Lip, Srp, Kol, Ruj, Lis, Stu, Pro}; enum month
{Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec};
Kod pobrojanog tipa se svakoj pobrojanoj varijabli redom pridružuje broj tipa unsigned int, počevši od 0. U gornjem primjeru, vrijednost od Sij i Jan je 0, a vrijednost od Pro i Dec je 11. Primjer 3.23 Sljedeći primjer ilustrira uporabu korisničkog pobrojanog tip month. Odredite što će ispisati na ekranu.
30 enum month {Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec}; cout << "Jan + Nov = " << Jan + Nov << endl; cout << "From February to September there are " << Sep - Feb << " months\n" << endl;
31
Poglavlje 4.
Osnovni operatori u jeziku C / C++
Operatori su uobičajene forme u programskim jezicima. Npr. aritmetički i logički operatori imaju zadaću da omoguće pregledno pisanje aritmetičkih i logičkih izraza po uzoru na matematičke. U jeziku C++ korisnik može definirati i redefinirati operatore u skladu sa svojim potrebama. Primjeri su operatori kopiranja ( >> i << ) koje smo već susreli, i za koje smo vidjeli da sa svoje lijeve strane moraju imati odgovarajući objekt (cin, te cout, respektivno) definirane u odgovarajućim klasama, a s desne strane prihvaćaju različite tipove podataka (nizove znakova, brojčane tipove). To je dobar primjer za svestranu, praktičnu i preglednu primjenu operatora. Na početku, ograničavamo se na razmatranje aritmetičkih i logičkih operatora. Oni mogu biti unarni i binarni. Unarni operatori se pišu u obliku: OpUnarni_Prefiksni Izraz1 , ili: Izraz1 OpUnarni_Postfiksni . tj. djeluju na izraz sa svoje desne strane ako su tzv. prefiksni (uobičajeni operatori i u drugim jezicima), odnosno na izraz sa svoje lijeve strane ako su postfiksni (specifično za C/C++). Binarni operatori su oblika: Izraz1 OpBinarni Izraz2 , i djeluju na izraze sa svoje dvije strane. Oba operatora kao rezultat daju odgovarajuće vrijednosti koje se mogu pojaviti kao dijelovi novih izraza.
Operator pridruživanja = Operator pridruživanja (engl. assignment operator) ' = ' je unarni operator kojim se varijablama pridružuju vrijednosti izraza, funkcija, itd. Njime se pridružuje vrijednost po načelu: variable = value , gdje na lijevoj strani može biti jednostavna varijabla ili element niza (polja, poretka), odnosno bilo koja l-vrijednost (engl. l-value, gdje l dolazi od engl. left). l-vrijednost mora označavati memorijsku lokaciju određenog tipa (ne može biti void) upisivu u vrijeme izvođenja (engl. writable at run-time), kao što i očekujemo. S desne strane operatora pridruživanja nalazi se bilo koji izraz odgovarajućeg tipa, ili tipa svodljivog na tip l-vrijednosti. Taj izraz se u kontrastu spram l-vrijednosti naziva r-vrijednost (engl. r-value, r dolazi od engl. right). Svaka l-vrijednost može imati ulogu r-vrijednosti, ali obrat ne vrijedi. Operator pridruživanja treba strogo razlikovati od matematičkog znaka = .* Npr. sljedeći izraz je uobičajen u programiranju: a=a+b;
Iz gornjeg izlaganja treba biti jasno da je njegovo značenje sljedeće: nova vrijednost varijable a je stara (prijašnja) vrijednsost te iste varijable a , plus vrijednost varijable b. Gledano matematički, taj izraz ne bi imao puno smisla, odnosno davao bi rezultat b = 0. Spomenimo usput da izraze gornjeg *
U nekim programskim jezicima to je i učinjeno uvođenjem posebnog znaka za pridruživanje. Npr. u Pascalu je znak pridruživanja := . U C/C++, kao i u mnogim drugim jezicima, zadržan je znak istovjetan matematičkom znaku jednakosti.
32
tipa koristimo uvijek kad u nekoj varijabli želimo «akumulirati vrijednosti». Tako u gornjem izrazu akumuliramo staru vrijednost od a i pridodajemo joj neku novu. Operator = vraća vrijednost sa svoje lijeve strane, tj. vrijednost koja se pridružuje u izrazu pridruživanja, što korisno služi za skraćivanje zapisa, te u produženim izrazima, kao npr.: z = y = a + b/c ;
Pridruživanje se obavlja s desna na lijevo. U gornjem primjeru najprije se izračuna r-vrijednost a + b/c , potom se ona pridruži l-vrijednosti y (varijabla), i potom se r-vrijednost y pridruži lvrijednosti z. ^ Zadatak 4.1 Nađite u programskoj okolini Visual Studia 6.0 u izborniku «Pomoć» dodatna objašnjenja o pojmovima «l-value» i «r-value» izraza, i prodiskutirajte.
Aritmetički operatori Aritmetički operatori imaju ulogu za obavljanje aritmetičkih operacija analogno ili slično svom matematičkom značenju. Aritmetički operatori (odijeljeni su razmacima) su sljedeći: Unarni: Binarni:
+
-
++
--
+
-
*
/
%
+=
-=
*=
/=
%=
Unarni operatori + i – imaju značenje predznaka kao u standardnom matematičkom zapisu (+ predznak se uobičajeno može ispustiti). Operatori predznaka mogu biti samo prefiksni. Operatori ++ i -- su C/C++ specifični u smislu da su prvi put uvedeni u jeziku C. Operator ++ je operator inkrementiranja, a operator -- operator dekrementiranja. Oni spajaju učestalu potrebu programera da inkrementira (uvećava za najmanji djelić), odnosno dekrementira (umanjuje za najmanji djelić) diskretne tipove podatka, sa procesorskim strojnim instrukcijama koje vrše tu istu radnju s vrijednostima podataka ili adresa. Na taj je način prevodilac ostvario optimalnu iskorištenost strojnih instrukcija, te programerima u C/C++ jeziku približio neke prednosti rada u zbirnom jeziku (asembleru). Ovi operatori mogu biti i prefiksni i postfiksni. Prefiksni operator najprije djeluje na izraz sa svoje desne strane i potom vraća tako promijenjenu vrijednost, a postfiksni najprije vraća postojeću vrijednost, a tek potom djeluje na svoju lijevu stranu. Prefiksna inačica operatora inkrementiranja (dekrementiranja) se naziva preinkrementirajući (predekrementirajući ), a postfiksna postinkrementirajući (postdekrementirajući). Sumarno o operatorima ++ i −− (srediti Xxxx): –
ovi operatori su C / C++ specifični, uvedeni u jeziku C;
–
operator ++ je operator inkrementiranja (engl. incrementation), a operator −− je operator dekrementiranja (engl. decrementation);
–
++ (−− ) služi općenito za inkrementiranje (dekrementiranje), tj. uvećanje (smanjenje) operanda ili izraza na koji djeluje za “najmanji djelić” ili inkrement (dekrement);
–
oba ova operatora mogu djelovati samo na l-vrijednost;
–
kod brojevnih tipova inkrement (dekrement) je 1, (vrijedi i za tipove s pomičnom točkom!).
–
oba ova operatora mogu biti i prefiksni i postfiksni.
33
Binarni operatori prikazani jednostrukim simbolom imaju svoje uobičajeno značenje aritmetičkih operacija sukladnih rabljenom tipu, odnosno ako ima više različitih tipova onda «jačem» tipu. Tu spadaju binarni operatori: + (zbrajanje), - (oduzimanje), * (množenje), / (dijeljenje), % (ostatak cjelobrojnog dijeljenja, tj. operacija a % b = a(modulo b) daje ostatak cjelobrojnog dijeljenja a s b. Neke od njih smo već koristili. Operatori koji se pišu kao kombinacija binarnog aritmetičkih operatora i znaka pridruživanja = su također C/C++ specifični. Npr. umjesto uobičajenog izraza: a=a+b;
// nova vrijednost od a = (početna vrijednost a) + b // ili: a <-- a + b
u jeziku C/C++ to možemo kraće zapisati: a += b ;
// iz operatora += uzmi '+' i izvrši zbrajanje: a + b // iz operatora += uzmi preostali '=' i izvrši // pridruživanje: a = (a + b)
Izrazi u jeziku C/C++ se izračunavaju tako da se pođe od najdesnijeg operatora pridruživanja, odnosno kombiniranog operatora aritmetičke operacije i pridruživanja (operatori: = , += , -= , *= , /= , %= ), te se izračuna vrijednost izraza s njegove desne strane, odnosno kod kombiniranih operatora izračuna se vrijednost prema prethodno izloženom pravilu. Pri tom izraz s desne strane i sâm može sadržavati izraze pridruživanja. Postoji velika fleksibilnost u pisanju izraza koja uvelike skraćuje zapis, ali se pri tom mora paziti da se ne izgubi jasnoća i čitljivost. Nepregledne izraze koje je teško komentirati treba preformulirati u jednostavnije i jasnije (vidi sljedeći zadatak). Zadatak 4.2 Promotrite šest izraza u donjem programskom odsječku. Koji od njih su jasni i pregledni a koji nisu? Napišite komentare za izraze 2 do 6. Odgovorite na pitanja: a) Predvidite koje će biti stanje varijabli nakon svakog od izraza. Ako niste sigurni proučite ponovo teorijsko gradivo o značenju operatora i načinu izračuna izraza. Tek potom umetnite u testni program tvrdnje za ispis (npr. preko cout) nakon svakog izraza i provjerite svoje odgovore. b) Za svaki od «produženih» izraza napišite ekvivalentni programski kôd s tvrdnjama u kojima se pojavljuje samo jedan znak jednostavnog pridruživanja ( = ). int iN1, iN2, iN3; // Inicijalizacija varijabli kroz produženi izraz // Izraz 1: iN1 = iN2 = iN3 = 10; // svi iNk = 10, k = 1, 2, 3. // Izraz 2: iN3 += iN2 += iN1; // iN1 = ?, iN2 = ?, iN3 = ? // Izraz 3: iN3 -= iN2 -= iN1 /= 4 + 4; // iN1 = ?, iN2 = ?, iN3 = ? // Reinicijalizacija varijabli (kao gore) iN1 = iN2 = iN3 = 10; // svi iNk = 10, k = 1, 2, 3. iN3 += iN2 += iN1; // iN1 = ?, iN2 = ?, iN3 = ? // Izraz 4 (modificirani izraz 3): iN3 -= iN2 -= (iN1 /= 4) + 4; // iN1 = ?, iN2 = ?, iN3 = ? // Reinicijalizacija: iN1 = iN2 = iN3 = 12; // svi iNk = 12, k = 1, 2, 3. // Izraz 5: iN1 *= iN2 /= iN3 %= 5; // iN1 = ?, iN2 = ?, iN3 = ? // Izraz 6: iN1 /= (iN2 /= 3 - iN2 % 5); // iN1 = ?, iN2 = ?, iN3 = ?
34 /*
// Primjer koda za ispis: cout << "iN1 = " << iN1 << "\n" << "iN2 = " << iN2 << "\n" << "iN3 = " << iN3 << "\n" << endl;
*/
Zadatak 4.3 Kroz sljedeći zadatak provjerite svoje znanje o djelovanju operatora ++ i -- u prefiksnoj i postfiksnoj formi. Prije provjere programskog odsječka u testnom programu, nastojte predvidjeti sve vrijednosti koje će se ispisati. Provjerite zatim mijenja li se i kako djelovanje operatora stavljanjem izraza unutar zagrada. Događa li se ono što ste očekivali? Objasnite! Napomena: prisjetite se što je potrebno da se skraćenica UINT ispravno prevede. Čime je možete zamijeniti ako ne želite uključivati novu zaglavnu datoteku? UINT uI1 = 0, uI2 = 0; UINT uIPre = 0, uIPost = 0; //Ispis stanja (početno): cout << "1.0 Inicijalno stanje:\n" << "===================================================\n" << "uIPre = " << uIPre << "\t" << "uIPost = " << uIPost << "\n"; cout
<< "uI1 << "uI2 << endl;
= =
" << uI1 << "\t" " << uI2 << "\n\n"
// Pridruzivanje inkrementiranih vrijednosti po djelovanju // 1. preinkrementnog, i 2. postinkrementnog operatora: uIPre = ++uI1 ; uIPost = uI2++ ; // Ispis cout << << << << cout
stanja po djelovanju ++ operatora: "1.1 Stanje po djelovanju ++ operatora:\n" "===================================================\n" "uIPre = ++uI1 = " << uIPre << "\t" "uIPost = uI2++ = " << uIPost << "\n";
<< "uI1 << "uI2 << endl;
= =
" << uI1 << "\t" " << uI2 << "\n\n"
// Pre i post-inkrementni operatori uz uporabu zagrada // // Reinicijalizacija: uI1 = uI2 = uIPre = uIPost = 0; // Ispis stanja (početno): cout << "2.0 Novo inicijalno stanje:\n" << "===================================================\n" << "uIPre = " << uIPre << "\t" << "uIPost = " << uIPost << "\n"; cout << "uI1 << "uI2 << endl;
= =
// Djelovanje operatora uIPre = (++uI1) ;
" << uI1 << "\t" " << uI2 << "\n\n"
35 uIPost = (uI2++) ; //Ispis cout << << << <<
stanja po djelovanju ++ operatora uz zagrade: "2.1 Stanje po djelovanju ++ operatora uz zagrade:\n" "===================================================\n" "uIPre = (++uI1) = " << uIPre << "\t" "uIPost = (uI2++) = " << uIPost << "\n";
cout << "uI1 << "uI2 << endl;
= " << uI1 << "\t" = " << uI2 << "\n\n"
cout << "Sto zakljucujete? Objasnite kako djeluju zagrade!\n\n" << endl;
Zadatak 4.4 Operatori ++ i -- , dodatni zadatak. Prije provjere programskog odsječka u testnom programu, odgonetnite sve vrijednosti koje će se ispisati. Usput ponovite što ste već ranije naučili o tipu UINT (unsigned int)? Kako će se izvršiti pretvorba tipa UINT u int? UINT uPPPre, uPPPost, uMMPre, uMMPost; uPPPre = uPPPost = uMMPre = uMMPost = 0; cout << "UINT uPPPre, uPPPost, uMMPre, uMMPost \n" << "========================================\n" << endl; // Ispis stanja (početno): cout << "uPPPre = " << uPPPre << "\n" << "uPPPost = " << uPPPost << "\n" << "uMMPre = " << uMMPre << "\n" << "uMMPost = " << uMMPost << "\n" << endl; // Ispis uz djelovanje pre i post inkrementirajućih i // dekrementirajućih operatora 1. put: cout << "uPPPre = ++uPPpre = " << ++uPPPre << "\n" << "uPPPost = uPPPost++ = " << uPPPost++ << "\n" << "uMMPre = --uMMPre = " << --uMMPre << "\n" << "uMMPost = uMMPost-- = " << uMMPost-- << "\n" << endl; // Ispis stanja (nakon prvog djelovanja): cout << "uPPPre = " << uPPPre << "\n" << "uPPPost = " << uPPPost << "\n" << "uMMPre = " << uMMPre << "\n" << "uMMPost = " << uMMPost << "\n" << endl; // Ispis uz djelovanje pre i post inkrementirajućih i // dekrementirajućih operatora 2. put: cout << "uPPPre = ++uPPpre = " << ++uPPPre << "\n" << "uPPPost = uPPPost++ = " << uPPPost++ << "\n" << "uMMPre = --uMMPre = " << --uMMPre << "\n" << "uMMPost = uMMPost-- = " << uMMPost-- << "\n" << endl; // Ispis stanja (nakon drugog djelovanja):
36 cout << "uPPPre << "uPPPost = " << "uMMPre = " << "uMMPost = " << endl;
= << << <<
" << uPPPre << "\n" uPPPost << "\n" uMMPre << "\n" uMMPost << "\n"
// Prijelaz s UINT na int: int iMMPre = uMMPre; int iMMPost = uMMPost; cout << "iMMPre = " << iMMPre << "\n" << "iMMPost = " << iMMPost << "\n" << endl;
Zadatak 4.5 Odgonetnite što će se ispisati nakon izvođenja sljedećeg programskog odsječka i potom provjerite izvođenjem u testnom programu. Pokušajte maknuti neku od zagrada oko izraza čije se vrijednosti ispisuju i vidite što se događa. int i = 5; cout << "int i = " << i << "\n" << endl; // ispis = ?, i = ? cout << " -(--i)++ \tispis : " << -(--i)++ << endl; cout << "\t\ti = " << i << "\n" << endl; // ispis = ?, i = ? cout << "( i -= - i++ ) \tispis : " << ( i -= - i++ ) << endl; cout << "\t\ti = " << i << "\n" << endl; // ispis = ?, i = ? cout << "( i += - ++i ) \tispis : " << ( i += - ++i ) << endl; cout << "\t\ti = " << i << "\n" << endl;
Logički operatori Tri su logička operatora: ! za negaciju, && za logički I (AND), te || za logički ILI (OR). U skladu s logičkim operacijama koje vrše jasno je da je prvi operator unarni prefiksni (djeluje na logičku varijablu sa svoje desne strane i vraća r-vrijednost), a druga dva su binarna operatora (vraća rvrijednost). Iako su strojne instrukcije koje izvode logičke operacije standardne na svim procesorima (to su tzv. bit-na-bit [engl. bit-wise] operacije), zbog prije spomenute pohrane logičkog tipa u jedan bajt i njegovog tretiranja tek kao posebnog slučaja cjelobrojnog tipa, logičke operacije se svode na modificirane aritmetičke operacije. Logički I se svodi na modificirano množenje, a ILI na modificirano zbrajanje. U skladu s time, razumljivo je da C/C++ dozvoljava uporabu logičkih operacija i nad cjelobrojnim tipom, po već spomenutom načelu da se sve vrijednosti različite od 0 (i negativne) tretiraju kao true, a 0 se tretira kao false. Svi rezultati logičkih operacija se, međutim, uvijek svode na cjelobrojne vrijednosti 0 i 1, bez obzira je li tip operanda u tim operacijama bio bool, int ili mješoviti. (Razmislite kako biste realizirali logičke operacije preko aritmetičkih.)
37
Prilikom utvrđivanja istinitosti logičkih izraza vrši se tzv. kratko-spojna provedba (engl. shortcircuit evaluation), tj. nikad se ne izračunava dio izraza koji nije potrebno određivati. Npr. ako je lijevi operand (izraz) kod logičkog I ( && ) vrijednosti false (0), tada desni operand (izraz) nije potrebno izračunavati, jer je rezultat automatski 0. Kaže se da je logička 0 tzv. kritična vrijednost operacije I, jer je dovoljno da jedan operand bude 0 pa da rezultat bude 0. Analogno vrijedi ako je lijevi operand (izraz) kod logičkog ILI ( || ) vrijednosti true (1). Pošto je kritična vrijednost operacije ILI 1 (true), tada se desni izraz ne izračunava i rezultat operacije je odmah određen kao 1. Zadatak 4.6 U sljedećem programskom odsječku odredite rezultat ispisa. bool b0 = false; bool b1 = true, bVar; cout
<< " b0 = " << b0 << "\t b1 = " << b1 << "\n" << endl;
bVar = b0 && b1; // AND cout << " b0 && b1 = " << bVar << "\n" << " !(b0 && b1) = " << !bVar << "\n" << endl; bVar = b0 || b1; // OR cout << " b0 || b1 = " << bVar << "\n" << " !(b0 || b1) = " << !bVar << "\n" << endl;
Zadatak 4.7 Napišite program koji upisuje vrijednosti dva bita b1 i b2 kao dvije logičke varijable, te ispisuje vrijednosti dvije logičke funkcije, sume i prijenosa, za poluzbrajalo (HA = Half Adder). Podsjetite se binarnog zbrajanja, te napišite najprije tablicu istinitosti za funkcije sume i prijenosa (vidi također kolegije Informatika, te područje Logike sudova u matematici). Zadatak 4.8 Napišite program koji upisuje vrijednosti dva bita b1 i b2, te prijenos iz nižeg bita b0, kao tri logičke varijable, te ispisuje vrijednosti sume i prijenosa, za potpuno zbrajalo (FA = Full Adder). Podsjetite se binarnog zbrajanja, te napišite najprije tablicu istinitosti za funkcije sume i prijenosa (vidi također kolegije Informatika, te područje Logike sudova u matematici).
Relacijski operatori Relacijskim operatorima utvrđuje se istinitost matematičkih relacija, i vraća vrijednost true ako je relacija istinita, odnosno vrijednost false ako nije. Pri tom s obje strane relacijskog operatora mogu stajati bilo l-vrijednosti, bilo r-vrijednosti (kao što se i očekuje). Relacijski operatori su sljedeći: ==
jednakost,
!=
nejednakost,
<
manje,
>
veće,
<=
manje ili jednako,
>=
veće ili jednako.
Npr. izraz ( 0 <= -1 ) ima vrijednost false, a izraz ( 7 == 71%8 ) ima vrijednost true. Relacijski operatori se često koriste u kombinaciji s logičkim operatorima za utvrđivanje uvjeta kod kontrole tijeka programa (vidi sljedeće poglavlje). Zadatak 4.9 U sljedećem programskom odsječku odredite vrijednosti svih varijabli, logičkih i cjelobrojnih. Dodatna pitanja:
38
a) Najprije dobro promislite o odgovorima, i prisjetite se «kratko-spojnog» načina izvršavanja logičkih izraza. Za svaku od logičkih operacija && i || odredite koja je vrijednost «kritična», odnosno nakon koje vrijednosti lijeve strane izraza, desnu stranu nije potrebno određivati. Tek potom testirajte vrijednosti ispisujući njihove vrijednosti na ekranu. b) Skratite (pojednostavite) sve izraze koji se daju skratiti, i ispitajte ih i tako skraćene. bool b1, b2, b3, b4, b5, b6; int i1 = 0, i2 = -1; b1 = (i1 >= i2) && !(i1 == i2); b2 = (i1 > -i2) || !(i1*i2 <= i2);
// b1 = ? // b2 = ?
// Nove vrijednosti cjelobrojnih varijabli: i1 = i2 = 0 ; // b3 = ?, b4 = ? b3 = (++i1 > i2++) && (--i1 < i2--); // i1 = ? , i2 = ? b4 = (++i1 <= i2++) || (--i1 < i2--); // i1 = ? , i2 = ? // b5 = ?, b6 = ? b5 = (++i1 < i2++) && (--i1 < i2--); // i1 = ? , i2 = ? b6 = (++i1 > i2++) || (--i1 < i2--); // i1 = ? , i2 = ?
Zadatak 4.10 Napišite odsječak programa u kojem ćete deklarirati sve potrebne varijable i napisati logički izraz za logičku varijablu bProsaoIspitIzProg1 pridružuje vrijednost true ako je student zadovoljio kriterije, a u suprotnom vrijednost false. Prvi preduvjet je da vrijednost nepredznačene cjelobrojne varijable uBrojPrisustNaPred nije manja od 60% maksimalnog broja prisustvovanja koji je sadržan u cjelobrojnoj nepredznačenoj konstanti uMaxPrisustNaPred (koja iznosi 8). Drugi preduvjet je da vrijednost nepredznačene cjelobrojne varijable uBrojPrisustNaVjez nije manja od 80% maksimalnog broja prisustvovanja koji je sadržan u cjelobrojnoj nepredznačenoj konstanti uMaxPrisustNaVjez (koja iznosi 12). U slučaju da su oba kriterija prisustvovanja zadovoljena, varijabli bApsolvirao se uspust (untar logičkog izraza) pridružuje vrijednost true. Potom se gleda je li student prošao oba kolokvija, čije su ocjene dane u nepredznačenim varijablama uOcjKolok1 i uOcjKolok2, ili je zadovoljio na pismenom ispitu čija je ocjena u varijabli uOcjPisIspita .
39
Poglavlje 5.
Organizacija izvornog koda, programske strukture, i kontrola toka
Strukturirani programi i osnovne programske strukture Za programe koji koriste propisane forme organizacije i programske strukture, kažemo da su strukturirani, a pošto se radi o osnovi o proceduralnom programiranju, u koje spada i funkcijsko, govorimo o proceduralno strukturiranim programima. Osnovna je odlika strukturiranih programa formalna strogost, koja će rezultirati u predvidljivost i preglednosti organizacije programskih dijelova i kontroli toka programa. Kontrolu toka i izvršavanja programa ostvarujemo korištenjem četiri osnovne programske strukture ili forme: • • • •
Slijed (sekvenca), nizanje tvrdnji jedne za drugom; Izbor (selekcija) tvrdnji, u ovisnosti o ispunjenosti uvjeta; Ponavljanje (iteracija); Implicitna struktura: bezuvjetni skok.
Ispravnom uporabom ovih programskih formi moguće je napisati svaki program. Istaknimo da ih u gornjoj listi nipošto nismo napisali prema hijerahiji, niti po inherentnoj složenosti. Napisane su prema uporabljivosti i prepoznatljivosti za programera. Tako je uobičajeno da u „dobrim“ suvremenim programskim jezicima postoje već pripremljene tvrdnje za strukturiranu selekciju i iteraciju, što vrijedi i za C / C++. S druge strane, bezuvjetan skok, iako elementarna programska struktura bez koje se ne može, ugrađen je i u selekciju i u iteraciju, ali u višim programskim jezicima postaje nevidljiv za programera. Također, i za slijed tvrdnji, koji je sâm po sebi jednostavan, možemo ustvrditi da ima dobru strukturu ako smo dijelove programa pregledno ispisali, te po potrebi organizirali unutar blokova. Vrlo često se u literaturi navodi da su dobro strukturirani programi oni koji ne rabe ozloglašenu goto (od engl. go to, hrv. idi na) tvrdnju bezuvjetnog skoka. Njome su se programeri često koristili u jezicima u kojima je manjkalo blokovne organizacije i dobro strukturiranih tvrdnji. Međutim odmah valja naglasiti da su bezuvjetni skokovi uključeni i u tvrdnje za ostvarenje selekcije i ponavljanja. To je razlog zašto je ono označeno kao implicitna struktura. Tako se kod selekcije ostvarene s pomoću if – else tvrdnje, po završetku bloka uz if dio vrši bezuvjetni skok na mjesto sljedeće tvrdnje, tj. iza else bloka. Jednako, prilikom ostvarenja iteracije s pomoću while tvrdnje, na kraju njegovog bloka vrši se bezuvjetan skok na ponovnu provjeru uvjeta petlje. Za detalje obiju ovih tvrdnji vidjeti niže. Također prilikom povratka iz funkcije, vrši se bezuvjetni skok na tvrdnju koja slijedi iza poziva funkcije. Sve ovo je pogotovo očito na razini zbirnog jezika, u kojem će bezuvjetno grananje pratiti ostvarenje navedenih programskih struktura uporabom odgovarajućih strojnih instrukcija granja ili skoka. One uobičajeno imaju mnemonike* kao npr. BRA ili BR (od engl. branch, granaj se), ili JMP (od engl. jump, skoči).
*
Mnemonik, u slobodnom prijevodu podsjetnik, je kratka rječca ili kratica koja sugerira zadaću strojne instrukcije.
40
Vratimo li se na više programske jezike, možemo ponovo naglasiti da kod svih onih koji imaju dobro strukturirane tvrdnje za kontrolu toka programa, kao što je i C / C++, zaista nema potrebe za korištenjem goto tvrdnje bezuvjetnog skoka. S druge strane, ona se ipak najčešće implementira i u tim jezicima, zbog načela potpunosti, odnosno da posluži programerima koji zbog nekih razloga insistirati na njoj. Opisana je na kraju poglavlja. Dobra je praksa da se programerima početnicima jednostavno zabrani njena uporaba, odnosno da se uopće ne koristi u primjerima programa u udžbeničkim tekstovima. Također, jasno je da je gornja definicija strukturiranih programa prejednostavna i prejednostrana. Zbog toga, a također i da se ne stvara mistifikacija oko tvrdnje goto , mora se naglasiti da ova tvrdnja sama nije bila razlogom nastanka loših i slabo strukturianih programa. Razlog za to je u prvom redu bila njena nekoznistentna i loša uporaba u jezicima u kojima je to bilo nužno (npr. BASIC, FORTRAN). Ako se programer nije držao „dobrih pravila“, koja je najčešće morao sâm stvarati na temelju svog računarskog znanja i programerske vještine, tada je nastali kôd bio nepregledan, težak za provjeru, korekciju i nadograđivanje. Također, da odemo korak dalje, u zbirnim jezicima (asemblerima), nužno je rabiti tvrdnje skoka i grananja, a programi se i u njima mogu pisati tako da imaju strogu i preglednu strukturu. Odnosno, svi programi pisani u višim programskim jezicima, bili oni neproceduralni ili proceduralni, formalno manje ili više strogi, prevode se u strojni kôd. Strojni kôd pak izravno korespondira s tvrdnjama pisanim u zbirnom jeziku (engl. assembly language), sastavljenom od strojnih instrukcija procesora date računarske platforme. Na razini strojnih instruckija, programske strukture viših jezika prevode se u niz tvrdnji, između kojih će postojati i tvrdnje grananja i skoka, iako se to u izvornom kodu eksplicitno ne vidi. Dakle, problem nije i ne može biti u suštini ove operacije, koja se ionako nikako ne može izbjeći, već u načinu njene uporabe. Pojavom jezika koji su ukinuli potrebu za korištenjem tvrdnje goto (Pascal, C), programere je u prvom redu trebalo upozoriti da iskoriste postojeće dobre programske strukture, odnosno da jednostavno prestanu rabiti tvrdnju goto . Za one koji se nisu susretali s tom tvrdnjom, dovoljno je reći da ona zaista nije ni potrebna, odnosno da ona lako može narušiti principe strukturiranog programiranja. No, jednako kao što je i uz tvrdnju goto bilo dobrih i formalno korektnih programa, tako i bez nje ima loših i nepreglednih. Stoga programeri moraju u prvom redu dobro proučiti sve strukture koje im kao alati i gradbeni blokovi stoje na raspolaganju, te ih naučiti adekvatno rabiti pri implementaciji algoritama.
Tvrdnja, blok tvrdnji i lokalne varijable Tvrdnja. Pojam tvrdnje (engl. statement) ukratko smo opisali kod opisa prvih programa (pogl. 2xx). Ovdje ćemo to učiniti preciznije. Tvrdnja se definira preko pojma izraz (engl. expression). Izraz je bilo koji niz znakova i ključnih riječi u jeziku koji udovoljava sintaktičkim pravilima jezika C / C++. Tvrdnja se sada može definirati kao izraz okončan znakom točka-zarez: expression_Opt ;
// Tvrdnja koja se sastoji od izraza expression_Opt. // S Opt smo označili da je izraz opcionalan, tj. ne mora ga biti, // u kom slučaju govorimo o praznom (NULL) izrazu.
Kao što smo naveli u komentarima, izraz je opcionalana (engl. optional), i to smo naznačili u njegovom imenu. Ako ga ispustimo, govorimo o praznom, nepostojećem ili NULL izrazu. Takav izraz kompilator tretira formalno kao izraz, ali koji ništa ne radi, tj. za izvođenje kojeg nije potrebno umetati strojne instrukcije. Gornji izraz možemo kratko zapisati kao:
41 statement
// statement = expression_Opt ;
uvažavajući da se unutar tvrdnje nalazi znak ';' za njeno okončanje. Ovakve tvrdnje se nazivaju i jednostavne tvrdnje (engl. simple statements, expression-statements) , da se naglasi da su sastavljene od jednog (ili nijednog) izraza. Tvrdnje se mogu nizati u slijedove tvrdnji (engl. statement sequence), za koje vrijedi rekurzivna definicija: statement_Sequence_Opt
Ù statement_1 statement_Sequence_Opt_1 statement_2
Tu smo znakom ekvivalencije Ù označili da je proizvoljan slijed tvrdnji statement_Sequence_Opt ekvivalentan nizu koji se sastoji od tvrdnje statement_1, iza koje dolazi neki općeniti slijed tvrdnji statement_Sequence_Opt_1, pa opet tvrdnja statement_2. Pri tom smo u definiciji pojma slijeda tvrdnji koristili taj isti pojam. Definicije u kojima se za objašnjenje koriste i sami pojmovi koje definiramo, nazivaju se rekurzivne definicije (engl. recursive definition). * Gornji je opis ilustracija uobičajenog formalnog opisa sintakse programskih jezika, kojim se mora postići i preciznost i općenitost. Ista definicija koja ne koristi rekurziju, bila bi: statement_Sequence_Opt
Ù statement_1 statement_2 ... ... statement_n
Ponovimo, ovdje je svaka tvrdnja statement _i ( i = 1, 2, … n ) okončana je znakom ' ; '. Na kraju ovog razmatranju o tvrdnjama, spomenimo da su u ranijim definicijama jezika C tvrdnje deklaracije varijabli i struktura imale osobitost da su se morale stavljati na početak bloka, prije ostalih, „normalnih“ tvrdnji. Stoga se u strogom gramatičkom smislu govorilo da to nisu tvrdnje, ili barem da nisu uobičajene tvrdnje. Pod utjecajem jezika C++ , kod kojeg to nije slučaj, novije definicije C kompilatora (možemo smatrati od 1999. pa na dalje), podvele su deklaracije pod standardne tvrdnje koje je moguće stavljati bilo gdje u bloku. O ovim i sličnim razlikama programer mora voditi računa kad izvorni kôd zamišljen u jeziku C++ (naravno pod uvjetom da ne koristi bitne odlike objektnog programiranja), želi prevesti u jezik C. Blok tvrdnji. Tvrdnje ili naredbe u jeziku C / C++ organiziramo u blokove. Blok tvrdnji (engl. statement block) čini skup tvrdnji koji čini funkcionalnu cjelinu. Blokovi se ograđuju vitičastim zagradama: { statement_1 statement_2 ...
...
statement_n }
*
Rekurzivan (engl. recursive), koji se vraća natrag (na sebe samog). Vidjeti također rekurzivni poziv funkcija u pogl. Xxx.
42
Pored mogućnosti grupiranja tvrdnji, blokovi predstavljaju vrlo važan koncept za definiranje dogleda lokalnih varijabli (vidi sljedeći odjeljak). Vratimo li se na organizaciju tvrdnji, njihov blok možemo promatrati kao novu, sastavljenu tvrdnju (engl. compound statement), koju definiramo na sljedeći način: { statement_Sequence_Opt }
// Sastavljena tvrdnja (compound statement).
Dakle, sastavljena tvrdnja se sastoji od proizvoljnog slijeda tvrdnji statement_Sequence_Opt definiranog gore. Pošto je slijed tvrdnji opcionalan, blok { ; } s praznim izrazom, predstavlja praznu ili NULL sastavljenu tvrdnju. Ukoliko se radi o samo jednoj tvrdnji, npr. u funkciji, ili u tvrdnjama za selekciju ili iteraciju (vidi dalje), nije potrebno organizirati blok. Dovoljno je samo navesti tu jednu tvrdnju, bez vitičastih zagrada. Općenito, i sastavljene i jednostavne tvrdnje obuhvatit ćemo pod zajedničkim nazivom: statement
// Sastavljena ili jednostavna tvrdnja
Spomenimo još da se blok tvrdnji ili naredbi može se organizirati samo unutar glavne ili drugih funkcija, tj. nije dozvoljeno pisanje bloka bilo gdje izvan toga. Pisanje znaka točka-zarez. Nakon gornjih definicija možemo rezimirati sljedeća pravila o pisanju znakova točka-zarez: i. Jednostavne tvrdnje oblika: expression_Opt ;
uvijek moraju biti okončane znakom točka-zarez; ii. Sastavljene tvrdnje oblika: { statement_Sequence_Opt }
iza zatvorene vitičaste zagrade nemaju znak točka-zarez. Možemo zamišljati da su one okončane znakom ' } ' . iii. U jeziku C / C++ ne postoji eksplicitno gramatičko pravilo glede pisanja znaka točka zarez, međutim, gornje pravilo ii. vrijedi u većini slučajeva, osim u nekoliko iznimaka, kao što je deklaracija klasa. U slučajevima kada pisanje znaka ' ; ' nije nužno, najbolje ga je i ne pisati. Naime, iako bi ga kompilator u tim slučajevima tretirao kao okončanje prazne tvrdnje i ignorirao, jednako kao i kod nizanja višestrukih znakova točka-zarez jednog za drugim, zbog slučajeva kada to rezultira krivom interpretacijom, bolje je ne stjecati takvu naviku. Spomenuti slučajevi se odnose na produljenu if -- else if tvrdnju u kojoj bi dodavanje točke-zarez iza bloka if dijela rezultiralo okončanjem tvrdnje i sintaktičkom pogreškom (vidi niže, odjeljak o selekciji). Gniježđenje blokova. Unutar bloka tvrdnji možemo otvarati proizvoljan broj novih blokova, koji mogu biti i ugniježđeni jedan u drugi, do proizvoljne razine. U sljedećem primjeru prikazano je takvo gniježđenje do dubine 2: // Organizacija tvrdnji u blokove: { statement_Sequence_1 { statement_Sequence_1_1 { statement_Sequence_1_1_1 }
43 { statement_Sequence_1_1_2 } } ...
...
}
Nazovemo li blok prema indeksu slijeda tvrdnji kojim taj blok započinje, cijeli gornji blok može mo označiti kao: block_1
// Blok s podblokovima.
Njegovu strukturu možemo pobliže opisati s pomoću podblokova prve razine: // block_1 { statement_Sequence_1 { block_1_1 } ...
...
}
gdje je podblok block_1_1 nadalje prikazan sljedećom strukturom: // block_1_1: { statement_Sequence_1_1 { statement_Sequence_1_1_1 } { statement_Sequence_1_1_2 } }
Lokalne varijable. Varijabla deklarirana unutar bloka naziva se lokalna varijabla (engl. local variable). Naziv lokalnost se tu odnosi upravo na blok u kojem je deklaracija izvršena. Kao što smo već istaknuli u poglavlju o tipovima podataka, kad kompilator naiđe na deklaraciju varijable unutar nekog bloka, on rezervira prostor potreban za nju u skladu s duljinom njenog tipa, a njeno ime (identifikator) unese u tzv. tablicu simbola. Deklarirana varijabla ima područje djelovanja, ili u slobodnom prijevodu, dogled (engl. scope), samo unutar dotičnog bloka, uključujući i sve ugniježđene blokove, ali ne u nadređene blokove (nadblokove), niti u sebi „paralelne“ i njima podređene blokove. Ime lokalne varijable je prepoznatljivo u navedenim područjima, jer kompilator prilikom nailaska na identifikator varijable konzultira najprije tablice simbola tekućeg bloka, a potom i svih njemu nadređenih blokova, uključujući i varijable deklarirane van svih blokova, tzv. globalne varijable (vidi dalje). Ako je ime korištene varijable nađeno u tablici simbola tekućeg ili njemu nadređenih blokova, tada kompilatora „prepoznaje“ varijablu, tj. mjesto u memoriji koje je rezervirano za nju. Ako ime nije nađeno, tada će biti dojavljena greška neobjavljeni identifikator (engl. undeclared identifier).
44
Ovakva organizacija izravno se odražava na tretman lokalnih varijabli prilikom izvršavanja programa: izlaskom programa iz bloka u kojem je varijabla deklarirana, ona postaje nepoznata, a mjesto rezervirano za nju se „briše“. To objašnjava već naglašenu činjenicu da dogled lokalnih varijabli ne seže niti u nadblokove, niti u blokove paralelne po hijerarhiji onom gdje su deklarirane. Iz istog razloga se lokalne varijable nazivaju i automatske varijable (engl. automatic variables) jer je njihova aktualizacija i brisanje automatski oranizirano njihovom pohranom na memorijskom stogu (detaljnije o tome vidjeti u poglavlju 8xx, a za mehanizam pohrane lokalnih varijabli na stog vidjeti dodatku Cxx). Globalne varijable. Varijable deklarirane izvan glavne i ostalih funkcija nazivaju se globalne varijable (engl. global variables).* One su dostupne svim funkcijama unutar iste datoteke, dakle imaju dogled u sve dijelove programske cjeline. Ovdje su globalne varijable spomenute radi potpunosti, i u kontekstu organizacije programa kroz blokove tvrdnji. Njihovu uporabu treba strogo izbjegavati, o čemu će biti riječi u narednim poglavljima (vidi pogl. 8xx). Primjer 5.1 Promotrite sljedeći programski odsječak u svezi s dogledom varijabli izvan i unutar bloka. Zašto smo varijable deklarirane izvan glavne funkcije nazvali «globalnim» varijablama? Jesu li one „sagledljive“ u unutarnjim blokovima? Što se događa kad u podređenom bloku deklariramo varijablu istog imena kao i u nadređenom. Hoće li to rezultirati greškom? Predvidite rezultate izvođenja i potom testirajte program. #include using namespace std; int iGlobal1 = 11; int iGlobal2 = 22;
// Varijable def. van glavne f-je: // globalne varijable.
int main() { int iInMainBlock = 33;
// Varijabla def. unutar glavne f-je.
// Ispis 1: cout << "1. Deklaracija varijabli van i unutar bloka glavne funkcije: \n" << "================================================================\n" << " iGlobal1 = " << iGlobal1 << "\n" << " iGlobal2 = " << iGlobal2 << "\n" << "iInMainBlock = " << iInMainBlock << "\n" << endl; // Ponovna objava iGlobal2. Hoće li prevodilac javiti grešku? int iGlobal2 = 222; // Ispis 2: cout << "2. Dekl. var. van i unutar bloka gl. f-je, redeklarac. iGlobal2:\n" << "================================================================\n" << " iGlobal1 = " << iGlobal1 << "\n"<< " iGlobal2 = " << " iGlobal2 = " << iGlobal2 << "\n" << "iInMainBlock = " << iInMainBlock << "\n" << endl;
*
Napomenimo usput da se u tim područjima izvornog koda, izvan glavne i ostalih funkcija, pored direktiva prevodiocu (npr. include, using namespace), mogu navoditi samo izrazi za objavu i inicijalizaciju varijabli, a ne i ostale proizvoljne tvrdnje.
45 }
return 0;
Primjer 5.2 Promotrite sljedeću glavnu funkciju i diskutirajte dogled varijabli u ugniježđenim i „paralelnim“ blokovima (blokovima iste hijerarhije). Koje varijable tretiramo kao zajedničke svim blokovima? Koje od njih redefiniramo u podblokovima? Pokušajte predvidjeti je li program sintaktički ispravno napisan, tj. hoće li proći kompilaciju? Ako ne, objasnite zašto i otklonite grešku. int main() { int iM = int i0 =
// Block 0 (nesting sublevel 0) 10; 100;
// Variables with scope to inner blocks // - " - " - " –
cout << "Block 0: " << "\n" << "iM = " << iM << "\n" << "i0 = " << i0 << "\n" << endl; i0 /= iM; // Referencing local variables. cout << "i0 /= iM, i0 = " << i0 << "\n" << endl; // Block 1.1 (nesting sublevel 1, block 1): { // Local variable declaration and initialization: int i0 = 1100; cout << "Block 1.1: " << "\n" << "iM = " << iM << "\n" << "i0 = " << i0 << "\n" << endl; i0 /= iM; // Referencing local variables in a nested block. cout << "i0 /= iM, i0 = " << i0 << "\n" << endl; } // Block 1.2 (nesting sublevel 1, block 2): { // Local variable declaration and initialization: int i0 = 1200; cout << "Block 1.2: " << "\n" << "iM = " << iM << "\n" << "i0 = " << i0 << "\n" << endl; i0 /= iM; // Referencing local variables in a nested block. cout << "i0 /= iM, i0 = " << i0 << "\n" << endl; // Block 2.1 (nesting sublevel 2, block 1): { int i2 = 2100; cout << << << << <<
"Block 2.1: " "iM = " << iM "i0 = " << i0 "i2 = " << i2 endl;
i2 /= iM;
} }
<< << << <<
"\n" "\n" "\n" "\n"
// Referencing local var. in a doubly-nested block
cout << "i2 /= iM, i2 = " << i2 << "\n" << endl;
46 // Is something missing? cout << << << << <<
"Block 0: " "iM = " << iM "i0 = " << i0 "i2 = " << i2 endl;
<< << << <<
"\n" "\n" // iM = ? "\n" // i0 = ? "\n" // i2 = ?
return 0; }
Slijed tvrdnji (sekvenca) Slijed tvrdnji je najjednostavnija programska struktura. Jedna ili više tvrdnji navode se jedna za drugom*, čime se i definira redoslijed kojim će biti izvršeni. Pri tom uvažavamo poznato pravilo da svaka tvrdnja u C / C++ završava znakom ; . Slijed smo već uvelike koristili u dosadašnjim primjerima. Jasno je da se radi o elementarnoj programskoj strukturi, u smislu da će se izvršiti točno toliko tvrdnji koliko ih je napisano. S druge strane, lako je primijetiti da se pukim izvođenjem niza tvrdnji jedne za drugom, ne mogu ostvariti niti vrlo jednostavni zadaci u kojima se tijek izvođenja mijenja u ovisnosti o određenim uvjetima, kao ni oni zadaci u kojima se mora ostvariti ponavljanje radnji za općenit, unaprijed nepoznat, broj puta.
Izbor (selekcija). Uvjetne tvrdnje Programska struktura izbora ili selekcije (engl. selection) služi da odaberemo različite tokove ili staze programa (engl. program paths), ili drugim riječima, izvršavanje njegovih različitih dijelova u ovisnosti o ispunjenju određenih uvjeta. Algoritmi iz uvodnog poglavlja dali su nam mnoštvo primjera za to. Pošto je u osnovi ove programske strukture provjera uvjeta koji će odlučiti o daljnjem toku izvršavanja programa, ona se ostvaruje s pomoću tzv. uvjetnih tvrdnji. Uvjetne tvrdnje. Uvjetne tvrdnje (engl. conditional statements) imaju sljedeći općeniti oblik: if (condition) block1 else block2 ;
koji se preglednije piše u dva retka: if (condition) block1 else block2 ;
Ovdje condition† prikazuje općeniti izraz, za koji bi, formalno gledano, trebalo vrijediti da je logičkog tipa. Ukoliko je condition = true, izvršava se block1, blok block2 se preskače, tj. prelazi se na izraz programa koji slijedi iza njega. U suprotnom, ako je condition = false, izvršava se block2. Potrebno je reći da C kompilator, za razliku od mnogih drugih programskih jezika, kao uvjet dozvoljava izraz svakog tipa, a ne samo logičkog. Dozvoljeni su i izrazi realnog tipa. Vrijedi standardno pravilo da će se svi rezultati izraza jednaki 0 tretirati kao false , a svi ostali kao true . Zlorabljenje ove slobode rezultira nečitkim programskim kodom i čest je izvor logičkih grešaka. Stoga je najbolje pridržavati se pravila da se uvjet organizira kao izraz logičkog tipa. *
Jedan ispod drugog ili jedan iza drugim. Za kompilator je nužno da su slijedni izrazi odvojeni znakom ; , dok se svi ostali delimiteri (razdvojnici), kao što su prazno mjesto ili znak za prelazak u novi red, ignoriraju. Dakle, mogli bismo izraze pisati i jedan za drugim, no slijedeći pravila dobrog pisanja programskog koda, najčešće ćemo izraze pisati jedan ispod drugog. †
Od engl. condition = hrv. uvjet.
47
Dio izraza else koristi se po potrebi, tj. može se ispustiti ako u slučaju nezadovoljenja uvjeta nije potrebno izvršiti nikakve radnje. Kao što je već spomenuto, ukoliko se u bloku nalazi samo jedna C / C++ tvrdnja, možemo skratiti zapis ispuštajući otvaranje bloka s pomoću vitičastih zagrada. U tom slučaju, ta tvrdnja, naravno, mora okončati znakom ; . Međutim, ako je blok otvoren, gore navedena sintaksa propisuje da se iza njegove zatvorene vitičaste zagrade ne piše točka-zarez: iza bloka block1 je nema. Kad je ne bismo ispustili, to bi značilo da je uvjetni izraz okončan, a kompilator bi javio da else nema prethodeći if dio. Ovi detalji sintakse se mogu iščitati i iz dolje navedenih primjera. Iza else se može pojaviti nova if tvrdnja, ponovo sa ili bez else. Pravilo je da se else odnosi na najbliži prethodni if. Tako je tvrdnja: if ( cond1 ) block1 else if ( cond2 ) block2 else block3
ekvivalentna sljedećoj: if ( cond1 ) block1 else { if ( cond2 ) block2 else block3 }
Primjer 5.3 Uporaba if – else tvrdnje. U sljedećem programskom odsječku učitava se logička varijabla i zatim ispisuje na dva načina: i) kao što ih tretira C++ kompilator, te s pomoću matematičkih oznaka T (true) i F (false). Za ostvarenje programske strukture selekcije koristi se if – else tvrdnja. Pažljivo promotrite uvjet koji je korišten u njemu, razmislite može li se on i cijela if tvrdnja napisati drugačije, odnosno jednostavnije (zadatak naveden u komentarima na dnu). Također provjerite kako se izvršava program kad bi zabunom uvjet napisali kao: b1 = false, tj. specificirali pridruživanje, a ne relaciju jednakosti, što je vrlo česta omaška. bool cout cin cout
b1; // Logička varijabla << "Unesite logicku var. (0 ili 1), b1 = " ; >> b1; << endl; // Red razmaka
// Prvi, zajednički dio ispisa. Logička var. kao što je tretira C++: cout << "Logicka var. b1 = " << b1; // Pridjeljivanje matematičkih oznaka logičkoj varijabli. // Selekcija s pomoću uvjetne tvrdnje, varijanta 1: if (b1 == false) cout << " = F (false)\n"; else cout << " = T (true)\n"; cout << endl;
// Red razmaka
// Modificirajte uvjet i prilagodite if tvrdnju na sljedeće načine. // Varijanta 2: u uvjetu rabite logičku konstantu true. // Varijanta 3: napišite čim jednostavniji mogući uvjet, bez // korištenja relacijskih operatora! ////////////////////
Primjer 5.4 Uporaba if – else if – else tvrdnje. U sljedećem primjeru dana je proširena uvjetna tvrdnja if – else if – else u kojoj su uvjeti relacijski izrazi za uspoređivanje dvije varijable fVar1
48
i fVar2 tipa float . Nakon što se ustanovi odnos između vraijbli, ispisuje se odgovarajuća poruka. Odgovorite zašto nakon tvrdnji if, else if, i else nismo morali otvarati blok pisanjem vitičastih zagrada? Diskutirajte što se događa za sve moguće odnose dvije varijable ( fVar1 > fVar2, fVar1 == fVar2 i fVar1 < fVar2 ). float fVar1, fVar2; // ... ... // ... ... if (fVar1 < fVar2) cout << "fVar1 = " << fVar1 << "\t<\t" << "fVar2 = " << fVar2 << "\n" << endl; else if (fVar1 == fVar2) cout << "fVar1 = " << << "fVar2 = " << << endl; else cout << "fVar1 = " << << "fVar2 = " << << endl;
fVar1 << "\t=\t" fVar2 << "\n" fVar1 << "\t>\t" fVar2 << "\n"
Zadatak 5.5 Dodajte programskom odsječku iz gornjeg primjera unos vrijednosti za varijable, testirajte program, te ujedno ponovite mogućnosti različitog upisa vrijednosti tipa float prema zaključcima iz prethodne vježbe. Zadatak 5.6 Zadane su tri varijable tipa float, fVar1, fVar2, fVar3. Sastavite tvrdnju selekcije koja određuje maksimalnu i minimalnu vrijednost, pohranjuje ih u varijablama fMin i fMax. Napišite program koji učitava naveden tri varijable, te ispisuje minimalnu i maksimalnu vrijednost na ekran konzolne aplikacije.
Izbor tipa skretnice (engl. switch). Kad se izbor vrši na temelju nekog diskretnog obilježja s više vrijednosti, tada ga je najlakše ostvariti s pomoću «skretničkog» izbora koji ostvaruje tvrdnja * switch. Vrlo često je to slučaj kad je svojstvo predstavljeno diskretnom varijablom cjelobrojnog ili korisnički definiranog pobrojanog tipa. Uporaba ove tvrdnje je pogotovo praktična kad obilježje ima više od dvije vrijednosti, u kom slučaju gomilanje if – else if tvrdnji postaje manje pregledno. Također, mnogi kompilatori imaju mehanizme za optimizirano prevođenje ove tvrdnje za veći broj slučajeva, što će rezultirati bržim strojnim kodom u odnosu na spomenute višestruke if – else if tvrdnje. Sintaksa switch tvrdnje opisana je na sljedeći način: switch ( izraz0 )
{
//
izraz0 diskretnog tipa: logički, cjelobrojni, pobrojani
deklaracije unutar bloka // ako su potrebne ... ... ... ... case konstantan_izraz_1 : // Izraz iza ključne riječi case mora biti diskretan,
// i konstantan, tj. poznat u vrijeme prevođenja!
{
blok tvrdnji koje se izvode ako je izraz0 == konstantan_izraz_1 ;
}
*
Engl. naziv switch u ovom kontekstu odgovara hrvatskoj riječi skretnica.
49 break;
// Bezuvjetan skok na kraj tijela tvrdnje switch. Ako se ispusti, oprez! // Tada se događa tzv. "propadanje " (engl. fall through!): // – bez daljnje provjere jednakosti izraza izvršavaju se svi blokovi, // sve do prve sljedeće break tvrdnje, ili do kraja switch tvrdnje.
case konstantan_izraz_2 :
{
blok tvrdnji koje se izvode ako je izraz0 == konstantan_izraz_2 ;
}
break; // Skok na kraj tijela tvrdnje. Ako se ispusti, oprez: slijedi "propadanje"!
... ... ...
... ... ...
case konstantan_izraz_n :
{ }
blok tvrdnji koje se izvode ako je izraz0 == konstantan_izraz_n ;
break;
// Skok na kraj tijela tvrdnje. Ako se ispusti, oprez: slijedi "propadanje"!
default : // Navodi se po potrebi.
{ blok tvrdnji koje se izvode ako izraz0 nije jednak nit jednom konst. izrazu navedenom iza ključnih riječi case ; }
} Iza ključne riječi switch, u okrugloj se zagradi navodi diskretni izraz, cjelobrojnog, logičkog ili pobrojanog tipa. Nakon nje dolazi blok označenih tvrdnji (engl. labeled statements). Naravno, u skladu s proizvoljnom hijerarhijom blokova, označene tvrdnje i same mogu predstavljati nove blokove tvrdnji. Označene tvrdnje se navode iza ključne riječi case i konstantnih izraza, gore označenih kao: konstantan_izraz_1 , konstantan_izraz_2 , … … , konstantan_izraz_n . Svi ovi izrazi moraju imati konstantu vrijednost, tj. moraju biti poznati u vrijeme prevođenja, i moraju biti međusobno različiti. Nakon ovih izraza piše se znak ':' , poslije kojeg se navodi tvrdnja, odnosno blok tvrdnji. Blok tvrdnji se izvršava ako je izraz0 jednak izrazu označenom s konstantan_izraz_i , gdje je i = 1, 2, … … , n . Ukoliko je kriterij izbora jedinstven, tj. ako je pojedini blok tvrdnji potrebno izvršiti za jednu i samo jednu vrijednost izraza izraz0, tada se poslije svakog bloka tvrdnji koji slijedi iza ključne riječi case, umeće tvrdnja break, kao što je navedeno u gornjem opisu. Ona vrši bezuvjetan skok na kraj tijela switch tvrdnje (vidi niže za njenu uporabu u drugim tvrdnjama za kontrolu toka programa). Ovo je ujedno i najjednostavniji slučaj uporabe tvrdnje switch , čije je izvršavanje najlakše kontrolirati. Ukoliko se break ispusti, skretnička tvrdnja u C / C++ povinuje se mehanizmu „propadanja“ (engl. fall through). Tok izvođenja programa se nastavlja kroz sve preostale označene tvrdnje, do prve sljedeće break tvrdnje ili do kraja switch tvrdnje. Drugim riječima, tada se nakon izvođenja bloka čiji je konstanti izraz jednak početnom izrazu, bez ikakve daljnje provjere izvršavaju sve označene tvrdnje (blokovi) redom. Navedeno se propadanje zaustavlja jedino nailaskom na tvrdnju break , ili nailaskom na kraj tijela skretničke tvrdnje. Npr. kad bismo u gornjem opisu ispustili sve tvrdnje break , tada bi se za svaki slučaj jednakosti konstantnog izraza s početnim izrazom tvrdnje switch , pored bloka tvrdnji vezanog za dotični konstantni izraz, izveli i svi blokovi ispod njega, a također i onaj iza ključne riječi default. Navedeni mehanizam propadanja može se dobro iskoristiti kada za više različitih slučajeva treba izvršiti iste blokove tvrdnji. S druge strane, zbog dodatne kompleksnosti nastale ispuštanjem pojedi-
50
nih tvrdnji break , svaku je granu izvođenja potrebno pažljivo provjeriti, i to krećući od ulazne točke određene konstantnim izrazom iza ključne riječi case, pa sve do prve sljedeće tvrdnje break , odnosno do kraja tijela tvrdnje switch . Na koncu, uporabom ključne riječi default možemo navesti koji će se blok tvrdnji izvršiti za slučaj različit od svih onih navedenih iza ključnih riječi case, dakle u gornjem opisu slučaj kad je izraz0 različit od svih n konstantnih izraza. Primjer 5.7 Sljedeći programski odsječak unosi nepredznačenu cjelobrojnu varijablu usOcjena, koju zatim ispisuje zajedno s njenim hrvatskim nazivom. // Hrvatske ocjene unsigned short int usOcjena; cout << "Unesite ocjenu od 1 do 5 : "; cin >> usOcjena; cout << "\nOcjena: " << usOcjena << " = "; switch (usOcjena) { case 5 : cout break; case 4 : cout break; case 3 : cout break; case 2 : cout break; case 1 : cout break;
<< "izvrstan"
<< "\n";
<< "vrlo dobar" << "\n"; << "dobar"
<< "\n";
<< "dovoljan"
<< "\n";
<< "nedovoljan" << "\n";
default : cout << "neispravna ocjena!" << "\n"; break; // Je li ovaj break neophodan? } cout << endl;
Zadatak 5.8 Definirajte pobrojani tip tjedan, koji definira dane u tjednu. Zatim po uzoru na gornji primjer sastavite program koji unaša broj dana u tjednu i ispisuje njegov naziv. Pri tom želimo da dan broj 1 bude ponedjeljak, a dan broj 7 nedjelja. Dodajte još jednu skretničku tvrdnju koja će korištenjem svojstva propadanja, ispisivati je li to radni dan (ponedjeljak do petak), ili dan vikenda (subota i nedjelja). Usporedba tvrdnji tipa switch i if – else if . Kao što je već rečeno, tvrdnja switch ima ograničenja glede forme svog glavnog izraza ( izraz0 ) — njegov rezultat mora biti diskretnog tipa. Čak i za izraze relacijskog tipa, koji vraćaju vrijednost logičkog tipa, prevodilac će dojaviti upozorenje da je to neočekivano za switch tvrdnju. Kod nje se u prvom redu očekuju cjelobrojni izrazi koji će davati rezultat na temelju iznosa neke variajble ili odgovarajućeg, najčešće, cjelobrojnog aritmetičkog izraza. Želimo li ipak koristiti relacijski izraz za izraz0, a izbjeći upozorenja, moramo izvršiti izričit nabačaj tipa bool , nakon čega kompilator više ne ispisuje upozorenje. Radi vježbe, dajemo ovdje primjer zamjene if – else if tvrdnje iz primjera 5.4 s pomoću dvije, ugniježđene switch tvrdnje. Primjer 5.9 Ostvarenje if – else if – else tvrdnje s pomoću switch . Zadatak iz primjera 5.4 ostvaren s pomoću switch tvrdnje. Cilj nam je ilustrirati posebnost selekcije ostvarene tvrdnjom skretničkog tipa. Uočite da je realizirani kôd kompleksniji, manje pregledan i teže čitljiv nego onaj ostvaren s pomoću if – else if tvrdnje.
51 // Switch statement versus if – else if statement. // Real variable comparison by the switch statement: float fVar1, fVar2; // ... ... switch ( (bool) (fVar1 < fVar2) ) // fVar1 < fVar2 { case true: cout << "fVar1 = " << fVar1 << "\t<\t" << "fVar2 = " << fVar2 << "\n" << endl; break;
//
fVar1 < fVar2
case false: // fVar1 >= fVar2 { switch ( (bool) (fVar1 == fVar2) ) // fVar1 == fVar2 { case true: cout << "fVar1 = " << fVar1 << "\t=\t" // fVar1 = fVar2 << "fVar2 = " << fVar2 << "\n" << endl; break; default:
}
cout << "fVar1 = " << fVar1 << "\t>\t" // fVar1 > fVar2 << "fVar2 = " << fVar2 << "\n" << endl;
}
}
Iteracija (ponavljanje) Ponavljanje dijelova programa predstavlja vrlo čestu programsku strukturu koje se još naziva petlja (engl. loop). Jezik C/C++ posjeduje tri strukturirane petlje: while, do-while i for. Navedene petlje se nazivaju strukturirane stoga jer je način njihovog izvršavanja strogo određene forme ili strukture. Tako je uvjet kojim se određuje hoće li se ponavljanje dijela programa izvršavati i dalje ili će se prekinuti, uvijek na određenom mjestu, bilo na početku, bilo na kraju. Način na koje se izvršavaju dodatne radnje, što vrijedi za petlju for, također je preciziran. Time se programera navodi da koristi standardne obrasce prilikom kreacije programskih iteracija, što je presudno za preglednost i provjerljivost programa. while petlja. Ukoliko u iterativnoj programskoj strukturi broj ponavljanja nije poznat, odnosno ako ga u fazi pisanja programa ne možemo ili ne želimo vezati uz vrijednost neke varijable, tada koristimo while petlju. U tim slučajevima obično ne postoji karakteristični indeks iteracije, kao što je to slučaj npr. kod for petlje (vidi dalje). Sintaksa while petlje je sljedeća: while ( condition ) statement ;
Ključna riječ while (od engl. while = hrv. dok, za vrijeme), prethodi okruglim zagradama unutar kojih se navodi neki općeniti uvjet koji smo nazvali condition. Iza toga slijedi općenita tvrdnja, odnosno blok tvrdnji statement. Navedenu while tvrdnju možemo na hrvatskom pročitati kao: dok je uvjet condition zadovoljen izvršavaj tvrdnju statement, a u nastavku zaključujemo: nakon što uvjet condition prestane biti zadovljen, izvršavanje se okončava, i prelazi se na sljedeću tvrdnju programa (ako takva postoji). Izvršavanje ove tvrdnje precizno ćemo opisati sljedećim algoritmom:
52
Alogritam: izvršavanje while petlje. i. Provjeri se vrijednost uvjeta condition. Ako vrijedi da je: condition == true, tj. ako je on istinit, ili zadovoljen, odnosno u skladu s C/C++ stilom, ako je taj izraz različit od 0 ( = false ), prelazi se na korak ii. Ako vrijedi da je: condition == false , tj. ako je uvjet lažan, odnosno nije zadovoljen, tada se „ izlazi“ iz petlje, tvrdnja statement se (više) ne izvršava, već se prelazi na sljedeću tvrdnju iza petlje while. ii. Izvršava se tvrdnja, odnosno blok tvrdnji statement, te se izvođenje vraća na korak i. U programerskom žargonu, dio petlje u kojem se provjerava uvjet petlje može se nazvati i glava petlje (engl. loop head), a blok tvrdnji koji se izvršavaju u slučaju zadovoljenja uvjeta tijelo petlje (engl. loop body). Prvi naziv (glava petlje) je manje prikladan za petlje koje provjeravaju uvjet na kraju (vidi petlju do – while). Promotrimo li sintaksu while petlje, jasno je da unutar bloka tvrdnji statement moraju biti osigurane radnje koje će mijenjati uvjet, tako da se nakon potrebnog broja ponavljanja on promijeni u vrijednost condition == false , te da se pri prvoj sljedećoj provjeri uvjeta petlje iteracija prekine. Pošto se uvjet kod while petlje ispituje na početku, ako je on inicijalno neistinit, petlja se neće izvršiti niti jedanput. Prilikom provjere uvjeta petlje, vrijede standardna C / C++ pravila za interpretaciju vrijednosti izraza koji nisu logičkog tipa: sve vrijednosti različite od 0 interpretiraju se kao true, a vrijednosti 0 kao false (vidi pogl. 3, logički tipovi). Npr. postavimo li kao uvjet cjelobrojnu varijablu, kao u sljedećem primjeru: int i ; // condition == counter // ... ... while ( i ) statement ;
// Avoid this! Condition is not of logic type. Formally incorrect.
tvrdnja statement se izvršava u slučajevima dok je i ≠ 0, dakle za sve pozitivne i negativne vrijednosti ove cjelobrojne varijable. Izvršavanje se prekida kad se prilikom provjere uvjeta petlje ustanovi da je i = 0 , što je ekivalentno sudu da je uvjet false. Odmah treba napomenuti da je formalno nekorektno za uvjet petlje rabiti izraze koji nisu logičkog tipa, bez obzira što to C / C++ kompilator dozvoljava. U praksi se to često ignorira, upravo zbog lakoće i jednostavnosti uporabe izraza drugih tipova. Međutim, to često rezultira logičkim greškama koje je teško pronaći, pa se programerima, pogotovo početnicima, sugerira da izbjegavaju takav način skraćivanja programskog koda. Tako bi formalno korektan odsječak programa istovjetan prethodnom, glasio: int i ; // counter == conditioin // ... ... while ( i != 0 ) statement ;
// Formally correct condition of logic type.
Ako se za uvjet while petlje postave logičke konstante, postoje sljedeća dva slučaja: i)
while ( false ) statement ;
ii)
while ( true ) statement ;
// useless loop //
infinite loop
Prvi slučaj predstavlja beskorisnu petlju (engl. useless loop), koja se neće izvršiti niti jedanput. Drugi je slučaj tzv. beskonačne petlje (engl. infinite loop), iz koje se regularnim načinom ne može izaći, osim ako se unutar tijela petlje izvrši tvrdnja break (vidi dalje). Pripomenimo odmah da se izvođenje programa tipa konzolne aplikacije u Windows okruženju može prekinuti za željeno vrijeme pritiskom
53
tipke Pause/Break, a nastaviti pritiskom bilo koje druge tipke. Prisilni izlazak iz programa može se ostvariti pritiskom Ctrl+Pause/Break. Ovo, naravno vrijedi i za prekid, odnosno izlazak iz beskonačne petlje. while petlja predstavlja realizaciju osnovne iterativne programske strukture s pomoću koje se mogu ostvariti sve ostale. Većina formalno uzornih programskih jezika (Algol, Pascal, C / C++) ima implementiranu ovu petlju (kod Algola se ona dobiva ispuštanjem nepotrebnih elemenata generalizirane tvrdnje za iteraciju). U jezicima koji to nemaju, programer će iznaći način njene najbolje implementacije i rabiti ga uvijek kad je potrebno ostvariti ovu vrstu iteracije (u svezi s time vidjeti odjeljak o goto tvrdnji). Stoga je dobra vježba da se sve druge petlje, kao npr. one izložene u daljnjem tekstu, izraze preko nje.
Primjer 5.9 Sljedeći programski odsječak osigurava ispravan unos ocjene u hrvatskom školskom sustavu, od 1 do 5 (tzv. ulazni filtar, engl. input filter). Unos se ponavlja sve dotle dok korisnik unosi neispravnu veličinu za ocjenu, a završava kad je uneseni podatak cijeli broj u rasponu od 1 do 5. short int sOcj; cout << "Unesite ocjenu (1 - 5): "; cin >> sOcj; while ( !((sOcj >= 1) && (sOcj <= 5)) ) { cout << "Ocjena " << sOcj << " je neispravna!\n\n" << "Unesite ocjenu (1 – 5): "; cin >> sOcj; }; cout << "Unesena ocjena = " << sOcj << "\n" << endl;
Zadatak 5.10 U gornjem primjeru pojednostavite uvjet petlje (djelujte operatorom negacije na izraz u zagradi, te promijenite relacije na odgovarajući način). petlja. U primjeru 3.4 smo imali slučaj ponavljanja u kojem se ono mora izvršiti barem jednom, jer se unos podatka mora obaviti prije nego što podatak testiramo. Za ostvarenje takve iteracije zgodnije je koristiti petlju tipa do–while. Njen opći oblik je:
do-while
do statment while ( condition ) ;
Jedina razlika u odnosu na standardnu while petlju je u tome što se uvjet ispituje na kraju. Najprije se izvrši tvrdnja (blok tvrdnji) statement, pa se tek onda ispituje uvjet condition. S pomoću ove iteracije Primjer 5.8 možemo elegantnije i kraće zapisati na sljedeći način. Primjer 5.11 Ulazni filtar ocjena uz uporabu do–while petlje. unsigned int uOcj; do { cout << "Unesite ocjenu (1 – 5): "; cin >> uOcj; } while ( !((uOcj >= 1) && (uOcj <= 5)) ); cout << "Unesena ocjena = " << uOcj << "\n" << endl;
54
petlja. for petlja je standardna C/C++ iterativna struktura, koja se koristi uvijek kad je broj ponavljanja „poznat“, u smislu da je vezan uz neku varijablu programa, bilo izravno, izračunom, ili tu vrijednost unosi korisnik. Tada se uvodi tzv. brojač ili indeks petlje, kojem definiramo početnu i konačnu vrijednost, te ga za vrijeme izvođenja mijenjamo za određeni korak. S pomoću ove tri vrijednosti kontroliramo izvođenje iteracije. Vrlo je čest slučaj uporabe for petlje za „prolaz“ kroz strukturu poretka (polja ili niza, vidi poglavlje 6), odnosno za pristup njenim elementima. for
Struktura ove petlje dana je sljedećim opisom: for ( init-expr; cond-expr; loop-expr ) statement ;
Ključna riječ for (od engl. za, „za vrijednosti“, i sl.) označava da se tvrdnja statement izvršava opetovano (iterativno) za određeni raspon vrijednosti brojača ili indeksa petlje određenog njenim trima izrazima. Dogled varijabli deklariranih u izrazima unutar zagrade for petlje je samo unutar njenog tijela, tj. vrijedi samo unutar (bloka) izraza statement . Izrazi mogu biti i prazni, tj. mogu se ispustiti neki ili svi, ali je pisanje delimitera ; obvezatno. Na početku se izvrši početni (inicijalizacijski) izraz init-expr (od engl. inital expression). Zatim se, slično kao kod while petlje provjeri uvjetni izraz cond-expr (od engl. conditional expression). Ako je on istinit, izvršava se tijelo petlje predstavljeno tvrdnjom statement , nakon čega se izvrši izraz petlje loop-expr (od engl. loop expression). Potom se ponovo prelazi na provjeru uvjetnog izraza cond-expr , čime je krug petlje zatvoren. Petlja nastavlja s radom sve dok je uvjetni izraz istinit, odnosno prekida s radom kad on postane lažan, nakon čega se nastavlja izvođenje sljedećih tvrdnji programa. Precizno je izvršavanje for petlje opisano algoritmom koji slijedi. Unutar algoritma odmah je opisana i uobičajena namjena pojedinih izraza petlje. Algoritamski prikaz izvršavanja for petlje. i.
Najprije se, i to samo jednom, izvrši inicijalizacijski izraz init-expr. Uobičajena namjena. Inicijalizacijskim izrazom se uobičajeno deklarira kontrolni indeks ili brojač petlje (ako se ne koristi indeks deklariran prethodno), te postavlja njegova početna vrijednost.
ii. Provjerava se uvjetni izraz cond-expr , te ako je on istinit , odnosno u skladu s C/C++ stilom, ako je taj izraz različit od 0 ( = false u C++) , prelazi se na sljedeći korak iii. Ako izraz nije istinit, tj. ako jest nula, tada je izvršavanje for petlje gotovo, i program se nastavlja izvođenjem sljedećeg izraza iza for petlje. Uobičajena namjena. Uvjetni izraz se najčešće odnosi na provjeru veličine indeksa petlje. Npr. ako indeks petlje raste od neke početne manje vrijednosti do konačne veće, ovdje se provjerava je li indeks još uvijek manji (ili manji ili jednak) od te granične gornje vrijednosti. Ako indeks petlje pada prema manjim vrijednostima, ovdje provjeravamo je li on još uvijek veći (ili veći ili jednak) od granične donje vrijednosti. iii. Izvršava se tvrdnja ili blok tvrdnji statement. Uobičajena namjena. Proizvoljna, prema potrebi zadatka koji ostvarujemo. iv. Izvršava se izraz petlje loop-expr. Uobičajena namjena. Ovaj izraz se najčešće odnosi na promjenu indeksa petlje za određeni korak, tj. njegovu inkrementaciju ako indeks raste, odnosno dekrementaciju ako indeks pada. v. Petlja se vraća na korak ii.
55
Realizacija for petlje s pomoću while. Petlja tipa for općenitog oblika: for( init-expr; cond-expr; loop-expr ) statement ;
može se ostvariti s pomoću while petlje na sljedeći način: init-expr; while( cond-expr ) { statement; loop-expr; };
Slijedimo li tijek izvršavanja gornje while petlje, lako je uočiti da to točno odgovara algoritamskom opisu izvršavanja for petlje. U praksi ćemo, naravno, koristiti implementiranu for petlju, koju C / C++ prevodioci prevode u vrlo učinkovitu iterativnu strukturu, podešenu procesorima korištene računarske platforme. Implementaciju s pomoću while petlje uputno je koristiti u drugim jezicima koji nemaju for petlju, odnosno prilikom izvedbe ove iterativne strukture u strojnim jezicima. Primjer 5.12 Sljedeća for petlja zbraja n prvih prirodnih brojeva, gdje je n dan nepredznačenom varijablom uN. unisgned int uN, uSum_n; // ... ... uSum_n = 0; for (unsigned int i = 1; i <= uN; i++) uSum_n += i; Prethodni primjer je proširen u kratak program s unosom i ispisom: #include using namespace std;
int main() { unsigned int uN; unsigned int uSumN = 0; unsigned int uSum_nG ;
// Prirodni broj // Suma prvih n = uN prirodnih brojeva // Suma prvih n = uN prirordnih brojeva // dobivena preko sume aritm. reda
cout << "Suma prvih n prirodnih brojeva\n" << "=========================================\n\n" << "Unesite n = ";
cin
>> uN;
for (unsigned int i = 1; i <= uN; i++) uSum_n += i; cout << "\nSuma dobivena zbrajanjem, S[i = 1 do i = " << uN << "] = " << uSum_n << "\n" << endl; // Dovršite izračun sume aritmetičkog reda koristeći // formulu iz log. tablica ili drugog izvora: // uSum_nG = ... ...
56 // Ispišite rezultat prema gornjem uzoru: // "Suma dobivena formulom, ... ... return 0; }
Zadatak 5.13 Testirajte gornji program i provjerite rezultate za nekoliko vrijednosti n, između ostaloga i za n = 10, 102, 103, 104, 105, 106 . Jesu li posljednji rezultati u redu? Dodatna pitanja: a) Jeste li prepoznali o kakvoj se sumi radi? To je suma jednostavnog aritmetičkog niza. Postoji priča da je gornji zadatak za n = 100 riješio mladi Gauss još u osnovnoj školi, kad je učitelj, želeći malo odmora, zadao djeci da zbrajaju redom brojeve od 1 do 100. Navodno je Gauss je riješio konkretan zadatak za nekoliko minuta, potpuno iznenadivši učitelja. Nije sigurno da je priča istinita, no Gauss jest kasnije pronašao i opću metodu, tj. formulu za sumu osnovnog aritmetičkog niza n
∑i = i =1
n( n + 1) . 2
b) Ako postoji eksplicitna formula, tj. gotov izraz za izračun gornje sume, je li kod pravih izračuna opravdano rabiti gornji program ? Ilustrirajte to razmatrajući zaključke testiranja, odnosno za neki veliki n, npr. n = 108. c) Izvedite opći zaključak: je li opravdano „programiranjem“ i nepotrebno dugotrajnim računom nadomještavati nedostatak matematičkog znanja? Realizacija petlje tipa for s pomoću while petlje. Petlja tipa for općenitog oblika: for( init-expr; cond-expr; loop-expr ) statement
može se ostvariti s pomoću while petlje na sljedeći način: init-expr; while( cond-expr ) { statement; loop-expr; }
U praksi to nije potrebno činiti, jer je C/C++ for petlja vrlo učinkovita iterativna struktura. Primjer 5.14 Radi vježbe, dajemo petlju iz primjera 3.10 realiziranu s pomoću while tvrdnje. unsigned int uN, uSum_n; // ... ... uSum_n = 0; int i = 1; while ( i <= uN ) { uSum_n += i; i++; }; Ovo se dade elegantno skratiti u petlju koja sadrži samo jednu tvrdnju: int i = 1;
57
while ( i <= uN ) uSum_n += i++; Odabir predznačenog i nepredznačenog tipa brojača petlje. Vrlo često, po prirodi stvari, brojač petlje ne može biti negativan, tj. ograničen je da bude iz skupa N0+ = N U 0 = { 0, 1, 2, 3, … }* . Već smo napomenuli da se for petlje često vežu uz algoritme na podatkovnim strukturama poretka, a za indekse te strukture u C / C++ vrijedi također da su pozitivni brojevi veći ili jednaki nuli (vidjeti sljedeće poglavlje). Stoga je za brojače najprirodnije odabrati upravo nepredznačeni cjelobrojni tip. Pored ovog formalnog računarskog razloga, dodatan je argument taj što nepredznačeni tip ima dvostruko veći opseg brojeva, što u nekim slučajevima može biti od velike koristi. Kod implementacije petlji s inkrementirajućim brojačima, dakle brojačima koji kreću od neke manje vrijednosti (vrlo često od 0 ili 1) prema nekoj maksimalnog vrijednosti, uvijek je dobro primjenjivati gornji zaključak o uporabi nepredznačenih brojača. S druge strane, kod dekrementirajućih petlji, tj. onih kod kojih se indeks petlje spušta od neke maksimalne vrijednosti prema manjoj (uobičajeno prema 1 ili 0), treba voditi računa ako je ta manja vrijednost 0. Iako je 0 obuhvaćena nepredznačenim tipom, do problema može doći kod provjere uvjeta petlje nakon dekrementacije vrijednosti 0. Napomenimo ukratko da, se u tom slučaju može ili rabiti predznačeni tip brojača i ostaviti jednostavan, očiti uvjet petlje, ili se može zadržati nepredznačeni tip i modificirati uvjet petlje. O tome detaljnije govori sljedeći odjeljak. Važna je napomena, da se uvijek nastoje uskladiti tipovi podataka za indeks petlje, njenu početnu vrijednost, te za vrijednost koraka. To će bitno doprinijeti konzistentnosti petlje i smanjiti moguće izvore logičkih pogrešaka. Ova diskusija, kao i ona u sljedećem odjeljku, vrijedi, naravno, i za brojače while tvrdnji koji simliraju for petlje na prethodno navedeni način. Petlje s dekrementirajućim brojačima. Vrlo često je potrebno ostvariti petlju u kojoj se brojaču smanjuje vrijednost, tj. kroz petlju se prolazi od neke maksimalne vrijednosti brojača prema manjima, u C / C++ jezicima uobičajeno prema nuli. Ostvarenje takvih petlji je jednostavno. U uvjetu petlje mora se promijeniti relacija brojača naspram donje vrijednosti, te umjesto inkrementacije izvršiti njegovu dekrementaciju. Napomenimo odmah da je kod dekrementirajćih brojača potrebno obratiti posebnu pažnju ako su oni nepredznačenog tipa, te ako se njihova vrijednost spušta do uključivo 0. Za početak promotrimo prijašnje sumiranje prirodnih brojeva koje ćeo ostvariti sumacijom od najveće broja prema 1. Dekrementirajuća for petlja je sljedeća: unsigned int uN, uSumN; // ... ... uSumN = 0; for ( unsigned int u = uN; u >= 1; u-- ) uSumN += u; Ekvivalentna while petlja glasi: unsigned int u = uN; while ( u >= 1 ) uSumN += u--;
*
Skup N0+ a(čitaj: skup N nula plus), unija je skupa prirodnih brojeva i nule.
58
Gornje petlje izvršit će se ispravno, jer nakon zadnje povoljne vrijednosti brojača petlje u = 1 , tijelo petlje se izvrši još jednom, brojač se dekrementira i padne na vrijednost u = 0, nakon čega neispunjenje uvjeta (u više nije veći ili jednak 1) rezultira okončanjem iteracije. Međutim, pretpostavimo da smo indeksom petlje trebali obuhvatiti i vrijednost u = 0 , tj. da petlja glasi: for ( unsigned int u = uN; u >= 0; u-- ) // OPREZ!!! { // BESKONAČNA PETLJA! // Blok tvrdnji: // ... ... } Pažljivi čitatelj će primijetiti problem nastao zbog uproabe brojača neprednačenog tipa. Nakon što se petlja izvrši za zadnju povoljnu vrijednost brojača u = 0, izvrši se njegova dekrementiranje. Međutim, pošto se radi o nepredznačenom tipu, umanjenje vrijednosti za 1 rezultira brojačem koji poprima maksimalnu vrijednosti neprednzačenog tipa u aritmetici modulo 232, pa vrijedi u = Nu, max = = 232 – 1 = 4 294 967 295 . Uvjet petlje je ponovo ispunjen i njeno izvršavanje se nastavlja bez okončanja, tj. gornja iteracija predstavlja tzv. beskonačnu petlju. Jednostavno rješenje gornjeg problema je već natuknuto u prethodnom odjeljku: promijeniti tip brojača u predznačeni, zbog čega će dekrementacija nule rezultirati ispravnim rezultatom od –1 , i okončanjem petlje: for (int i = uN; i >= 0; i-- ) { // Blok tvrdnji: // ... ... }
// Korektna petlja.
Ukoliko takvo rješenje nije zadovoljavajuće, odnosno ako želimo zadržati formalno korektniji tip brojača, jednostavna preinaka uvjeta će riješiti problem. Već smo uočili da je nakon zadnje željene vrijednosti brojača u = 0, potrebno okončati petlju. Pošto se brojač spušta na gore navedenu vrijednost Nu, max , koju jednostavno možemo dobiti dekrementiranjem nule (Nu, max = 0 – 1= – 1), uvjet treba promijeniti na sljedeči način: for ( unsigned int u = uN; u < -1; u-- ) //Korektna petlja za { // uN < Nu,max = // Blok tvrdnji: // = 4294967295 // ... ... } Početna vrijednost brojača je neki pozitivni broj uN za kojeg mora vrijediti: uN < Nu, max . Dakle broj ne smije biti jednak maksimalnom nepredznačenom broju Nu, max koji je u memoriji računala napisan na istovjetan način kao i predznačena vrijednosti –1. Petlja se izvršava, i indeks u se dekrementira. Za vrijednost brojača u = 0 on je i dalje manji od Nu, max (naravno, jer smo ga ionako stalno smanjivali!), pa još uvijek vrijedi uvjdet petlje. Kao što smo i željeli, za tu se vrijednost još izvrši tijelo petlje. Brojač se ponovo dekrementira, ali sada dolazi do obmatanja brojeva, vrijednost brojača postane u = Nu, max = 4 294 967 295 , pa brojač više nije manji od Nu, max nego jednak, pa uvjet više ne vrijedi. Izvršavanje petlje je okončano, s upravom navedenom vrijednosti brojača.* Primjer 5.14 Donji programski odsječak koji vrši odbrojavanje od nekog cijelog broja n ≥ 0 do 0, uz korištenje for petlji s dekrementirajućim brojačima. Dodatno, odbrojavanje se može vršiti u koracima k ≥ 1. Tako npr. ako je početni broj paran (neparan) i odaberemo korak 2, bit će odbrojavani *
Pošto je brojač bio deklariran kao varijabla lokalna bloku petlje, po završetku petlje njegova se vrijednost gubi.
59
svi parni (neparni) brojevi do uključivo 0 (1), gdje 0 (1) možemo smatrati najmanjim parnim (neparnim) brojem. Ili, kao sljedeći primjer, ako smo za početni broj odabrali n = 10, a za veličinu koraka k = 3, tada odbrojavanje treba rezulitrati nizom: 10, 7, 4, 1. Proučite programski kôd i uočite koja je od realiziranih petlji problematična. Korigirajte joj uvjet tako da njen rad bude korektan. Poslužite se znanjem stečenim proučavanjem prethodnog primjera. Potrebno je razmisliti kako osigurati ispravan rad za svaku vrijednost koraka k. // Programski odsječak za odbrojavanje od n do 0, uz korak k. int iN, iK = 1; // Podrazumijevajući korak iK = 1 unsigned int uN, uK = 1; // Podrazumijevajući korak uK = 1 cout << " Program za odbrojavanje od n do 0, uz korak k \n" << "========================================================\n" << endl; cout << "Unesite pocetni broj odbrojavanja, cin >> uN;
n = ";
do { cout << "Unesite korak odbrojavanja, k >= 1, k = "; cin >> iK; } while ( iK < 1); cout << endl; iN = (int) uN; uK = (unsigned) iK;
// Prilagodba unesenih vrijednosti na tip int. // Prilagodba unes. vrijed. na unsigned int.
// 1. Petlja s dekrementirajućim predznačenim brojačem: cout << "Odbrojavanje s dekrementirajućim predznačenim brojačem: \n" << "========================================================" << endl; for ( int i = iN; i >= 0; i -= iK) cout << "i = " << i << endl; cout << endl; system("pause"); // Pauza pred izvođenje sljedećeg dijela programa. // 2. Petlja s dekrementirajućim nepredznačenim brojačem (OPREZ!): for ( unsigned int u = uN; u >= 0; u -= uK) // Okončanje petlje!? cout << "u = " << u << endl; cout << endl; // Korigirajte for petlju br. 2 s dekrementirajućim nepredznačenim // brojačem tako da korektno izvršava odbrojavanje! // Modificirajte uvjet petlje tako da se ona ispravno okončava za // proizvoljnu vrijednost koraka uK. // Prilikom testiranja uputno je koristiti posebnu varijablu (uEnd) // za određivanje donje granice uvjeta. // // // // // // // // // // // // // // // // // // // // // // // //
Zadatak 5.15 Na temelju definicije rada odredite koliko se puta izvršavaju i s kojom vrijednosti brojača okončavaju niže navedene for petlje. Donja vrijednost indeksa petlje je n1 , a gornja je n2 , tj. uvijek vrijedi da je n1 ≤ n2 . Korak petlje je k ≥ 1 , dakle pozitivan i veći od 0. Radi jednostavnosti i povećane općenitosti, tj. mogućeg uključenja i pozitivnih i negativnih vrijednosti početne i konačne vrijednosti, sve su veličine deklarirane kao predznačene. Diskutirajte koje su petlje s rastućim, a koje s padajućim brojačem. Za početak pretpostavite da je korak jedinični, k = 1, a potom razmotrite i složenije slučajeve. int iN1, iN2, iK, i;
60 // 1. for petlja for ( int i = iN1; i <= iN2; { // Blok tvrdnji ... } // 2. for petlja for ( int i = iN1; i < iN2; { // Blok tvrdnji ...
i += iK )
i += iK )
} // 3. for petlja for ( int i = iN2; i > iN1; { // Blok tvrdnji ... } // 4. for petlja for ( int i = iN2; i >= iN1; { // Blok tvrdnji ... }
i -= iK )
i -= iK )
Zadatak 5.16 Napišite program koji izračunava aritmetičku sredinu n brojeva ( n > 1 ) tipa float. Korisnika se najprije pita da unese broj n (prikazati nepredznačenom cjelobrojnom varijablom uN), i potom se unaša n realnih vrijednosti, koristeći varijablu fSum. Sumacija se obavlja uzastopnim pridodavanjem unesenih vrijednosti varijabli fSum po uzoru na prethodni primjer. Na osnovi fSum i uN, na koncu se izračuna srednja vrijednost i ispisuje se na ekranu. Zadatak je potrebno riješiti uporabom for petlje. Zadatak 5.17 Napišite C/C++ program koji unosi cijeli broj u rasponu od 1 do 7, te primjenom selekcije tipa switch ispisuje odgovarajući dan u tjednu, prema sljedećoj shemi: 1 = PON, 2 = UTO, … , 7 = NED. Unos treba ponavljati primjenom selekcije tipa do while. Drugim riječima, nakon prvog unosa treba pitati korisnika: «Želite li unositi još brojeva (da / ne, ne = 'n')?». Zadatak 5.18 Napišite C/C++ program koji unosi cijeli nenegativni broj n i primjenom for petlje izračunava n! (čitaj n faktorijela). Definicija funkcije faktorijela glasi: n
n ! = ∏ i = 1 × 2 × ... ... × (n − 1) n , tj. faktorijela od n je umnožak svih prirodnih brojeva od 1 do n. i =1
Dodatno, zbog matematičkih razloga definiciji nalaže da je 0! = 1. Nakon izračuna i ispisa, korisnik može odabrati želi li ponovni unos i izračun, ili završetak programa. Naputak. Za opetovano izvršavanje cijelog programa iskoristite povoljnu iterativnu strukturu. Potrebno je ostvariti ulazni filtar koji kontrolira da broj nije „prevelik“. Provjeru korisnikove želje za ponavljanjem programa ostvarite odgovarajućim upitom. Dodatna pitanja: a) Razmislite o mogućim problemima pri izvršavanju zadatka. Pošto funkcija faktorijela ima trend rasta jednak eksponencijalnoj funkciji, što će se dogoditi već i kod relativno malih n? b) Kako možete proširiti opseg ispravnih rezultata? c) Kako biste dojavljivali premašenje dozvoljenog opsega brojeva? d) Je li dobiveni program računski zahtijevan ili nije, s obzirom da množenje dva n bitna broja ima
61
otprilike težinu n-strukog zbrajanja n bitnih brojeva? e) Ako funkcija faktorijela doseže ogromne vrijednosti već i za relativno male n, ima li je smisla svaki put iznova računati? Kako biste to riješili u praksi? f) Pronađite značenje pojma «robustnost programa», i prodiskutirajte ga u svezi s ovim jednostavnim primjerom. Tvrdnja break. Tvrdnja break okončava izvršavanje najbliže switch, while, do, ili for tvrdnje unutar koje se pojavljuje. Program se nastavlja na prvoj sljedećoj tvrdnji iza one koju smo prekinuli tvrdnjom break. Najčešća je, i opravdana, uporaba ove tvrdnje u gore spomenutoj selekciji tipa switch (vidjeti prethodne komentare). U iterativnim tvrdnjama ona služi za prepoznavanje posebnog slučaja koji prekida daljnje izvođenje (npr. u iteraciji ostvarenoj s for, vidi Primjer 8.9). Međutim, važno je napomenuti da tvrdnja break u tijelu petlje narušava njenu strukturiranost, a posljedično tome i njenu čitljivost. U većini slučajeva moguće ju je izbjeći odgovarajućom preinakom rasporeda tvrdnji i uvjeta petlje. Tako je uporaba break tvrdnje na početku bloka, kao u sljedećem primjeru: while ( condition1 ) { if ( condition2 ) break; statement; };
potpuno neopravdana. Gornju petlju treba zamijeniti sljedećom: while ( condition1 && ! condition2 ) { statement; };
U prvoj petlji smo, pored uvjeta condition1 za ostanak u petlji, imali skriveni i dodatni uvjet za izlzaka iz nje. U drugoj smo petlji saželi oba uvjeta u jedan kombinirani uvjet: petlja se izvršava ako je uvjet condition1 istinit, i ako je uvjet conditon2 neistinit. (Za vježbu raspišite ove logičke tvrdnje na način da napišete uvjete za ostanak u petlji, i zatim za izlazak iz petlje, te primjenom Booleove algebre pređete iz jednog uvjeta u drugi.) U sljedećm primjeru, break tvrdnja je postavljena na kraju bloka tijela petlje: while ( condition1 ) { statement; if ( condition2 ) break; };
Dodatni uvjet izlaska iz petlje „skriven“ je na dnu njenog tijela, što znatno umanjuje njenu jasnoću. I ovaj slučaj možemo jednostavno preinačiti u bolje strukturirani programski kôd na sljedeći način: if (condition1) statement; while ( ! condition2 && condition1 ) { statement; };
U drugom odsječku, osigurali smo da se izraz statement izvrši jednom u ovisnosti o ispunjenju prvog uvjeta. Potom se provjerava uvjet condition2, te ako je on ispunjen izlazi se iz while petlje. Drugim riječima, preduvjet nastavljanja petlje je da taj uvjet nije ispunjen, odnosno da je ispunjena
62
njegova negacija: !condition2. Pored toga, kao i ranije, mora biti ispunjen i uvjet condition1, pa je skupni uvjet modificirane petlje: !condition2 && condition1. Dakle, u novom smo rješenju izbjegli uporabu uvjetne break tvrdnje i sve ostvarili jednakim brojem redova programskog koda. Prednost se sastoji u tome da smo uvjete izvršenja petlje postavili tamo gdje i pripadaju ― na mjesto uvjeta, unutar okruglih zagrada petlje while. Slična razmatranja mogu se izvršiti i kod uporabe tvrdnje break u for petljama. Općenito je načelo da se uporaba ove tvrdnje izbjegava kod svih iterativnih struktura. Tvrdnja goto . Kratka diskusija o ovoj tvrdnji je već izložena u uvodu ovog poglavlja u kontekstu opisa strukturiranih programa. Implementirana je u mnogim programskim jezicima, i kao što je već spomenuto, u nekim je jezicima njena uporaba neizbježna. Njen naziv dolazi od engl. go to, u prijevodu: idi na. Ova je tvrdnja implementirana i u jeziku C / C++, i njena je sintaksa: goto label ;
gdje je label oznaka (engl. label) , tvrdnje (tvrdnje) ili općenito mjesta na koje se vrši skok ili grananje. No zaista, uporaba ove tvrdnje u jezicima sa strukturiranim tvrdnjama za kontrolu toka potpuno suvišna. Stoga za njeno korištenje nema nikakvog teorijskog opravdanja, niti praktične potrebe. S druge strane, potrebno je naglasiti da tvrdnja sama nije bila razlogom da su programi Štoviše, tvrdnja goto u potpunosti narušava principe strukturiranog programiranja. Čak i u „praksi“ se njena uporaba rijetko može opravdati, pa je treba strogo izbjegavati. [Dodati: primjer ostvarenja strukturianih petlji s pomoću if i goto tvrdnje.]
63
Poglavlje 6. Osnovna struktura podatka – poredak. Jedno i više dimenzionalni poredci Već u prijašnjim zadacima nametala se potreba za strukturama podataka. Npr. pri izračunu srednje vrijednosti niza brojeva, bilo bi korisno imati taj niz pohranjen u nekoj takvoj podatkovnoj strukturi, pa bismo mogli izračunati cjelokupnu njegovu statistiku, ili ga po potrebi mijenjati, obnavljati (ažurirati), itd. Odmah uočavamo da je uz određene programske strukture, odnosno algoritme, pogodno rabiti prikladne podatkovne strukture. Nalaženje prikladnih podatkovnih struktura, zajedno s nalaženjem algoritama s pomoću kojih se ste strukture obrađuju, predstavlja temelj računarske znanosti. Struktura podataka (engl. data structure) predstavlja složen tip podataka (engl. complex data types) sastavljen od elemenata nekog osnovnog ili korisnički definiranog tipa. Elementi su organizirani, odnosno pohranjeni u memoriju na specifičan način, te s propisanim metodama pristupanja tim elementima. Dakle, za pojedinu strukturu podataka, pored toga od kakvih se elemenata ona sastoji, te kako su oni raspoređeni u memoriji, presudno je i kako se tim elementima pristupa. Primjerice, podatkovnoj strukturi poretka, koja se sastoji od istovrsnih elemenata jednakog tipa, svakom elementu možemo pristupiti izravno, bez posredništva drugih elemenata. S druge strane, kod strukture stoga (vidi sljedeći odjeljak), dozvoljen je pristup samo njegovom vršnom elementu. Bez odgovarajućih struktura podataka nema ni uspješnog «programiranja», odnosno pisanja algoritama za računala. Problem koji je teško riješiti organizacijom podataka jednom podatkovnom strukturom, često se elegantno i lako rješava uporabom druge, pogodnije, podatkovne strukture. Stoga je poznavanje struktura podataka u praksi jednako važno kao i poznavanje tvrdnji, sintakse i svojstava nekog programskog jezika, a na teorijskoj razini jednako važno kao i poznavanje algoritama.
Temeljna podatkovna struktura: poredak (1-dim.), ili polje, ili niz Poredak ili polje, niz (engl. array = hrv. niz) je temeljna struktura podataka u kojoj su svi elementi jednakog tipa, pa posljedično tome i jednake «duljine» izražene u broju osnovnih memorijskih lokacija (memorijskih zrna), odnosno bajtova. Specifičnost ove strukture je da se svim elementima može pristupiti izravno, na temelju njihovog „indeksa“. „Složenost pristupa“ svakom elementu, a samim time i vrijeme dohvata ili promjene, jednaka je za sve elemente, bez obzira na vrijednost indeksa. Elementi su u poretku smješteni od neke početne memorijske lokacije s poznatom adresom, tako da se svakom elementu može pristupiti (tj. pročitati njegov sadržaj ili ga promijeniti) navođenjem indeksa elementa, na temelju kojeg se adresa svakog elementa lako može izračunati preko sljedeće formule:
A(i ) = A0 + t × i .
(6.1)
A( i ) je adresa elementa s indeksom i, i = 0, 1, … … , n – 1, pa elemenata ima ukupno n. A0 je adresa početnog (nultog) elementa koja se još naziva i bazna adresa (engl. base address), a t je duljina podatkovnog tipa elementa izražena brojem bajtova (B). Podsjetimo, svako «memorijsko zrno» ima jedinstvenu adresu, a standardna veličina zrna na današnjim procesorima opće namjene je upravo 1B. Gornja formula slijedi implementaciju strukture poretka na razini zbirnog jezika procesora, s pomoću tzv. indeksnog adresiranja operanada.
64
Iako programer u višem programskom jeziku ne mora izravno voditi računa o adresama niti poznavati detalje strojnog jezika, svojstava poretka u C/C++ jeziku u potpunosti slijede gornju formulu, pa je vrlo korisno poznavati je. Tako se u poretku s n elemenata, indeksacija uvijek vrši idući od 0 do n – 1. Tj. iako se u matematici pobrojavanje vrši elementima iz skupa N prirodnih brojeva, u računarstvu je zgodnije pobrojavanje početi od 0. Uvrstimo li i = 0 u gornju formulu vidimo da je adresa nultog elementa A( 0 ) = A0 , što upravo odgovara početnoj adresi poretka. Pristup svakom elementu poretka izravno je moguć navođenjem imena poretka i indeksa elementa u uglatoj zagradi, i jednako je brz za sve elemente. To je analogno proizvoljnom pristupu bilo kojoj memorijskoj lokaciji u RAM (engl. Random Access Memory, memorija s proizvoljnim pristupom). Osnovna struktura poretka (niza) je jednodimenzionalna, što znači da posjeduje samo jedan indeks. Pogodna je za pohranu svih matematičkih veličina s jednim indeksom (npr. vektora, elemenata skupova, nizova, itd…). Za prikaz dvodimenzionalnih matrica dimenzija n × m koriste se dvodimenzionalni poredci s dva indeksa. Oni se mogu svesti na jednodimenzionalni poredak (redaka) dimenzije n, kojem su elementi ponovo jednodimenzionalni poredci (stupci) dimenzije m. Deklaracija poretka u C/C++ je slična deklaraciji varijabli. Najprije se navede tip elementa, zatim ime poretka iza kojeg mora uslijediti uglata zagrada i neki od načina specifikacije njegove dimezije. Npr. sljedeća shema deklarira poredak pod imenom ArrayName s elementima tipa type (proizvoljan tip), s ukupno n elemenata, gdje n mora biti konstanta (konstantan izraz) definiran u vrijeme prevođenja: type ArrayName[const. n];
Prevodilac će jednostavno rezervirati n mjesta za elemente tipa type, a vrijednost elemenata ostaje nedefinirana. Pošto se veličina poretka mora unaprijed odrediti, govorimo o tzv. statičkoj strukturi podataka, čija se veličina ne može mijenjati. Nemogućnost promjene ove strukture u tijeku izvođenja u potpunosti je kompenzirana činjenicom da se radi o strukturi podataka s najbržim pristupom svojima elementima, koji su, kao što je već rečeno, svi izravno dostupni. Kao primjer, deklariramo ovdje cjelobrojni poredak pod nazivom iArray veličine 100 elemenata: int iArray[100];
// Deklaracija bez inicijalizacije elemenata.
Dakle, radi se o podatkovnoj strukturi poretka sa 100 cjelobrojnih elemenata tipa int , od kojih svaki odgovara jednoj varijabli tog istog tipa, veličine 4B. Elementi gornjeg poretka su redom:: iArray[0] , iArray[1] , iArray[2]
, …
…
, iArray[98] , iArray[99] .
Gornjom deklaracijom nismo inicijalizirali vrijednosti elemenata, već se to ostavlja „za kasnije“. Npr. u takvim slučajevima za očekivati je da će unos vrijednosti elemenata poretka biti izvršena eksplicitnim pridjeljivanjem vrijednosti svakom elementu posebice, bilo unosom podataka s tastature, bilo izračunom, uz uporabu for petlje koja prolazi kroz sve vrijednosti indeksa, od i = 0, do i = 99. Inicijalizacija vrijednosti elemenata poretka može se obaviti odmah pri njegovoj objavi. U tom slučaju iza objave imena i veličine poretka stavlja se operator pridrživanja i par vitičastih zagrada, unutar kojih se navode vrijednosti elemenata odijeljene zarezima. Ovaj način inicijalizacije nudi i jednostavnu mogućnost postavljanja svih elemenata na vrijednost 0. Tako će sljedećom linijom: int iArray[100] = { };
// Deklaracija i inicijalizacija: svi elementi na 0 .
svi elementi poretka iArray[0] biti postavljen na 0. Nadalje, sljedećom objavom i inicijalizacijom: int iArray[100] = {1, 2, };
// Deklaracija i inicijalizacija: spec. vrijednosti elemenata .
65
početni, 0-ti element će biti postavljen na 1, sljedeći, 1-vi element na 2, a svih ostalih 98 će biti postavljeno na 0. Indeks elementa
i
Adresa elementa
Sadržaj (vrijednost) elementa fX[i]
A( i )
B0
B1
B2
B3
0011 FFF8
##
##
##
##
0011 FFFC
##
##
##
##
0
0012 0000
fX [0]
1
0012 0004
fX [1]
2
0012 0008
fX [2]
3
0012 000C
fX [3]
4
0012 0010
fX [4]
…
…
…
… …
…
…
…
… …
…
…
…
49
0012 00C4
fX [49]
… …
50
0012 00C8
fX [50]
…
…
…
… …
…
…
…
… …
…
…
…
98
0012 0188
fX [98]
… …
99
0012 018C
fX [99]
0012 0190
##
##
##
##
0012 0194
##
##
##
##
Slika 6.1 Prikaz podatkovne strukture poretka u memoriji. Na slici je ilustrirano kako je u glavnoj memoriji računala pohranjen 1-dim. poredak sa 100 elemenata tipa float . Deklaracija je obavljena tvrdnjama: const int cN = 100;
float fX[cN] ;
Uređaj elemenata slijedi formulu (6.1). Element s indeksom 0 je početni element fX[0] koji općenito ima neku adresu A0 unutar glavne memorije računala. Radi jednostavnosti, za nju je pretpostavljen okrugli iznos: A0 = 0012 0000h (za primjer ispisa konkretne vrijednosti vidi zad. xx). Sadržaj i-tog elementa fX[i] pohranjen je na adresi A(i) = A0 + 4 × i , unutar četiri bajta B0 do B3, kojih su adrese redom: A( i ) + 0, A( i ) + 1, A( i ) + 2 i A( i ) + 3. Adresa posljednjeg elementa je: A(100 ) = ( A0 + 4 × 99 ) d = ( A0 + 396 ) d = ( A0 + 18C ) h = 0012 018C h = 0x0012018c . Zadnja vrijednost je napisana u „C / C++ stilu“, malim latinskim slovima A do F, na način kako adrese, odnosno općenito heksadekadske vrijednosti, ispisuju mnogi prevodioci. Gornji poredak ima 100 elemenata duljine 4B i zauzima ukupnu memoriju od 400B. Označimo to kao njegov kapacitet C = 400d B = = 190h B , gdje je drugi prikaz u heksadekadskoj formi. Iz gornjeg izlaganja slijedi da početni, nulti, bajt poretka ima adresu A0 = 0012 0000h , a zadnji, 399-ti A399 = 0012 018F h (primijetite: 190h – 1 = = 18Fh ). Isti prikaz i razmatranje vrijedi za sve poretke s jednakim brojem i duljinom elemenata. Na gornjoj slici prikazano je i osam bajtova prije i poslije deklariranog poretka. Za njih, naravno, ne postoje „legalne“ vrijednosti indeksa, pa nisu niti napisane. Radi se o dijelovima memorije u kojima su pohranjeni drugi sadržaji, kao npr. druge lokalne varijable. Vrijednost ovih bajtova nevezana je uz poredak, te označena s ## Dva znaka # sugeriraju dvije heksadekadske znamenke kojima je u potpunosti određen sadržaj jednog bajta.
Za sve poretke deklarirane na uobičajeni način — kao lokalne, tj. unutar blokova funkcija — sve dok se ne izvrši inicijalizacija elemenata poretka njihova se vrijednost smatra nedefiniranom, odnosno
66
„nepredvidivom“, već prema tome kakav je sadržaj glavne memorije nađen na rezerviranim memorijskim lokacijama. Iznimka od toga su poredci deklarirani kao globalni, dakle izvan blokova funkcija i smještena u statičkom dijelu memorije (vidi prethodno i sljedeće pogl.). Njima prevodilac odmah pridružuje vrijednost 0 (odnosno 0. za tip double, te 0.f za tip float). Razlog za takvo postupanje proizlazi iz same namjene globalnih objavljenih varijabli i podatkovnih struktura. Naime, ako je neki poredak objavljen kao globalni, lako se može desiti da će neka od funkcija koja ga koristi „pretpostavljati“ da je on odgovarajuće pripremljen za uporabu. Npr. to se može postići inicijalizacijom odmah pri objavi, ili bi to bilo ostvareno u glavnoj funkciji programa. Ukoliko to nije napravljeno, početna vrijednost 0 je dobar izbor, koji predstavlja matematički neutralno polazište. Visual C++ prevodilac sâm će inicijalizirati vrijednosti elemenata poretka na uniformne vrijednosti čak i ako to programer nije učinio ni na koji gore opisani način. O kojoj se točno vrijednosti radi, te postupaju li na isti način i prevodioci drugih razvojnih okolina, čitatelj može sâm provjeriti kroz jednostavne testne programe (vidjeti i zadatke koji slijede). Primjer 6.1 Vizualna predodžba podatkovne strukture poretka je vrlo jednostavna, a istovremeno i važna. Radi boljeg zora, skicirajte raspored elemenata (njihove adrese i sadržaj) u memoriji računala, za poredak deklariran na sljedeći način: const int n = 100; float fX[n];
// Konstantna veličina n // Objava jednodimenzionalnog poretka fX // s n elemenata
Adresa poretka, odnosno adresa početnog elementa, neka je A0 = 0000h (adrese se uobičajeno pišu u heksadekadskoj formi). Rješenje je dano na slici 4.1. Zadatak 6.2 Dobro proučite strukturu poretka na primjeru 4.1. i odgovorite na sljedeća dodatna pitanja: a) Provjerite adrese navedenih elemenata, te izračunajte adrese još nekih po vlastitom izboru. b) Odredite koliko elemenata duljine 4B možemo pohraniti u poredak koji zauzima ukupnu duljinu od 1KB, koliko u poredak duljine 16KB, 64KB i 1 MB. c) Može li se struktura poretka primijeniti i na cijelu radnu memoriju računala? Objasnite! d) U svezi s potpitanjem c — ako se sve ostale strukture pohranjuju u memoriju računala, što zaključujete o važnosti strukture poretka? Primjer 6.3 Primjeri deklaracije poretka i inicijalizacije elemenata: a) Deklaracija bez inicijalizacije ostavlja elemente nedefiniranima. U tom slučaju mora postojati eksplicitna definicija dimenzije poretka konstantnim izrazom u uglatoj zagradi. b) Elementi poretka se redom inicijaliziraju na vrijednosti navedene unutar vitičaste zagrade. Ukoliko je ispuštena eksplicitna definicija dimenzije poretka, kao u našem slučaju, dimenzija je jednaka broju elemenata navedenih u listi (odijeljenih zarezima). U našem slučaju deklariran je poredak s elementima tipa float dimenzije 3. c) Elementi liste mogu biti konstante ili proizvoljni C/C++ izrazi (r-value). Provjerite: ako se u vitičastoj zagradi inicijalizira barem jedan element, kolika je vrijednost onih koji nisu inicijalizirani? Kako ćete to koristiti? d) Izvedite zaključak o dozvoljenom broju elemenata liste pri inicijalizaciji poretka eksplicitno zadane veličine. // Konstanta za definiciju veličine poretka: const unsigned int cN = 10; // a) Deklaracija bez inicijalizacije:
67 float
fX[cN];
// Objava jednodimenzionalnog poretka fX // s 10 elemenata tipa float. // Elementi poretka su nedefinirani!
// b) Deklaracija bez eksplicitnog navođenja dimenzije // uz inicijalizaciju iz liste: float fY[] = { 5.2f, 4.7f, 23.4f }; // Dim. poretka = ? // c) Deklaracija s eksplicitnim navođenjem dimenzije // uz djelomičnu inicijalizaciju. Je li što pogrešno? const int i4 = 4; int iA1[cN] = { 1, 2, 3, i4, i4 + 1 }; // Provjerite kolika je vrijednost neinicijaliziranih elemenata // sljedećim ispisom: for(i = 0; i < cN; i++) cout << "iA1[" << i << "] = " << iA[i] << '\n'; cout << endl; // d) Deklaracija s eksplicitnim navođenjem dimenzije // uz inicijalizaciju iz liste. Je li što pogrešno? int iA2[10] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 8 + 1, 9 + 1 };
Pristup elementima poretka. Kao što je već rečeno, a ponavljamo zbog važnosti, i-ti element poretka X dimenzije n se navodi kao X[i] , uz donju granicu indeksa imin = 0 , i gornju granicu imax = = n – 1, tj. raspon raspoloživih indeksa je uvijek: i = 0, 1, 2, … … , n – 1 (vidi formulu (1)). Kao vrijednosti indeksa u uglatoj zagradi mogu se pojavljivati konstante, te bilo koji izrazi (r-vrijednosti). Međutim, svako navođenje nepostojećeg elementa, tj. indeksa vrijednosti i < 0 te i >= n – 1, rezultira pristupu memorijske lokacije koja nije element danog poretka. Pri tom ta, nelegalna, lokacija ima adresu kao što slijedi iz formule (1). Drugim riječima, prevodilac neće upozoriti na „nedozvoljenu“ vrijednost indeksa, već o tome mora brinuti sâm programer. Zadatak 6.4 Odredite vrijednosti elemenata sljedećeg poretka nakon naznačenih operacija. Objasnite način na koji je ostvaren ispis elemenata poretka s pomoću tvrdnje for. // 1-dim poredak tipa short int: const unsigned int cuN = 7; short int sI[cuN] = {1, 2, 3};
// Ispišite vrijednosti svih elemenata // redom, sI[0] = ?, sI[1] = , ...
// Ako niste sigurni, dodajte ovdje njihov ispis: /* cout << "Ispis inicijalnih vrijednosti elemenata poretka \n” << endl; // ...
...
...
*/ // Pristup i aritmet. operacije sI[4] = sI[3]; // sI[4] = sI[5] = ++sI[4]; // sI[5] = sI[5]++ ; // sI[5] = sI[6]-- ;
s elementima poretka: ? ? , sI[4] = ? ?
// Ispis vrijednosti nakon aritmetičkih operacija s elementima: for (int i = 0; i < cuN ; i++) cout << "sI[" << i << "] = " << sI[i] << "\n"; cout << '\n' << endl; // ... ...
68
Zadatak 6.5 Objasnite najprije što je problematično, a zatim pokušajte predvidjeti kako će se prevesti sljedeći programski odsječak. Uočite koje vrijednosti poprima indeks for petlje, te je li to u skladu s dozvoljenim indeksima elemenata deklariranog poretka. Razmislite, hoće li biti grešaka pri prevođenju? Možete li predvidjeti što će se ispisati? Testirajte program i provjerite ispis pokrećući program nekoliko puta. Što zaključujete? const unsigned int cuN = 5; // 1-dim poredak tipa short int: short int sI[cuN] = {0, 1, 2, 3, 4};
// min. indeks, i(sI)min = ? // max. indeks, i(sI)max = ?
// Ispis: for (int i = 0 - 5; i < cuN + 5 ; i++) // Oprez!!! i(sI)min = ? , i(sI)max = ? cout << "sI[" << i << "] = " << sI[i] << "\n"; cout << '\n' << endl; // ... ...
Zadatak 6.6 Potrebno je odgovoriti na sljedeća pitanja: a) Inicijalizira li C++ prevodilac (MS Visual Studio 6.0, platforma Win32) vrijednosti elemenata poretka po njegovoj deklaraciji, iza koje nije bilo eksplicitne inicijalizacije? Specifično, ostaju li elementi takvog poretka potpuno nedefinirani, tj. slučajnih vrijednosti koje odgovaraju prethodnim sadržajima u rezerviranom dijelu memorije, ili se elementi ipak postavljaju na određenu vrijednost? To ćete lako provjeriti jednostavnim programskim odsječkom. Što zaključujete nakon njegovog izvršenja? Je li moguće da se ovakav rezultat dobije slučajno? Obrazložite svoj odgovor i usporedite sa obrazloženjem u zadatku 2.12. b) Često je zgodno da i elementi velikih poredaka budu inicijalizirani na neku željenu vrijednost, npr. 0 ako se radi o brojevnim nizovima. Provjerite možete li to postići i bez inicijaliziranja svakog elementa posebice, tj. bez uporabe for petlje? // a) // Skica rješenja: int iA[0x100]; cout << endl; for (UINT i = 0; i < 0x100; i++) { cout << "[" << i << "]" << iA[i] ; if ( (i + 1) % 4 ) cout << "\t";
}
else cout << "\n";
// b) // Djelomična(?) inicijalizacija: int iY[0x100] = {0, } // Ispis b: // ...
...
...
Zadatak 6.7 Potrebno je objaviti poredak sP tipa short int, dimenzije: const UINT n = 10. Zatim napišite program koji će s tastature unašati vrijednosti za sve elemente poretka. Nigdje, osim na početku kod definicije konstante n ne smije se u programu pojavljivati njena konkretna vrijednost, tj. pisati broj 10 (razmislite zašto!?). Na početku programa ispisuje se poruka korisniku: "Unos niza
69 P od n cjelobrojnih podataka:", gdje n mora imati stvarnu vrijednost. Zatim slijedi unos
podataka, tako da se prilikom unosa vrijednosti za i-ti element korisniku na ekranu pojavi poruka: Unesi podatak P(j) =
gdje je j = i + 1. Tj. za korisnika će pobrojavanje elemenata poretka biti s pomoću indeksa iz skupa prirodnih brojeva. Po završenom unosu program ispisuje poruku "Ispis niza P od n cjelobrojnih podataka:", nakon čega slijedi ispis podataka s porukom: Podatak P(j) =
.
uz konkretnu vrijednost indeksa j.
Dvodimenzionalni i višedimenzionalni poredak Dvodimenzionalni i višedimenzionalni poredak (polje) deklariraju se analogno jednodimenzionalnome. Nakon navoda tipa elemenata, slijedi ime poretka, te onoliko uglatih zagrada sa specifikacijama veličine koliko njegovih „dimenzija“ postoji. Pojam dimenzija se tu odnosi na broj indekasa koji su potrebni za jedinstveno određenje pojedinog elementa poretka. U jeziku C/C++ prirodno je promatrati dvodimenzionalni poredak kao poredak kojem su elementi ponovo poredci. Npr. sljedeća deklaracija dvodimenzionalnog poretka: type tMtrx[n][n] ;
// n, m = integer constants
objavljuje dvodimenzionalno poredak s n × m elemenata tipa type, što možemo zamišljati i kao poredak koje se sastoji od n poredaka s m elemenata tipa type. Matematički gledano, ovakav poredak odgovara matrici dimenzija n × m, s n redaka i m stupaca, ili drugim riječima s n redaka, sa po po m elemenata u svakom retku. Opći element poretka je: tMtrx[i][j]
uz uobičajenu indeksaciju od 0: i = 0, 1, 2, … … n – 1, i j = 0, 1, 2, … … m – 1. Prilikom pohrane u memoriju prevodilac «linearizira» dvodimenzionalni poredak upravo tako da najprije pohrani «prvi» (nulti) redak, tj. elemente: tMtrx[0][0], tMtrx[0][1], … … , tMtrx[0][m-1],
zatim sljedeći redak: tMtrx[1][0], tMtrx[1][1], … … , tMtrx[1][m-1],
itd. do posljednjeg: tMtrx[n-1][0], tMtrx[n-1][1], … … , tMtrx[n-1][m-1].
Radi se o tzv. uređaju po retcima (engl. Row Major Ordering), u kojem se drugi indeks «najbrže vrti». Inicijalizacija elemenata može se vršiti odmah prilikom objave, navođenjem iznosa elemenata u vitičastim zagradama, prema sljedećem primjeru: float fMtrx[ ][2] = { {1.1f, 1.2f}, {2.1f, 2.2f}, {2.1f, 2.2f} } ;
Za prvu dimenziju nije eksplicitno navedena veličina u uglatoj zagradi, već je to, slično kao i kod 1-dim poredaka, određeno brojem elemenata — u ovom slučaju brojem unutarnjih parova vitičastih zagrada. Za sve ostale dimenzije osim prve, nužno je navođenje veličine (granice).
70
Analogno ovome se deklariraju i višedimenzionalni poredci, s tri indeksa ili više (npr. za prikaz tenzorskih* veličina). Uređaj u memoriji je ponovo takav da se najbrže vrti zadnji (najdesniji) indeks. Nakon što desniji indeks poprimi svoju najveću vrijednost, inkrementira sebi lijevi indeks, a sam se postavi na 0, slično kao znamenke na brojaču kilometara. Ovakav uređaj elemenata mora slijediti i inicijalizacija višedimenzionalnih poredaka navođenjem elemenata u vitičastim zagradama. Sljedeći primjer deklarira i inicijalizira 3-dim poredak iTnsr, tako da mu je 12 elemenata u prvom redu postavljeno na vrijednosti koje odražavaju iznose njegovih indeksa uvećanih za 1 (dakle, indeksa prema matematičkom stilu pisanja). Drugih je 12 elemenata postavljeno na 0 (drugi red inicijalizacije): int iTnsr[ ][3][4] = { { {111, 112, 113, 114}, {121, 122, 123, 124}, {131, 132, 133, 134} }, { {0, }, } } ;
Veličina prve dimenzije (2) određena je postojanjem dva para vitičastih zagrada odijeljenih zarezom, unutar vanjskog para istovrsnih zagrada. Primjer 6.7 Deklaracija, inicijalizacija i pristup elementima 2-dim poretka. Najprije je objavljen je dvodimenzionalni poredak tipa float, veličine 3 × 4, što odgovara matrici s 3 retka i 4 stupca. U duhu jezika C možemo reći da se radi o poretku s tri elementa (retka) u kojem je svaki element poretka s četiri elementa (stupca). Promotrite različite načine objave i inicijalizacije elemenata. Odgovorite na pitanja u komentarima. // a) Objava 2-dim poretka bez inicijalizacije: float fMtrx1[3][4] = { {1.f}, }; // Koliko elemenata ima poredak fMtrx1? // Koliko memorije zauzima? // b) Objava uz potpunu inicijalizaciju: float fMtrx2[ ][4] = { {1.1, 1.2, 1.3, 1.4}, {2.1, 2.2, 2.3, 2.4}, {3.1, 3.2, 3.3, 3.4} }; // Koliko elemenata ima poredak fMtrx2? // Koliko memorije zauzima? // c) Objava uz djelomičnu inicijalizaciju // Dopišite vrijednosti svih elemenata po započetom obrascu! float fMtrx3[ ][3] = { {1.1, 1.2, 1.3}, {2.1, 2.2, 2.3}, {3.1, 3.2, }, { }, {5.1 } }; // Koliko elemenata ima poredak fMtrx3? // Koliko memorije zauzima? // Ispis elemenata poredaka. // "Ručni" ispis elemenata fMtrx1: cout << "fMtrx1[0][0] = " << fMtrx1[0][0] << "\tfMtrx1[0][1] = " << fMtrx1[0][1] << "\tfMtrx1[0][2] = " << fMtrx1[0][2] << "\tfMtrx1[0][3] = " << fMtrx1[0][3] << '\n' << "fMtrx1[1][0] = " << fMtrx1[1][0] << "\tfMtrx1[1][1] = " << fMtrx1[1][1] << "\tfMtrx1[1][2] = " << fMtrx1[1][2] << "\tfMtrx1[1][3] = " << fMtrx1[1][3] << '\n' // Dovršite ovaj produženi ispis ... // ... ... ...
// !!! !!!
...
// "Ručni" ispis elemenata fMtrx2 (vidi primjer za fMtrx1): // ... ... ... *
Tenzor (engl. tensor) označava veličine koje se koriste u matematici, fizici i tehnici. One predstavljaju generalizaciju pojma vektora i matrica, te su uobičajeno određene s 2, 3 ili više indeksa.
71 // "Ručni" ispis elemenata fMtrx3 (vidi primjer za fMtrx1): // ... ... ...
Zadatak 6.8 Potrebno je ispisati sadržaj dvodimenzionalnog poretka (slične veličine kao u gornjem primjeru) na dva načina: i) ispisujući elemente redom jedan ispod drugog, po načelu «retci prvo», te ii) ispisujući elemente u dvodimenzionalnu shemu analognu matrici. Dodatna pitanja: a) Na što liči ispis i? Podsjeća li on na način na koji su elementi dvodimenzionalnog poretka pohranjeni u (jednodimenzionalnu) memoriju? Objasnite! b) Je li potrebna velika preinaka programa da se ostvari 2-dim (matrični) zapis?. Ostvarite to. Radi preglednosti otklonite oznaku elementa "sMtrx[i][j] =" iz ispisa i testirajte program. // Objava konstantnih dimenzija: const int cN = 3, cM = 4; // Deklaracija i djelomična inicijalizacija: short int sMtrx[cN][cM] = { {1, 2, 3, 4}, {5, 6, 7, 8 }, {9,
} };
// Ispis podataka o poretku: cout << "2-dim poredak sM dimenzija " << cN << " x " << cM << "tipa short int." << "\n\n"; // Ispis i), rješenje: // Poruka i: cout << "i) Linearni ispis – retci prvo (Row Major Ordering):\n" << "=======================================================\n " << endl; // Vanjska petlja (indeks i = 0, 1, ... , n – 1): for (UINT i = 0; i < cN ; i++) { // Nutarnja petlja (indeks j = 0, 1, ... , m – 1): for (UINT j = 0; j < cM ; j++) cout << "sMtrx[" << i << "][" << j << "] = " << sMtrx[i][j] << "\n" ; cout << "\n" ; // prazna linija nakon svakog retka matrice } // Ispis ii) // Poruka ii: cout << "ii) 2-dim (matrični) ispis:\n" << "=======================================================\n " << endl; // ...
...
...
72
Poglavlje 7. Kazaljke – pokazivački tip. Navodi (reference) Motivacija za rad s adresama. Prilikom programiranja računala na razini zbirnog ili asemblerskog jezika (engl. assembly language), za programera je bilo nezaobilazno poznavanje memorijskih lokacija u kojima će biti pohranjeni ulazni podaci, međurezultati i rezultati. Svaka memorijska lokacija u računalu definirana je svojom jedinstvenom adresom. Nadalje, tip podataka točno definira koliko je osnovnih memorijskih lokacija (ili memorijskih zrna, odnosno bajtova), utrošeno za njegovu pohranu, i za pojedine tipove podataka postojale su zasebne strojne instrukcije. Dakle, prilikom programiranja u jezicima niske razine, podrazumijevao se manje-više izravan rad s adresama i potpuni nadzor nad smještajem varijabli i podaktovnih struktura u memoriji. Tako napisani programi mogu optimalno iskoristiti kapacitete procesora i računala u cjelini. Međutim, njihova je izrada vrlo zahtjevna i dugotrajna. Stoga se danas na standardnim računalima samo najkritičniji dijelovi posebnih programa, kao što su npr. jezgre operacijskih sustava i pogoniteljskih programa (engl. drivers), pišu u zbirnim jezicima procesora. Iako programiranje u višim programskih jezicima oslobađa programera od izravne brige o adresama varijabli i podatkovnih struktura, učinkovito programiranje, a posebice implementacija dinamičkih struktura podataka koje se mijenjaju u vrijeme izvođenja, nije moguća bez dobrog razumijevanja načina njihove pohrane u memorije. Stoga, prije nego što pređemo na obrazloženje pojmova vezanih uz pokazivački i navodnički tip podataka, korisno je ponoviti osnovne činjenice o organizaciji memorije računala. Naslovni prostor i programski model memorije*. Programer mora razumijeti osnovne memorijske koncepte kao što su naslovni ili adresni «prostor» (engl. address space) i programski model memorije (engl. programming memory model). Programski model memorije je jednostavan, funkcionalni, model memorije, kako ga vidi „programer“. Pri tom pod programerom podrazumijevamo i tvorca operacijskog sustava. Ukratko, adrese iz naslovnog prostora služe za jedinstveno određenje memorijskih lokacija, slično kao što kućna adresa po shemi: država, grad, ulica i broj, jedinstveno određuju neku poštansku lokaciju na našoj planeti. Na temelju kućne adrese, možemo poslati pošiljku na to mjesto, ili pak je preuzeti. Na računalima su adrese prikazane u njima najprimjerenijoj formi — kao binarni brojevi. U ovisnosti o tome koliko smo bitova predvidjeli za njihovo određenje, toliko ćemo različitih adresa imati, o čemu govori pojam naslovnog prostora. Naslovni ili adresni prostor Aspc je jednostavno skup svih raspoloživih adresa na određenom računalu:
Aspc = { 0 , 1, 2 , ... ... , Amax } ⊂ N 0+ . Sve adrese su u računalu prikazane binarno (kao i svi ostali podaci), te je veličina naslovnog prostora određena brojem n bitova adrese. Tada govorimo o n-bitnom adresnom prostoru. Očito je da će za adrese vrijediti isto što i za n–bitni cjelobrojni nepredznačeni tip (vidi vježbu 2). Stoga vrijedi da su minimalna Amin i maksimalna Amax adresa:
Amin = 0 , Amax = 2 n − 1 , te da je ukupan broj NA adresa:
73
N A = card (Aspc ) = 2 n , gdje je card funkcija kardinalnog broja skupa, tj. broja elemenata od kojih se skup sastoji. Adrese se redovito i isključivo pišu u heksadekadskoj formi, koja jednostavno korespondira s binarnim prikazom (ponovite pravila za pretvorbu binarnog broja u heksadekadski i obrnuto). Također je pravilo da se prilikom pisanja adresa uvijek navode i vodeće nule, tako da je stalno očito kolika je bitnost (broj bitova) adrese. Npr. za 16-bitni naslovni prostor vrijedi (podsjetimo se, svaka heksadekadska znamenka «pokriva» 4 bita): Aspc (16-bit) = { 0000, 0001, 0002, … , 000F , … … , FFFE, FFFF } . Ovakav je adresni prostor bio na procesorima sa 16-bitnim adresnim registrima i 16-bitnim sabirnicama. Sve adrese su 16-bitni pozitivni cijeli brojevi, uključujući i nulu. Veličina (broj adresa) 16-bitnog naslovnog prostora iznosi:
N A, 16 − bit = 216 = 2 6 + 10 = 2 6 × 210 = 64 K = 65536 . Današnji standardni procesori su 32-bitni,* pa su i adrese 32-bitne, a odgovarajući 32-bitni naslovni prostor je sljedeći: Aspc (32-bit) = {00000000, 00000001, 00000002, … … , FFFFFFFC, FFFFFFFE, FFFFFFFF} . Veličina ovog naslovnog prostora je:
N A, 32− bit = 2 32 = 2 2 + 30 = 2 2 × 2 30 = 4 G = 4 294 967 296 . Naglasimo odmah da je 4G neimenovani broj. G predstavlja računarsku kraticu koja se čita “giga”, i za koju vrijedi 1G = 230 = 1 073 741 824 . Radi se o broju koji je za oko 7.3% veći od 1 milijarde (engl. billion), 1 milijarda = 109 = 1 000 000 000. Kratica za milijardu u SI sustavu mjernih jedinica je također G, pa se iz konteksta mora iščitati radi li se o računarskom ili SI prefiksu (faktoru). Ako se želi ostvariti veći „memorijski prostor“, s više raspoloživih adresa, moraju se osigurati veći adresni registri, kao i odgovarajuće popratne adresne sabirnice. Detalje vidjeti u dodatku Axxx. Programski model memorije. Osnovica programskog modela memorije već je korištena kod prikaza memorijske pohrane strukture poretka u prethodnom poglavlju (sl. 6.1xx). Ponovimo da programski model memorije daje njen funkcionalni prikaz kako ga vidi programer, bez tehničkih detalja i mehanizama za unapređenje njenog rada (kao što su npr. sustavi priručne i virtualne memorije). Prikazan je na slici 7.1. Najmanji niz bitova koji ima jedinstvenu adresu naziva se osnovna memorijska lokacija ili memorijsko zrno (kao što je već spomenuto kod razmatranja poretka), a standardni izbor veličine zrna za procesore opće namjene je 1B = 1 bajt. Svakom zrnu je pridružena jedinstvena adresa iz skupa svih adresa Aspc . Uz gore navedeni 32-bitni naslovni prostor adresiranje ukupno 232 = 230 + 2 = 22 × 230 = 4G memorijskih lokacija (bajtova), s adresama od: Amin = 0000 0000h do Amax = FFFF FFFFh . On se može smatrati dovoljnim za organizaciju glavne memorije današnjih računala. Uz memorijsko zrno standardne veličine od 1B, i 32-bitni naslovni prostor, maksimalni je kapacitet glavne memorije: C32-bit, max = 4GB.
*
S trendom prelaska jačih radnih stanica i poslužitelja na 64-bitne procesore i platforme.
74
Standardan način pohrane programa i podataka je od manjih prema većim adresama, a smjer rasta memorijskih stogova je suprotan — od većih prema manjim adresama.
Adresa (općenita)
Osnovna memorijska lokacija, «memorijsko zrno» = 1B = 8 bit
Adresa 32-bitna b7
b6
b5
b4
b3
b2
b1
Amin = 0 =
0000 0000
XXh = sadržaj bajta 0
Amin + 1 =
0000 0001
XXh = sadržaj bajta 1
Amin + 2 =
0000 0002
XXh = sadržaj bajta 2
Amin + 3 =
0000 0003
XXh = sadržaj bajta 3
…
…
…
…
…
…
Adresni potprostor A
…
…
…
…
…
…
…
…
…
…
…
…
…
…
Smjer pohrane programa i globalnih varijabli (prema većim adresama)
…
…
…
…
…
…
…
…
…
…
…
↓ …
↑
Adresni potprostor B
…
b0
…
… Smjer rasta stogova -pohrana lokalnlih varijabi (prema manjim adresama)
…
Amin + NA – 2 =
FFFF FFFE
XXh = sadržaj bajta FFFF FFFE
Amin + NA – 1 =
FFFF FFFF
XXh = sadržaj bajta FFFF FFFF
Slika 7.1 Programski model memorije. Svako memorijsko zrno (danas je to standardno 1B = 8bit) ima jedinstvenu adresu iz naslovnog prostora. Naslovni prostor se može dijeliti na potprostore određene veličine, npr. za smještaj operacijskog sustava, za korisničke programe itd… Standardno se programi i globalne varijable, uključujući i globalno deklarirane poretke, pohranjuju u memoriju u smjeru od manjih adresa prema većima. Lokalne varijable pohranjuju se na stog. Stogovi rastu u suprotnom smjeru. Sa XXh (dvije heksadekadske znamenke) označen je heksadekadski ekvivalent sadržaja jednog bajta, pri čemu su, radi jednostavnosti, istom oznakom označeni općenito različiti sadržaji.
Memorijski stog. Memorijski stog ili kratko stog (engl. memory stack, stack) jedna je od osnovnih programskih struktura, koja je, poslije poretka, najelementarnija i najviše korištena u računarstvu. Za razliku od poretka, stog je dinamička struktura, jer stalno mijenja broj svojih elemenata. Struktura stoga određena je „položajem“, tj. adresom svojeg vršnog elementa, te adresom dna stoga, koja označava mjesto na kojem nije dozvoljeno pospremati elemente stoga. Veličina elemenata koji se stavljaju na stog ne mora biti jednaka ukoliko se vodi računa o redoslijedu njihovog stavljanja i uzimanja sa stoga, što proizlazi iz same prirode uporabe ove programske strukture. Također, pored same organizacije elemenata, podatkovne strukture određene su i operacijama koje se s elementima smiju vršiti. Za stog vrijedi da se elementi se na stog mogu gurati (operacija guranja, ili stavljanja, engl. push), i uzimati (engl. pull ili pop). To se čini po načelu LIFO. Ovaj akronim
75
dolazi od engl. Last In First Out, odnosno zadnji unutra prvi van. Navedeni princip je istovjetan kao kod stavljanja sijena na stog. Sijeno koje je zadnje stavljeno na stog, mora se prvo uzeti s njega. Slično načelo čitatlje će prepoznati u mnoštvu slučajeva u fizičkom svijetu. Npr. ako su knjige ili kompaktni diskovi, stavljani u usku kutiju, jedino što je dostupno je ona knjiga (disk) koja je zadnja stavljena u knjigu. Sve ostale knjige (diskovi) su nedostupni, odnosno možemo im pristupiti tek ako uklonimo sve „elemente“ iznad njih. Kad dođemo do zadnjeg elementa, i kad ga skinemo sa stoga, stog je prazan, odnosno došli smo do njegovog dna. Dakle, legalne operacije na stogu su samo one koje se vrše nad vršnim elementom stoga.* Na razini strojnog jezika, stožna struktura je podržana postojanjem posebnog adresnog registra SP, koji sadrži «kazaljku stoga» (engl. Stack Pointer). U tom registru se čuva adresa trenutnog vršnog elementa stoga. Svi ostali elementi su nedostupni u normalnim stožnim operacijama. Kad se elementi stavljaju na stog, on „raste“ (ili namata se, od engl. stack winding), a kad se elementi uzimaju sa stoga, on „se smanjuje“ (ili odmata se, engl. stack unwinding). Kaže se i da stog «diše», što se može usporediti s disanjem harmonike.† Stogovi mogu biti sustavski, tj. oni koje pri svom radu kreira operacijski sustav, ili korisnički, koji su unutar korisničkih programa kreirani i organizirani prema nakanama njihovog pisca. Sustavske stogove opisat ćemo ovdje u grubim crtama, jer je razumijevanje načela na kojima rade nužno za uspješno programiranje. Sustavski stogovi redovito rastu prema manjim adresama, dakle u smjeru suprotnom od smjera u kojem se normalno popunjava memorija s programskim kodom i podacima (u C/C++ globalnim podacima), kao što prikazuje sl. 7.1. To znači da će se novi element na stog pospremati na adresu koja je manja od adrese prethodnog vršnog elementa na stogu. S druge strane, korisnik može organizirati rast svojeg stoga prema želji, bilo prema manjim, ili prema većim adresama. Na sustavski stog u koji se pohranjuju podaci nužni za povratak iz potprograma, odnosno u C/C++, povratak iz funkcija koje su pozvane, u programe (funkcije) iz kojih je poziv izvršen. Taj skup podataka naziva se pozivni kontekst (engg. call context), i on, između ostalog, sadrži i adresu strojne instrukcije na kojoj se nastavlja izvođenje programa nakon što se pozvani potprogram (funkcija) izvrši. Također, stog može biti i korisnički, koje kreira sam „korisnik“, tj. u u ovom kontekstu pisac programa. Sustavski stogovi redovito rastu prema manjim adresama. Prilikom izvođenja C/C++ programa na korisničkom tekućem stogu (engl. user run-time stack), pozivna funkcija (procedura, program) stvara tzv. pozivni stožni okvir (engl. call stack frame) koji omogućuje povratak u pozivnu proceduru stavljanjem na stog sadržaja npr. pojedinih (rabljenih) registara, zatim registra stanja procesora (registra zastavica), te obavezno trenutni sadržaj registra programskog brojila koje sadrži adresu povratka. Na taj je način omogućena restauracija stanja procesora kakvo je bilo prije poziva, i povratak u pozivnu funkciju neposredno iza mjesta poziva pozvane funkcije. Pozvana funkcija (procedura, «potprogram») na tom istom stogu kreira i okvir za pohranu «lokalnih varijabli», što uključuje i lokalno objavljene strukture podataka, poretka itd… Lokalne varijable se zovu još i varijable okvira (engl. frame variables) po tome što se smještaju unutar „stožnog okvira“, odnosno dijela memorije s propisanim sadržajem i veličinom na stogu. Za njih prevodilac unaprijed predviđa mjesta koja će zauzeti na stogu, *
Načelo funkcioniranja stoga naziva se LIFO (od engl. Last In First Out), što znači: zadnji unutra prvi van. Iz toga proizlazi da se onaj element koji je stavljen prvi na stog, može zadnji skinuti sa stoga, pa bismo mogli govoriti i o načelu: prvi unutra zadnji van (engl. First In Last Out) ili kratko FILO. Objasnite zašto se tog ne označava ovim akronimom? †
Ponovite svoje znanje o stogovima iz odgovarajuće literature. Bez poznavanje ove podatkovne strukture nije moguće razumijevanje mnogih temeljnih mehanizama u računarstvu.
76
pa su ovako definirani «objekti» nepromjenjivi u tijeku izvođenja, tj. nisu dinamički.* Također, lokalne varijable se nazivaju i «automatske», jer prevodilac automatski predviđa mjesta za njih, a također, po izlasku iz funkcije, one se automatski brišu, jer se i cijeli stog, i okviri unutar njega brišu. Kažemo da su varijable izašle iz dogleda. Time se ostvaruje racionalno korištenje memorije, jer lokalne varijable funkcija koje su izvršene, više ne postoje i ne zauzimaju mjesta. Na koncu ovog šireg razmatranja, rezimirajmo da su motivi za rad s adresama u višim programskim jezicima mnogobrojni. Želimo li npr. raditi s podatkovnom strukturom poretka u nekom potprogramu, očito je da mu moramo nekako dojaviti adresu njegovog početka. Ili, ako želimo organizirati strukturu stoga unutar glavne memorije, moramo na nekom mjestu pohraniti adresu vršnog elementa tog stoga. Prilikom pohrane novog sadržaja na stog, moramo tu adresu adekvatno ažurirati, jednako kao i prilikom skidanja sadržaja sa stoga. Navedeni primjeri dobro ilustriraju potrebu za uvođenje tipa koji omogućuje učinkovit rad s adresama podataka. Rad s adresama u višim programskim jezicima. Kazaljke. Mnogi viši programski jezici omogućavaju da se s adresama radi na elegantan, posredan, način uporabom kazaljki ili pokazivača (engl. pointers). Kazaljke se definiraju kao poseban „tip podataka“ čiji su sadržaj adrese na određene druge programske tvorbe: varijable, podatkovne strukture (kao npr. poredci), objekti, funkcije (tj. mjesta gdje je pohranjen izvršni kôd funkcija), i drugo. Pošto adresa pokazuje gdje se taj entitet nalazi, ovaj je tip nazvan pokazivački tip, ili tip kazaljki (engl. pointer type). Iako se u osnovi radi o adresama, programer ne mora voditi računa o njihovom konkretnom iznosu. Njega zanima kuda ta adresa pokazuje, što opravdava izbor imena ovog tipa. S pokazivačkim tipom se gore spomenuta funkcionalnost rada u zbirnom jeziku dovodi u jezike više razine. Dodatno, što nije slučaj u zbirnim jezicima, kompilator vodi računa o tipu podatka na koji se adresa odnosi, i tako pomaže programeru da osigura konzistentnost pohrane i čitanja sadržaja s te adrese. Otuda slijedi kratka definicija: − kazaljke su «tipizirane adrese», tj. adrese određenog tipa podataka (u širem smislu), o čemu prevodilac vodi računa. Pri tom se za svaki tip podataka, kao i za svaki korisnički definirani složeni tip podatka (strukturu), te za svaki objekt kao primjerak svoje klase (C++ specifično, vidi kasnije), može definirati odgovarajuća kazaljka ili pokazivač. Mnogi programski jezici koji nisu imali implementiran pokazivački tip bili su, i još su uvijek vrlo uspješni u svojim domenama, kao npr. FORTRAN u numeričko-matematičkoj primjeni, COBOL u poslovnoj primjeni, BASIC u svojoj izvornoj inačici kao jezik za uvod u programiranje†. S druge strane, takvi jezici nisu pogodovali elegantnoj implementaciji kompleksnih podatkovnih struktura, a zbog toga nisu bili pogodni niti za pisanje računarski kompleksnih programa, kao što su operacijski sustavi i složeni primjenski programi. Jedan od glavnih razloga za to je upravo njihov manjak dobre operabilnosti s adresama, odnosno odsustvo pokazivačkog tipa. S druge strane, upravo je jezik C odmah po svom nastanku primijenjen za pisanje uzornog operacijskog sustava UNIX, poznatog po svojoj izvanrednoj funkcionalnosti, robustnosti i sigurnosti. Također jezik C i njegov nasljednik C++ je odmah uporabljen za pisanje najsloženijih primjenskih programa, danas bez iznimke sa složenim i korisniku ugodnim grafičkim sučeljem, u koje spadaju: teks*
Kažemo da su statični u strukturalnom smislu. To se ne smije miješati s uporabom C/C++ ključne riječi static za određenje globalnih varijabli.
†
Danas BASIC ima vrlo mnogo različith izvedbi ili okusa (engl. flavor) , koji sadrže mnogobrojne dodatke u odnosu na izvorne inačice ovog jezika.
77
tualni procesori, programi za dizajniranje i projektiranje, grafički programi, programi za obradu signala, a također i mnogobrojne programske razvojne okoline, kao što su i one za pisanje i kompiliranje programa, uključujući i one spomenute u ovom tekstu (Microsoft Visual Studio, Dev-C++). Kazaljke kao tipizirane adrese. Navedenu funkcionalnost kazaljki možemo opisati jednostavnom matematičkom formulacijom. Neka je varijabla vT tipa T smještena na adresi A( vT ). Uvedimo kazaljku pT na tip T. Indeks T kojim smo označili kazaljku, podsjeća da se ona odnosi upravo na tip T i nikoji drugi. Toj kazaljci možemo pridijeliti adresu bilo koje varijable tog tipa:
pT = A( vT ) .
Gornji izraz čitamo: kazaljka pT na tip T jednaka je adresi varijable vT tipa T. S matematičkog stanovišta, A( vT ) je funkcija, koju možemo zvati funkcija adrese varijable, ili kratko: funkcija „adresa od“. Ona je zaista funkcija, jer svaka varijabla ima jednu i samo jednu adresu. Općenito gledano, na istu memorijsku adresu (adrese), možemo stavljati različite tipove podataka. Stoga je bitno da su naše kazaljke „tipizirane“, tj. da one sadrže informaciju o tipu podataka na koji pokazuju, kao što sugerira gornja formula. Iz izlaganja o naslvonom prostoru, jasno je da su sve adrese određene računarske platforme jednake duljine, točno određene svojstvima procesora, pa će sâm sadržaj kazaljki uvijek biti istovrstan. Uz 32-bitne adrese, prilikom iščitavanja sadržaja kazaljki dobivat ćemo 32-bitne brojeve prikazane u formi heksadekadskog ekvivalenta, kod kojeg svaka heksadekadska znamenka interpretira 4 bita. Naglasimo da za programera nije presudno poznavnje konkretnih vrijednosti kazaljki, već sadržaja lokacija na koje one „pokazuju“. Vrijednosti kazaljki možemo ispisati u svrhu primjera i učenja. Također, to može biti korisno i u fazi testiranja i ispravljanja programa, odnosno prilikom korištenja otklanjača grešaka. Dakle, „brigu“ oko toga gdje će točno što biti smješteno prepuštamo kompilatoru, a po potrebi zatražimo informaciju o tome uvođenjem kazaljki na željene programske tvorbe. Nakon što smo dobili informaciju o adresi pohrane, možemo s tim memorijskim sadržajem napraviti željene radnje. Npr. ako se radi o memorijskoj adresi početka poretka, možemo pročitati početni element. Također, možemo kazaljku (adresu) uvećati za veličinu elementa i pristupiti sljedećem elementu poretka, itd… Ili, ako znamo adrese dviju varijabli, možemo izvršiti zamjenu njihovih sadržaja. Sadržaj adrese interpretiran u određenom tipu podataka. Uvedimo funkciju memorijskog sadržaja MT koja služi za pristup (čitanje ili upisivanje) sadržaju neke adrese A , uz njegovu interpretaciju kao tip podataka T :
M T ( A) = sadržaj adrese A prikazan kao tip podataka T . Sve dok se adresa A tretira kao općenita, tj. nevezana uz tip podataka, nužno je da uz funkciju M navedemo i tip podataka u skladu s kojim će sadržaj adrese biti interpretiran. Mi smo to naveli uvođenjem indeksa T uz funkciju M. Ovakav je pristup uobičajen kod rada u zbirnim jezicima. U ovisnosti o kojem tipu T se radi, birat ćemo odgovarajuće strojne instrukcije za rad s njima (suvremeni procesori imaju strojne instrukcije za rad s većinom tipova podataka koje susrećemo i u višim programskim jezicima). Kao što je već obrazloženo u pogl. 3xx, razlika među tipovima podatka je i u duljini (broju bajtova) i u načinu kodiranja njihovih vrijednosti u binarni oblik u kojem su pohranjeni u memoriju. Npr. znakovni tip char je duljine 1B , a kodiranje i dekodiranje njegovog sadržaja odvija se prema ASCII kodu. Nadalje, ostali diskretni tipovi su cjelobrojni tipovi (short int, int, long long) duljina 2B, 4B i 8B. Kod njih se kodiranje pozitivnih brojeva vrši izravnim prevođenjem u binarnu formu, a negativnih brojeva u potpuni komplement apsolutnog iznosa. Realni tipovi (float, double), pak su duljine 4B i
78
8B, i zapisani su preko svje mantise i eksponenta baze 2 (vidjeti pogl. 3). Dakle, kad se pristupa nekoj adresi, moramo znati koji niz bajtova promatramo i kako ćemo ih interpretirati, odnosno kako ćemo kodirati ili dekodirati njihov sadržaj. Sve je to sadržano u konceptu tipa podatka, i ta je informacija presudna za rad kompilatora. Kad je potrebno pristupiti sadržaju adrese, što je uobičajena radnja prilikom uporabe kazaljki, kompilator mora ugraditi strojne instrukcije za rad s određenim tipom podataka, i on odabire upravo strojne instrukcije za tip podataka određen tipom kazaljke. Na taj način se zadane operacije, kao što su npr. pohrana u memoriju, aritmetičke operacije, interpretacija sadržaja, odnosno njegov ispis na ekran, i sl., mogu jednoznačno izvršiti. Umetnemo li u gornju formulu, umjesto općenite adrese, kazaljku, koja već sadrži informaciju o tipu podatka, tada je možemo prepisati kao:
M T ( A) = M ( pT ) = memorijski sadržaj na koji pokazuje kazaljka pT . Funkciju M( pT ) sada čitamo jednostavno kao: memorijski sadržaj na koji pokazuje kazaljka pT. Pokazivački tip u C / C++. Jezik C / C ++ je poznat po elegantnoj sintaksi pokazivačkog tipa. Kazaljka ili pokazivač se objavljuje navođenjem znaka * (zvjezdica, engl. asterisk) ispred njenog identifikatora (imena), prema sljedećoj shemi: type
* pToType ;
Ovdje smo objavili kazaljku imena pToType na varijablu (odnocno općenito, varijable) nekog tipa Dobra je navika da imena pokazivča počinju malim slovom p (od engl. pointer), iza čega slijedi kratica tipa na koji kazaljka pokazuje, i tek zatim karakteristični dio imena. Pošto se u višim programskim jezicima bjeline standardno ignoriraju, gornja je deklaracija ekvivalentna sljedećoj:
type.
type *
pToType ;
Ovaj način objave kazaljki sugerira da se radi o posebnom, pokazivačkom tipu podataka, vezanom uz tip type, u smislu da se u njega pohranjuju adrese varijabli navedenog tipa. Međutim, pošto se zvjezdica odnosi samo na prvi identifikator s desna, ovaj je stil pisanja prikladan jedino u slučaju kad se objavljuje samo jedna kazaljka. Npr. u sljedećem retku: type *
pT1,
pT2 ;
// Oprez!!! pT2 nije kazaljka, već varijabla tipa type
samo je prva varijabla pT1 pokazivačkog tipa. Druga varijabla, pT2 , je „obična“ varijabla tipa type, iako njeno ime upućuje da je namjera vjerojatno bila da i to bude kazaljka. Očito se radi o zabuni, koju treba razriješiti promjenom stila pisanja i dodavanjem još jedne zvjezdice ispred druge varijable: type
*pT1, *pT2 ;
// Deklaracija dviju kazaljki na tip type.
U gornjim deklaracijama, kazaljkama nije inicijalizirana vrijednost. Drugim riječima, još nije određeno koju će konkretnu vrijednost adrese one sadržavati, odnosno na koje će varijable pokazivati. Samo je određeno ime kazaljki, kao i tip podatka na koji ona može (smije) pokazivati. Pridruživanje vrijednosti kazaljkama vrši se s pomoću operatora „adresa od“ opisanog u sljedećem pasusu. Operator &. Operator & je tzv. „adresa-od“ operator (engl. address-of operator). To je unarni prefiksni operator, dakle smještamo ga uvijek s lijeva od operanda na koji djeluje. Njime možemo kazaljci pridijeliti adresu, pri čemu se tip kazaljke i tip varijable, odnosno izraza (l-vrijednost) na koji operator & djeluje, mora podudarati:
79 type
tX = 1;
type *
pT = &tX ;
// Deklaracija i inicijalizacija varijable tX tipa type . // Deklaracija kazaljke pT na tip type : type *pt ; // i usputno joj pridruživanje adrese varijable isto tipa: pT = &tX .
Primijetimo da operator & ima značenje istovjetno funkciji A( vT ) , uvedenoj u prethodnom odjeljku, kojom se kazaljci pridjeljuje adresa varijable tipa istovjetnog tipu kazaljke. Pokušamo li kazaljci na određeni tip pridijeliti adresu varijable drugog tipa, kao u sljedećem primjeru, kompilator će javiti grešku: type1 type2
t1X = 1; t2X = 2;
type1 *
pT1 = &t1X ;
pT1 = &t1X ; pT1 = &t2X ;
// Deklaracija i inicijalizacija varijable t1X tipa type1 . // Deklaracija i inicijalizacija varijable t2X tipa type2 . // Deklaracija kazaljke pT1 na tip type1.
// Ispravna inicijalizacija kazaljke pT1 na adresu varijable tipa type1. // NEISPRAVNA inicijalizacija kazaljke pT1 na adresu varijable tipa type2 !!! // ERROR: cannot convert from 'type1 * ' to 'type2 *' .
Operator *. Znak * napisan ispred identifikatora kazaljke naziva se operator dereferenciranja ili odnavođenja (engl. dereferencing operator). To je unarni, prefiksni operator koji djeluje samo na pokazivačke tipove. On vraća sadržaj tipa jednakog tipu kazaljke, koji je pohranjen na memorijskoj adresi koja se nalazi u kazaljci. Primijetimo da je djelovanje tog operatora istovjetno djelovanju gore uvedene funkcije memorijskog sadržaja M( pT ) , koja daje memorijski sadržaj na koji pokazuje kazaljka tipa T. Sintaksa uporabe ovog operatora vidljiva je iz sljedećih linija koda: type
tX1 = 1, tX2 = 2;
type
*pT;
pT = &tX1 ; tX2 = *pT ;
// Deklaracija i inicijalizacija 2 varijable tipa type . // Deklaracija kazaljke pT na tip type (bez inicijalizacije).
// Adresa varijable tx1 pohranjuje se u kazaljku, pT = A( tX1 ). // tX2 = M( pT ) , tX2 = sadržaj na koji pokazuje pT . // Ekvivalentno pridruživanju: tx2 = *(&tx1), tj. tx2 = tx1.
Ovdje smo definirali varijable tx1 i tx2 tipa type i kazaljku pT na tip type. Zatim smo pokazivaču pT pridijelili vrijednost adrese varijable tx1 uporabom operatora & ispred njenog imena.* U našem slučaju varijabla tx2 poprima vrijednost tipa type koja je pohranjena na adresi pT . Pošto je u pT adresa varijable tx1 , tx2 poprima vrijednost jednaku vrijednosti varijable tx1. Primijetimo da je to već treće značenje znaka * kao operatora u sintaksi jezika C / C++. Podsjetimo se, prvo je bilo: binarni operator množenja kod brojevnih (cjelobrojnih i realnih) tipova podataka. Drugo je značenje operatora * deklaracija pokazivačkog tipa u tvrdnjama deklaracije varijabli. Ovo je tipično za jezik C/C++. Jedne te iste prikladne oznake se koriste za različite namjene, kad to ne može dovesti do dvoznačnosti. Iako se na prvi pogled može učiniti da uporaba iste oznake za dvije
*
Ovo pridruživanje adrese obavlja se, naravno, u tijeku izvođenja programa. Kod jednostavnih računalskih sustava, u prošlosti npr., ili na 8-bitnim mikroprocesorskim sustavima, programer je mogao imati pregled nad statičkim strukturama rabeći tzv. apsolutne adrese (u svezi s apsolutnim ili izravnim adresiranjem koje se koristi u strojnim instrukcijama za dohvat operanada iz memorije). Prilikom izvođenja složenih, tzv. modularnih programa, pod kontrolom operacijskog sustava (često višezadaćnog, engl. multitasking), koji je i sam složeni modularni program, programer, bilo da radi u zbirnom ili višem programskom jeziku, mora imati mogućnost da dozna adresu varijable. Isto je neophodno potrebno kod svih dinamičkih struktura podataka (koje mijenjaju svoju veličinu, raspored elemenata u memoriji itd. u tijeku izvođenja programa). U zbirnom jeziku postoje strojne instrukcije koje daju pravu adresu određene memorijske lokacije u tijeku izvođenja programa, i one se koriste u prijevodu gornjeg izraza da kazaljci pridijele aktualnu adresu varijable.
80
različite stvari u kontekstu pokazivačkih tipova, može biti zbunjujuća, jednostavno objašnjenje pokazaje da to nije tako. Uporaba znaka * za deklaraciju pokazivačkog tipa posve je u duhu djelovanja operatora dereferenciranja. Ako napišemo: type
* pT ;
možemo to pročitati na sljedeći način: nakon dereferenciranja pokazivač pT ima tip type, tj. sadržaj na koji pokazuje kazaljka pT je tipa type. Time je potvrđena formalna korektnost uporabe istog znaka za dvije uporabe, te još jednom ilustrirana elegancija jezika C/C++. Kazaljka na tip void . Kazaljku možemo definirati i kao općenitu adresu, nevezanu uz tip podataka. Tada u njenoj deklaraciji za type odabiremo „prazan“, ili ništavan tip void :* void
* pV ;
Dobivena kazaljka pV može sadržavati adresu proizvoljnog tipa, pa će sve deklaracije koje slijede biti ispravne: void type1
* pV ; t1X ; type2
pV = &t1X ; pV = &t2X ; pV = &t3X ;
t2X ; type3
t3X ; // Deklaracija 3 varijable različitih tipova.
// Kazaljci pV pridružuje se vrijednost adrese varijable t1X tipa type1. // Kazaljci pV pridružuje se vrijednost adrese varijable t2X tipa type2. // Kazaljci pV pridružuje se vrijednost adrese varijable t3X tipa type3.
U skladu s prethodnom diskusijom o pokazivačkom tipu, jasno je da se sadržaju adrese pohranjene u kazaljci na tip void ne može pristupiti na uobičajeni način kao kod „normalnih“ kazaljki. Nastavimo li prethodni programski odsječak sljedećim linijama: type4
t4X ;
t4X = *pV ;
// Deklaracija 4. varijable tipa type4. // Izravno dereferenciranje kazaljke na tip void!!! NEISPRAVNO!!!
// ERROR: illegal indirection, ERROR: cannot convert from ' void *' to
'type4'.
kompilator će dojaviti grešku da je došlo de ilegalne indirekcije (preusmjerenja). Dakle, kao što smo ranije objasnili, da bismo odredili kako interpretirati sadržaj bitova na određenoj memorijskoj lokaciji, odnosno, kako ga kodirati, moramo odrediti tip podataka da kompilator za njega odredi odgovarajuće strojne instrukcije. Stoga je ovakvim kazaljkama prije dereferenciranja potrebno nabaciti određeni tip, usklađen s tipom izraza u okviru kojeg se kazaljka javlja. Sintaksa je prikazana sljedećom tvrdnjom: t4X = * (type4*) pV ;
// Kazaljci tipa void eksplicitno nabacujemo tip kazaljke na tip // type4 izrazom: (type4*), koju potom dereferenciramo sa * .
Kao što je navedeno u komentaru, najprije smo kazaljku na prazan tip preveli u kazaljku na željeni tip, u našem slučaju na tip type4 varijable t4X, s pomoću izraza (type4*). Potom smo uporabom operatora dereferenciranja * dohvatili sadržaj tipa type4 s adrese pohranjene u pV . NULL (0)
vrijednost kazaljke. Pridružimo li kazaljci vrijednost 0, ili NULL (od engl. null, ništavan, nevažeći), tada u duhu naziva, smatramo da takav pokazivač ne pokazuje nikamo.† Često se kazaljke
*
O praznom ili ništavnom tipu void već je bilo riječi u poglavlju 2xx, kao jednom od mogućih tipova glavne funkcije main, u slučaju kad ona ne vraća vrijednost. Više govora o tom tipu bit će u sljedećem poglavlju 8xx.
†
Vrijednost NULL se u računarskoj teoriji i mnogim drugim primjenama (od programskih jezika do baza podataka), smatra nevažećom, ili ništavnom vrijednošću, striktno različitom od bilo koje tekstualne ili brojevne ili druge vrijednosti, uključujući i prazan niz, broj 0, itd… U jeziku C/C++ ta strogost je ublažena. Vrijednost NULL se koristi kod pokazivača i ekvivalentna je 0 .
81
prilikom objave inicijaliziraju na NULL da se naglasi kako još nisu poprimili nikoju valjanu vrijednost. Prije njihovog odnavođenja testirat ćemo ih, te u slučaju da je njihova vrijednost (još uvijek) nula, ne pristupamo toj adresi. Iako je adresa 0 sastavna adresa memorijskog prostora svakog računalskog sustava, odnosno brojanje adresa počinje upravo od 0, uglavnom je to dio privilegiranog memorijskog prostora kojem korisnički programi ne smiju i ne mogu pristupati. On je rezerviran za operacijski sustav i stoga je dereferenciranje takvog pokazivača zabranjeno (vidi Primjer 7.11). Tako će sljedeći programski odsječak rezultirati greškom u vrijeme izvođenja: type * type
pT = NULL ;
// Deklaracija kazaljke na tip type i njena incijalizacija na // vrijednost pT = NULL = 0.
tX = *pT ;
// NEISPRAVNO: Dereferenciranje kazaljke vrijednosti 0 !!! // ERROR: An unhandled win32 error exception occurred // in ProgramX.exe .
Kao što je sugerirano, da se izbjegnu ovakvi problemi, prije dereferenciranja provjerit ćemo stanje kazaljke: type *
pT = NULL ;
// ... // ...
// Deklaracija kazaljke na tip type i njena incijalizacija na // vrijednost pT = NULL = 0.
... ...
Moguće pridjeljivanje korektne vrijednosti kazaljci pT . ...
if pT != 0 type tX = *pT ;
// Dereferenciranje samo u slučaju vrijednosti kazaljke ≠ 0 .
Primjer pohrane varijabli (i kazaljki) u memoriji. Da pojasnimo gornje izlaganje, prikazat ćemo kako se varijable i kazaljke na njih pohranjuju u memoriju. Polazimo od primjera koji ilustrira deklaraciju kazaljke, te određenje njene vrijednosti adresom varijable istog tipa, u skladu s gornjim općenitim opisom. Programski kôd je dan u primjeru 7.1. Primjer 7.1 Deklaracija i inicijalizacija kazaljki, prikaz smještaja varijabli u memoriji. // 1. Deklaracija i definicija varijabli i kazaljki: int iX1 = +1, iX2 = +2; // Deklaracija i inicijalizac. 2 var. tipa int int *pI ; // Deklaracija kazaljke pI na tip int pI = &iX1 ;
// Adresa var. iX1 pohranjuje se u kazaljku
// Ispis 1. Ispis // A(var) = adresa // var = vrijednost cout << hex << "A(iX1) = " << "A(iX2) = " << "A(pI ) = " << endl;
pI.
adresa varijabli i njihovih vrijednosti. varijable var, varijable = sadržaj na adresi A(var): << &iX1 << "\t iX1 = " << iX1 << '\n' << &iX2 << "\t iX2 = " << iX2 << '\n' << &pI << "\t pI = " << pI << '\n'
// 2. Pristup sadržaju na koji pokazuje kazaljka: iX2 = *pI ; // iX2 = M(pI), iX2 = sadržaj na koji pokazuje pI. // Ispis 2. Ispis adresa varijabli i njihovih vrijednosti. // Ponovite gornji ispis nakon promjene varijable iX2: // ... ... ...
Izgled stanja memorije nakon 1. dijela programa u kojem se deklariraju i inicijaliziraju varijable i kazaljke, prikazuje sl. 7.2. Kompilator pohranjuje lokalne varijable na stog koji raste prema manjim
82
adresama. Sve gornje varijable i kazaljke su 32-bitne, pohranjene u 4 uzastopna bajta. Njihov je sadržaj prikazan s 8 heksadekadskih znamenaka. Pretpostavili smo da je prva varijabla iX1 pohranjena na adresu: A( iX1 ) = 0012 FF6Ch .* Vrijednost varijable u heksadekadskoj formi je, naravno, istovjetna kao i u dekadskom prikazu: iX1 = 0000 0001 . Pošto stog raste prema manjim adresama, sljedeća varijabla iX2 , ponovo duljline 4B, pohranjena je na adresi za 4 manjoj od prethodne, a isto vrijedi i za sljedeću deklariranu varijablu, kazaljku pI . Kazaljka pI pohranjena je na adresi A( pI ) = 0012 0FF64 . Njoj je u zadnjoj tvrdnji 1. dijela primjera 7.1 pridijeljena adresa varijable iX1 , pa ona poprima vrijednost pI = A( iX1 ) = 0012 FF6C
.
Ova je vrijednost napisana masnim slovima, kao i sve njoj odgovarajuće veličine: memorijska adresa istog iznosa, te sama kazaljka pI . Strelicom je označeno da ta adresa pokazuje na varijablu iX1 , što je uobičajena ilustracija pokazivačkog tipa. Na koncu 1. dijela programskog odsječka je produljena tvrdnja kojom se za sve tri deklarirane varijable ispisuje njena memorijska adresa, a potom u istom redu i vrijednost same varijable, odnosno sadržaj prethodno napisane adrese. Uporabom ispisnog manipulatora hex ostvaruje se ispis u heksadekadskoj formi. Umetanjem gornjeg programskog odsječka u testni program i njegovim izvršavanjem, čitatelj se može uvjeriti da su rezultati istovjetni onima prikazanim na sl. 7.2. Razlika je samo u konkretnom iznosu adresa†, te u redoslijedu prikaza. Na slici su adrese poredane od gore prema dolje u uzlaznom nizu (od manjih prema većima), kao što se memorija uobičajeno prikazuje. Varijabla koja je prva deklarirana tu će biti prikazana najniže, jer je pohranjena najbliže dnu stoga, odnosno na najvećoj adresi. S druge strane, program ispisuje varijable redom kojim su bile deklarirane, pa će tu najveće adrese biti na vrhu ispisa. Ostali detalji ispisa diskutirani su odjeljku niže. Memorijska adresa: …
Dno stoga:
…
Sadržaj memorije (4 uzastopna bajta)
Značenje sadržaja
… …
… …
0012 FF58
XXXX XXXX
–
0012 FF5C
XXXX XXXX
–
0012 FF60
XXXX XXXX
–
0012 FF64
0012 FF6C
pI
0012 FF68
0000 0002
iX2
0012 FF6C
0000 0001
iX1
0012 FF70
XXXX XXXX
–
0012 FF74
XXXX XXXX
–
… …
… …
…
…
Smjer rasta stoga
Slika 7.2 Prikaz pohrane varijabli i kazaljki deklariranih u primjeru 7.1. Stanje memorije nakon 1. dijela programa. Lokalne varijable se pohranjuju na stog koji raste prema manjim adresama. Deklarirane su dvije varijable tipa int . Prva, iX1, smješta se na najveću adresu, za koju je odabrana proizvoljna vrijednost A( iX1 ) = 0012 FF6C . Zauzima ukupno 4 slijedna bajta s adresama: 0012 FF6C, * †
Sve adrese su standardno u heksadekadskoj formi, pa ćemo oznaku
h
za heksadekadski sustava ispuštati.
Navedene vrijednosti adresa su okvirne, i namjerno su stavljene da budu različite od onih koje se dobivaju prilikom izvršavanja programa u često korištenim razvojnim okolinama. Stvarne adrese ovise o verziji kompilatora, kao i o korištenom operacijskom sustavu.
83 0012 FF6D, 0012 FF6E
i 0012 FF6F. Druga varijabla iX2 , smješta se na adresu za 4 manju od prethodne, tj. na 0012 FF68. „Iznad“ toga, tj. na adresu ponovo za 4 manju od prethodne, na stog se smješta pI kazaljka na cjelobrojni tip. Kazaljci pI se uporabom operatora & (operator „adresa od“) pridružuje adresa varijable iX1 (masno pisana vrijednost), u zadnjoj tvrdnji 1. dijela prije ispisa: pI = &iX1 ; Strelice sugeriraju značenje pokazivačkog tipa: vrijednost kazaljke je adresa koja pokazuje gdje se nalazi promatrana programska tvorba (varijabla, poredak, objekt, …). Navedene vrijednosti adresa su okvirne. Stvarne adrese ovise o verziji kompilatora i operacijskom sustavu. Čitatelj ih može provjeriti u svojim razvojnim okolinama na temelju rezultata ispisa. Nakon izvršenja drugog dijela programa, varijable iX1 i iX2 imaju istu vrijednost (nije prikazano na slici).
U drugom dijelu programa se primjenom operatora dereferenciranja * , varijabli iX2 pridružuje vrijednost sadržaja na koji pokazuje kazaljka pI, tj. vrijednost varijable iX1 . Čitatelju se ostavlja da dovrši primjer, kopiranjem tvrdnje za ispis adresa i njihovih sadržaja, te provjerom stanja memorije nakon promjene varijable iX2 . Sljedeći je primjer jednostavna nadogradnja prethodnoga. Dodatno se deklarira poredak, i zatim, kao i ranije, ispisuju adrese i njihovi sadržaji. Cilj je prikazati način na koji se na lokalnom stogu rezervira memorija za strukturu poretka. Primjer 7.2 Deklaracija i inicijalizacija kazaljki i poretka. Prikaz smještaja varijabli u memoriji. // 1. Deklaracija i definicija varijabli i kazaljki: const unsigned int cuN = 100; // Nepredznačena konstanta za definiranje // broja elemenata 1-dim poretka int iX1 = +1, iX2 = -2; // Deklaracija i inicijalizac. 2 var. tipa int int *pI = &iX1; // Deklaracija i inicijal. kazaljke pI = A(iX1) int iY[cuN] = {0, }; // Deklaracija poretka s cuN elemenata i // inicijalizacija elemenata na 0. // Ispis 1. Ispis adresa varijabli i njihovih vrijednosti. // Zaglavlje tablice: \n" cout << " Mem. adresa A(var) Sadržaj adrese MT(A) << "================================================\n" << "1. dio:\n"; cout << << << << << << << <<
hex "A(cuN) = " << &cuN << "\t cuN = " << cuN "A(iX1) = " << &iX1 << "\t iX1 = " << iX1 "A(iX2) = " << &iX2 << "\t iX2 = " << iX2 "A(pI ) = " << &pI << "\t pI = " << pI "A(iY[0])= " << &iY[0] << "\tiY[0] = " << "A(iY[1])= " << &iY[1] << "\tiY[1] = " << "A(iY[" << cuN - 1 << "])=" << &iY[cuN-1] << "\tiY[" << cuN - 1 << "]= " << << endl;
// 2. Dio iY[0] = iY[1] = iY[cuN-1] = *pI ; // //
<< '\n' << '\n' << '\n' << '\n' iY[0] << '\n' iY[1] << '\n' iY[cuN-1] << '\n'
iY[0] = iY[cuN-1] = M(pI) = sadržaj na koji pokazuje pI.
// Ispis adresa varijabli i njihovih vrijednosti. // Ponovite gornji ispis nakon promjene početnog iY[0], iY[1], // i zadnjeg iY[cuN-1], elementa poretka. // ... ... ...
84
Najprije se deklarira konstanta cuN koja će definirati broj elemenata u poretku. Potom slijedi dio deklaracija i inicijalizacija identičan onom u prethodnom primjeru. Nakon toga se deklarira poredak iX , s cuN elemenata. Kao što će se vidjeti u kasnijim odjeljcima, upravo je podatkovna strutura poretka dobar primjer za uporabu i aritmetiku kazaljki. Zato na kraju prvog dijela programa ispisujemo adrese i njihove sadržaje za sve deklarirane pojedinačne varijable ( cuN, iX1, iX2, pI), te za nulti, prvi i posljednji element poretka ( iY[0], iY[1], iY[cuN-1] ). U drugom dijelu programa, u tvrdnji višestrukog pridruživanja, 0-tom, 1-vom i posljednjem elementu poretka iX dodjeljuje se sadržaj na koji pokazuje kazaljka pI. Izgled stanja memorije, u ovom slučaju nakon izvršenja cijelog programskog odsječka (1. i 2. dijela), prikazan je na sl. 7.3. Iako detalji oko memorijske pohrane varijabli nisu presudni za razumijevanje pokazivčkog tipa, zbog potpunosti ćemo analizirati i ovaj slučaj. To će doprinijeti temeljitijem razumijevanju gradiva, kao i povezivanju pojmova varijabli, poredaka i kazaljki. Na stog je najprije, tj. na najveću adresu, pohranjena varijabla cuN . Za dno stoga na koji se pohranjuju lokalne varijable, odabrana ista vrijednost kao i u prethodnom primjeru: A( StackBottom ) = 0012 FF70. Dno stoga je određeno adresom na koju, i „ispod“ koje (prema slici), se lokalne varijable ne mogu stavljati. Zato prvi raspoloživi bajt loklanog stoga ima adresu 0012 FF6F. Ako je prva lokalna varijabla tipa T i duljine t B , tada će ona biti pohranjena na adresi A( var1 ) = A(StackBottom) – t . U našem je slučaju tip T = int , duljina tipa u bajtovima t = 4, pa je adresa prve varijable A( cuN ) = 0012 FF70 – 4 = 0012 FF6C . Ova je varijabla označena kvalifikatorom const kao konstanta. Pridružena joj je vrijednost 100d , pa je heksadekdski prikaz sadržaja 4 bajta: 0000 0064 .* Razmatranje se nastavlja kao i u prethodnom primjeru. Pošto stog raste prema manjim adresama, sljedeća varijabla, ponovo duljline 4B, pohranjena je na adresi za 4 manjoj od prethodne, sljedeća također, itd… Vidljivo je kako su u memoriju pohranjene varijable iX1 i iX2 . Varijabla iX2 = −2 je negativna i stoga je u memoriji kodirana s pomoću potpunog (dvojnog) komplementa kao iX1 = = FFFF FFFE . Za kodiranje negativnih brojeva s pomoću potpunog komplementa vidjeti pogl 3xx i dodatak Xxx. Zbog potpunosti, pokazat ćemo ukratko kako u 32-bitnoj aritmetici, ili aritmetici modulo 232 = 4G , broj FFFF FFFE ima aritmetičko značenje broja −2. Dodamo li broju −2 njemu suprotni broj +2, rezultat je −2 + 2 = 0. Slično, dodamo li broju FFFF FFFE broj +2, tj. Ako izvršimo zbrajanje: FFFF FFFE + 0000 0002 1|0000 0000
dobivamo nulu. Ovaj jednostavan račun izvršen je izravno u heksadekadskom sustavu. Zbrajanje se vrši analogno zbrajanju u dekadskom sustavu, uz bazu 16. Tako je E ( = 14d ) + 2 = 16d = 0 i 1 dalje. Prenesena se jedinica zbraja sa znamenkom F ( = 15d ) na višem mjestu, što daje: F + 1 = 16d = 0 i 1 dalje, itd... Rezultat je da svih 8 heksadekadskih znamenaka postaje 0, uz pojavu bita prijenosa u najviši nepostojeći bit.† Konačni rezultat je korektan i iznosi 0. Isti račun aritmetičko logička jedinka procesora vrši u binarnom sustavu, i postavlja svih 32 bita tipa int na vrijednost 0. Ovakav i sličan račun čitatelj može provjeriti na kalkulatoru koji dozvoljava heksadekadski račun, vodeći računa o odbacivanju nepostojećih vodećih znamenaka, kao u gornjem primjeru.
* †
64h = 6 × 161 + 4 × 160 = 96 + 4 = 100d .
Iako je došlo do prijenosa (C = Carry = 1), u ovom slučaju nije došlo do preljeva (V = oVerflow = 0), tj. opseg tipa nije prekoračen pošto se radi o zbrajanju brojeva mješovitog predznaka.
85
Iznad varijable iX2 na stog se smješta kazaljka pI . Mada je njen sadržaj pI = 0012 FF68 za 4 manji u odnosu na prethodni primjer, jer su varijable iX1, iX2 pohranjene na adresama za 4 manjima, njen smisao je isti: ona upućuje na varijablu iX1. Nakon smještaja kazaljke pI, kompilator nailazi na objavu poretka iY tvrdnjom: int iY[cuN] = {0, } ;
Ovime su ujedno svi elementi poretka incijalizirani na vrijednost iY[i] = 0 (i = 0, 1, 2, … cuN-1 ). Pošto poredak ima 100 elemenata duljine 4B, za njega je potrebno rezervirati ukupno 400d B = = 190h B. Kompilator od adrese A(iP) = 0012 FF68 kazaljke iP oduzima heksadekadski broj 190h, i dobiva adresu početka poretka: A( iP ) = 0012 FF60 − 0000 0190 = 0012 FDD0
.
Kompilator u svoju tablicu simbola, bilježi ime poretka, u našem slučaju iY , i tretira ga u bitnom kao konstantu kazaljku, čija je vrijednost jednaka dobivenoj adresi. Dakle, dobivena adresa početnog, ili nultog, elementa, smatra se i adresom poretka samog (vidjeti odjeljak kazaljke i poredci). Prilikom izračuna adrese poretka mogu se pojaviti i dodatni zahtjevi za poravnanjem podataka, što će biti obrazloženo u sljedećem odjeljku. Iako se varijable na stog smještaju prema manjim adresama — pa se tako i adresa poretka tvori oduzimanjem njegove veličine od adrese prethodne varijable — razmještaj elemenata unutar poretka je kao što bismo očekivali: adrese elemenata rastu linearno s porastom indeksa elementa. To je u skladu s gradivom izloženom u prethodnom poglavlju. Sadržaj memorije na adresama koje su na stogu „iznad“ poretka, odnosno čiji su iznosi manji od adrese poretka, nepoznat je i nevažan za naš programski odsječak. To je dio memorije koji je program dobio na raspolaganje za smještaj lokalnih varijabli „na stog“, i koji je, barem za sada, ostao nepopunjen. Tu bi se smještale nove varijable, kad bi bile deklarirane. U 2. dijelu programskog odsječka se, na isti način kao i u prethodnom primjeru, vrijednost na koju pokazuje kazaljka pI, dakle vrijednost varijable iX1, pridružuje varijablama istog tipa. Sada su to elementi poretka iY[0], iY[1], iY[99], čija je konačna vrijednost 0000 0001 prikazana na slici. U našem primjeru, sve su varijable, uključujući kazaljke i elemente poretka, imale istu veličinu od 4B. Primijetimo da su sve smještene na adresama koje su djeljive s 4, tj. onima sa zadnjom znamenkom 0, 4, 8 ili C (pošto je baza heksadekadskog sustava Bh = 16 = 24 djeljiva s 4). Općenito, za tip podataka T duljine t bajtova kažemo da je poravnat, ako mu je adresa na koju je smješten djeljiva s t. Iz toga proizlazi da je tip char uvijek poravnat, da je tip short int poravnat ako je na parnim adresama, itd… Poravnatost podataka (engl. data alignement) vrlo je važna za učinkovitiji prijenos podatka iz procesora u memoriju. Za dohvat poravnatog podatka potreban je minimalan broj sabirnčkih ciklusa, konkretno, za tipove duljilne 4B je na sustavima s 32-bitrnom adresnom sabirnicom potreban upravo jedan sabirnički ciklus. S druge strane, ukoliko takav podatak nije poravnat, tada će biti potrebna 2 sabirnička ciklusa, tj. duplo duže vrijeme. Da bi ostvarili poravnatost, prevodioci će u općenitom slučaju ostavljati prazne bajtove. Npr. da smo u gornjem primjeru deklarirali varijablu tipa char veličine 1B, između varijabli veličine 4B, prevodilac bi 3 bajta ostavio prazna da sljedeća varijabla duljine 4B bude ponovo na adresi djeljivoj s 4 (detaljnije u dodatku Cxx). Poravnatost poredaka prevodioci vrlo često tretiraju posebno. U našem je primjeru adresa poretka sjela točno na „okruglu adresu“ djeljivu sa 16, tj. na adresu kojoj je zadnja znamenka 0. Za slučaj da to nije tako, mnogi će prevodioci pomaknuti adresu poretka na prvu nižu okruglu adresu.* Na taj se
*
Npr. GNU GCC u razvojnim okolinama kao što su Dev-Cpp, Code::Blocks.
86
način osigurava brži rad se elementima poretka, tj. njihov brži premještaj unutar memorijskog sustava glavne i priručne memorije. Da se ostvari taj cilj, žrtvuje se neiskorištenost do desetak bajtova, slično kao što se za poravnanje pojedinih varijabli žrtvovalo nekoliko bajtova. Dodatno, razvojne okoline nude mogućnost da se u tijeku razvoja programa gradi tzv. „debug“ inačica (engl. debug version) programa koja je posebice prilagođena da pomogne programeru pri otklanjanju grešaka. Neke od njih će za pojedinačne varijable rezervirati dodatne bajtove kako bi se olakšao proces nalaženja logičkih grešaka. Primjerice, MS Visual Studio od inačice 2005 za pohranu svih varijabli rezervira dodatnih 8B za potrebe učinkovitijeg rada otklanjača pogrešaka. Nakon što je program korektan, gradi se završna inačica (engl. release version) programa u kojoj se za podatke rezerviraju minimalna potrebna mjesta uz uvažavanje poravanja podataka. Također, u postupku optimizacije, koja vodi računa o detaljima rada procesora, vrše se dodatne promjene u svrhu optimizacije brzine rada i utrošene memorije. Tako u svrhu boljeg pakiranja varijabli, kompilatori ih često prerazmjeste tako da združuju istovrsne varijable i one manjih duljina, smanjujući na taj način broj neiskorištenih bajtova (dodatak Cxx). Memorijska adresa: …
…
0012 FDC8
Dno stoga:
Sadržaj memorije (4 uzastopna bajta)
Značenje sadržaja
… …
… …
XXXX XXXX
–
0012 FDCC
XXXX XXXX
–
0012 FDD0
0000 0001
iY [0]
0012 FDD4
0000 0001
iY [1]
…
…
… …
… …
…
…
… …
… …
…
…
… …
… …
0012 FF50
0000 0000
iY[98]
0012 FF54
0000 0001
iY[99]
0012 FF60
0012 FF68
pI
0012 FF64
FFFF FFFE
iX2
0012 FF68
0000 0001
iX1
0012 FF6C
0000 0064
cuN = 100d
0012 FF70
XXXX XXXX
–
0012 FF74
XXXX XXXX
–
… …
… …
…
…
Smjer rasta stoga
Slika 7.3 Prikaz memorijske pohrane varijabli i kazaljki deklariranih u primjeru 7.2. Stanje memorije nakon izvršenja 1. i 2. dijela programskog odsječka. Prva je deklarirana konstanta cuN tipa int koja se smješta na adresu 0012 FF6C , za 4 manju od dna stoga označenog adresom 0012 FF70. Zatim na stogu slijede varijable iX1, iX2 te pI, kao u prethodnom primjeru. Dodatno, deklariran je poredak iY tipa int, sa cuN = 100 elemenata, što je glede duljine pojedinog elementa i ukupne duljine jednako primjeru poretka sa slike 6.1 u prethodnom poglavlju. Adresu poretka kompilator dobiva tako da od adrese sljed Asdfg Raspored ovih elemenata u memoriji ijede jedeća varijabla iX1 istog tipa, smješta se na adresu za 4 manju od prethodne, tj. na 0012 019C. Iza toga se smještaju sljedeće dvije varijable, od kojih je druga kazaljka na cjelobrojni tip. Njoj se u zadnjoj tvrdnji prvog dijela programskog koda pridružuje adresa varijable iX1 (masno pisana vrijdenost). Strelice sugeriraju značenje pokazivačkog tipa: njegova vrijednost je adresa koja pokazuje gdje se nalazi promatrana programska tvorba (varijabla, poredak, objekt, vidi dalje). Navedene vrijednosti adresa su fiktivne. Stvarne adrese ovise o verziji kompilatora i operacijskom sustavu. Čitatelj ih može provjeriti u svojim razvojnim okolinama.
87
Pokazivačka aritmetika. Jezik C/C++ dopušta potpunu pokazivačku aritmetiku (engl. pointer arithmetic). S pokazivačima istog tipa mogu se izvoditi aritmetičke operacije zbrajanja i oduzimanja kao i na cjelobrojnom tipu. Operacije množenja i cjelobrojnog dijeljenja nemaju smisla nad pokazivačkim tipom i zato su ilegalne. Rezultat, kao što i očekujemo za adrese, interpretira se kao nepredznačeni cjelobrojni tip. Kod računa s adresama uvažava se duljina tipa pokazivača, što uvelike olakšava programiranje i čini ga elegantnijim. Npr. u sljedećem programskom odsječku: type
t1, t2, *pT1 , *pT2;
pT2 = &t2; pT1 = pT2 + 1;
programer je pokazivaču pT1 pridružio vrijednost za 1 veću od pT2, pa će pT1 pokazivati na prvo «susjedno» mjesto u memoriji gdje se može smjestiti varijabla istog tipa. Rječnikom adresa, pT1 će sadržavati adresu: pT1Add = pT2Add + t ,
gdje je t duljina tipa type izražena brojem memorijskih zrna (bajtova). Npr. uz type = int , u pT1 će biti adresa za 4 veća od one u pT2, jer je duljina tipa izražena brojem bajtova, t(int) B = 4B : pT1Add = pT2Add + 4 // t(int) = 4.
Uz tip short int ta će adresa biti veća za 2, jer je duljina t(short int) B = 2B, i tome slično. Primijetimo da kompilator sâm vodi računa da inkrementira adresu na način da ona pokazuje na prvo „susjedno mjesto“, uvažavajući duljinu datog tipa. Već vidimo da je ova inkrementacija vrlo korisna kod npr. prolaska kroz strukturu poretka. Uzastopnom inkrementacijom kazaljke ona pokazuje redom na sljedeće elemente. U aritmetici pokazivača samo će mali broj operacija zaista imati smisla. Kao što smo upravo natuknuli, to je uglavnom vezano za rad s podatkovnom strukturom poretka, gdje su slijedni elementi poredani u memoriji (vidi također sljedeći odjeljak). Dakle, iako prevodilac dozvoljava aritmetiku pokazivača, programer mora sam voditi računa o značenju, smislenosti i valjanosti takvih izraza. Zadatak 7.2 Proanalizirajte sljedeći programski odsječak i odgonetnite što će se ispisati. Razlučite što od ispisa možemo predvidjeti, a što je u načelu teško ili čak nemoguće znati bez poznavanja mnogih dodatnih detalja? Dodatna pitanja uz dijelove programskog koda: a) Hoće li biti sintaktičkih grešaka u dijelu a programskog koda? int
*pI;
// Deklaracija kazaljke na tip int // bez inicijalizacije. int i1 = 0x40; // Varijabla istog tipa kao i kazaljka // Ox40 = ? // a) Definiranje kazaljki: pI = &i1; // Kazaljka pI ima značenje adrese koja pokazuje // na tip int – pridružujemo joj vrijednost adrese // varijable i1 s pomoću operatora & (address-of) // Ispis a: cout << " i1 = " << i1 << "\n" << " pI = " << pI << "\n" << " *pI = " << *pI << "\n" << " *&i1 = " << *&i1 << "\n" << "&*pI = " << &*pI << "\n" << endl;
Zadatak 7.2 b i c* Dodajte prethodnom zadatku dolje navedeni nastavak i odgovorite: b). Hoće li biti grešaka u ovom dijelu? Ako da otklonite ih. Razmotrite što je ispisano odnavođenjem
88
pokazivača pI s pomoću operatora * ? Jeste li to očekivali? Što zaključujete o pokazivačima kao adresama? Smije li se pokazivaču pridruživati adresa drugog tipa (naravno, osim ako programer dobro zna što radi)? c) Objašnjenje: kompajler koji rabimo svim varijablama duljine 4B ili manje pridjeljuje po 4B na korisničkom stogu, te prilikom rezervacije mjesta inicijalizira sve rezervirane bajtove na vrijednost CCh (vidjeti Zadatak 3.12). U dijelu koda c smo uveli pokazivač pC tipa char, i tri više značajna bajta (koji su na većim adresama, prema uređaju ”Little Endian Byte Ordering”) koji su nakon inicijalizacije c1 = '@' ostali na vrijednosti CCh smo postavili na nulu. Rezultat ispisa će sada biti kao što očekujemo. Odgovorite za koliko se poveća adresa sadržana u pC prilikom inkrementacije ++pC? A za koliko bi se povećala adresa sadržana u pI? Ako niste sigurni, provjerite! // *b) Nova lokalna varijabla: char c1 = '@'; // Varijabla različitog tipa od pI // ASCII(@) = 40h // Smije li kazaljka pokazivati na drugi tip? pI = &c1; // Što dojavljuje prevodilac? // Popravite to prisilnim pridjeljivanjem: // pI = (int *) &c1 // Ispis b (što je problematično)? cout << " char c1 = " << c1 << "\n" << "(int) c1 = " << (int) c1 << "\n" << " pI = " << pI << "\n" << " *pI = " << *pI << "\n" << endl; // *c) Nova kazaljka: char *pC = &c1; // pC = Adress(c1), tip char! // Objasnite što se događa // Za koliko se *(++pC) = 0; *++pC = 0; *++pC = 0; // Ispis c: cout << " char c1 = " << c1 << "\n" << "(int) c1 = " << (int) c1 << "\n" << " *pC = " << (int) *pC << "\n" << " pI = " << pI << "\n" << " *pI = " << *pI << "\n" << endl;
U prethodnim zadacima smo se upoznali s pokazivačima kao tipiziranim adresama. Viši programski jezici omogućavaju programeru rad bez stalne kontrole vrijednosti adresa, ali je poznavanje i potpuno razumijevanje načela rada pokazivača vrlo važno. Pogotovo je to presudno prilikom otklanjanja logičkih grešaka (engl. debugging) i provjere koda preko otklonjivača grešaka. Zadatak 7.3 Odredite sve vrijednosti varijabli, i odgovorite na pitanja postavljena u komentarima donjeg programskog odsječka. Potom provjerite svoje odgovore odgovarajućim ispisom. int i1, i2, iP* // 1. iP = &i1; i1 = 99; *iP = 100; // Ispis 1: i1 = ?, i2 = ?, *iP = ?, kuda pokazuje iP? // ... ... ...
89 // 2. Preusmjeravanje kazaljki: iP = &i2; *iP = ++i1; // Ispis 2: i1 = ?, i2 = ?, *iP = ?, kuda pokazuje iP? // ... ... ... // 3. ++iP; // Oprez – znamo li što radimo!? *iP *= *iP; // Je li ovo neizvjesno? // Ispis 3: i1 = ?, i2 = ?, *iP = ?, kuda pokazuje iP? // ... ... ...
Zadatak 7.4* U sljedećem primjeru odredite sve vrijednosti koje se mogu odrediti prije testiranja programa. Posebice odgovorite na sljedeća pitanja u svezi s odgovarajućim dijelom koda. 0) Ako pokušamo ispisati sadržaj sa adresa na koje pokazuju pokazivači vrijednosti NULL, što će se desiti? Promotrite upozorenja prevodioca, te otklonite problematičan dio tvrdnji da možete nastaviti s testiranjem programa. a) Kako su ispisane vrijednosti pokazivača, u kojem brojevnom sustavu? Kako to da su lokalne varijable smještene obrnutim redom od onog kojim smo ih deklarirali? Znate li kako se zove podatkovna struktura kod koje je redoslijed uzimanja obrnut od redoslijeda umetanja? Promotrite na kojim je adresama rezerviran prostor za varijable tipa int, te short int. Znajući duljine pojedinih tipova, diskutirajte je li preostalo praznog (neiskorištenog prostora)? Što mislite zašto je prevodilac pojednostavio rezervaciju mjesta za varijable? Bi li to bilo prihvatljivo i za poretke? b) Do lokalnih varijabli u našem primjeru možemo doći na normalan način navođenjem imena varijable (kao pod a), ali i "odnavođenjem" pokazivača. Osim za vježbu, je li to potrebno raditi? c) Promotrite navedenu aritmetiku pokazivača i diskutirajte. Može li nevaljala uporaba pokazivača biti uzrokom slučajnih, ili čak i namjernih subverzija u programu? // Varijable: int i1 = 1, i2 = 2 ; short int s1 = 1, s2 = 2 ; // Kazaljke na "ništavnu" vrijednost (NULL) int *pI1 = 0, *pI2 = NULL; short int *pS1 = 0, *pS2 = 0; // 0) Ispis vrijednosti kazaljki vrijednosti NULL, // i sadržaja s tih adresa. Zahtijeva li ovo oprez? cout << " pI1 = " << pI1 << "\t*pI1 = " << *pI1 << "\n" << " pI2 = " << pI2 << "\t*pI2 = " << *pI2 << "\n" << " pS1 = " << pS1 << "\t*pS1 = " << *pS1 << "\n" << " pS2 = " << pS2 << "\t*pS2 = " << *pS2 <<"\n " << endl; // Konkrente i korisnički korektne vrijednosti kazaljki pI1 = &i1; pI2 = &i2; // kazaljke na i1 i i2 pS1 = &s1; pS2 = &s2; // kazaljke na s1 i s2 // a) Ispis adresa // A(var) = adresa cout << "A(i1) = " << "A(i2) = " << "A(s1) = " << "A(s2) = " << endl;
i sadržaja adresa: od varijable var << pI1 << "\t i1 = << pI2 << "\t i2 = << pS1 << "\t s1 = << pS2 << "\t s2 =
" " " "
<< << << <<
i1 i2 s1 s2
<< << << <<
"\n" "\n" "\n" "\n"
// b) Ispis kazaljki i dereferenciranih kazaljki: cout << " pI1 = " << pI1 << "\t*pI1 = " << *pI1 << "\n" << " pI1 = " << pI2 << "\t*pI2 = " << *pI2 << "\n" << " pS1 = " << pS1 << "\t*pS1 = " << *pS1 << "\n"
90 << " pS2 = " << pS2 << "\t*pS2 = " << *pS2 <<"\n " << endl; // c) Aritmetika kazaljki (ima li pI1 -= 1; // Za koliko se smanjila pI2 -= 1; // Za koliko se smanjila pS1 -= 1; // Za koliko se smanjila pS2 += 5; // Za koliko se povećala // Provjera: cout << " pI1 << " pI2 << " pS1 << " pS2 << endl;
= = = =
" " " "
<< << << <<
pI1 pI2 pS1 pS2
<< << << <<
to smisla raditi?): adresa A(pI1)? adresa A(pI2)? adresa A(pS2)? adresa A(pS2)?
"\t*pI1 "\t*pI2 "\t*pS1 "\t*pS2
= = = =
" " " "
<< << << <<
*pI1 *pI2 *pS1 *pS2
<< "\n" << "\n" << "\n" <<"\n "
Kazaljke i poredci Prilikom deklaracije poretka tvrdnjom: type tArr[n] ;
prevodilac imenu poretka tArr pridjeljuje adresu njegovog početka, dakle adresu nultog elementa. Drugim riječima vrijedi da je: tArr == &tArr[0]
,
odnosno tArr svojom vrijednosti odgovara pokazivač na početni element poretka. Također, u skladu s formulom (4.1), vrijedi da je kazaljka pTLast na zadnji, (n – 1)-vi, element jednaka: type * pTLast ; pTLast = tArr + n – 1 ;
Međutim, vrijednost vezana uz identifikator poretka (u našem slučaju tArr) se nikako ne može promijeniti (vidi Primjer 7.8), jer prevodilac to ne dozvoljava. Navedeno ime se odnosi isključivo na deklarirani poredak, i uvijek pokazuje na početni (nulti) element. Za njega se ne otvara posebno mjesto na stogu lokalnih varijabli. Zato se ime poretka ne može smatrati pokazivačkim tipom. Želimo li pristupati elementima, najbolje je, kao u gornjem primjeru uvesti pokazivač inicijaliziran preko imena poretka. U paragrafu o pokazivačkoj aritmetici je već istaknuto da se ona redovito i gotovo isključivo primjenjuje kod rada s poredcima. Npr. dodavanje ili oduzimanje cjelobrojne konstante pokazivaču ima smisla jedino ako programer vodi računa da novodobivene kazaljke pokazuju unutar određenog poretka. Oduzimanje manje kazaljke od veće unutar nekog poretka dat će broj elemenata između te dvije kazaljke, i sl. Kao što je već sugerirano, glavna je uporaba pokazivača u slijednom pristupu elementima poretka (vidi Primjer 7.5). Takav se prolaz kroz poredak ostvaruje uvođenjem promjenjivog pokazivača, kojeg možemo nazvati indeksni pokazivač. Njega inicijaliziramo tako da pokazuje na početni element poretka, tj. pridružimo mu vrijednost koju ima ime poretka. Zatim odnavođenjem indeksnog pokazivača pristupimo elementu poretka, tj. obavljamo potrebne radnje s tim elementom. Potom taj pokazivač inkrementiramo, i ponavljamo petlju sve dok pokazivač ne pokaže na prvo prazno mjesto iza poretka, kada se izlazi iz petlje (Primjer 7.6). Usporedba slijednog pristupa elementima poretka preko indekasa i preko pokazivača*. Pristup slijednim elementima poretka s pomoću inkrementacije pokazivača je u načelu brži nego kad se elementima poretka pristupa njihovim eksplicitnim navođenjem, tj. oblikom tArr[i], i inkrementiranjem indeksa: i = 0, 1, 2, … … , n – 1. Naime kod eksplicitnog navođenja elementa poretka s indek-
91
som u uglatoj zagradi, prevodilac za dobivanje adrese elementa koristi tzv. indeksni način adresiranja (engl. indexed addressing mode). Izračun u potpunosti slijedi linearnu formulu (4.1). Bazna adresa A0 se pri tom pohranjuje u neki adresni (bazni) registar, a indeks u indeksni registar. Svaka inkrementacija indeksa znači da će biti inkrementiran sadržaj indeksnog registra, a potom se ponovi cijeli izračun adrese prema formuli (4.1). Sveukupno se izvrši jedna inkrementacija, jedno množenje (faktorom duljine tipa t), i jedno zbrajanje. Sve strojne operacije su «registarskog» tipa, s operandima u registrima procesora, dakle vrlo brze, ali ih ipak ima više nego ako pristup elementima vršimo preko pokazivača. U tom slučaju se početna vrijednost pokazivača smjesti u neki adresni registar i to je odmah adresa nultog elementa. Zatim se tom adresnom registru dodaje broj t, odnosno vrši samo jedna operacija zbrajanja, ponovo registarskog tipa. Očito je da je u ovom slučaju izračun adrese jednostavniji, te da će ovakav prolaz, pogotovo kroz vrlo dugačke poretke doprinijeti uštedi na vremenu. Testiranje na računalu s procesorom Pentium IV i platformi Win32, pokazuje da ta razlika u brzini za najnovije procesore nije velika, ali to ne znači da treba odstupati od principa i savjeta za dobro programiranje. Npr. za prolaz kroz poredak od 170 × 106 elemenata tipa short int, jednostavan testni program pokazuje da je prolaz preko pokazivača u slučaju neoptimiziranog koda brži za oko 5% do 10% ovisno o detaljima radnji koje se vrše s elementima. Razlika je mala djelomično i zbog naprednih arhitekturalnih mehanizama na procesoru koji ubrzavaju indeksno adresiranje. Kad se programski kôd optimizira, tj. kad posebni dijelovi prevodioca rasporede i prilagode strojne instrukcije za najbrže izvođenje, razlika se još smanji, na svega oko 1%. Primjer 7.5 U sljedećem primjeru odgonetnite na koje elemente poretka pokazuju kazaljke, te predvidite ispise programa za programske odsječke a, b i c. Otklonite sintaktičke greške te izvršite testiranje. const short int cN = 5; short int sArr[cN] = {0, 1, 2, 3, 4}; // 1-dim poredak veličine 5 short int *pS1, *pS2; // Pokazivači na short int cout << << << << << <<
"\n1-DIM POREDAK: short int sArr[" << cN << "]\n\n" "Adresa 0-tog el. = &sArr[0] = sArr = " << sArr << "\n\n\n" " ADRESA A" "\t\tSADRŽAJ od A\n" "===================================================\n" endl;
// A. pS1 = &sArr[0] ; pS2 = sArr + 1 ; sArr++; // Ispis A: cout << "pS1 = sArr << "pS2 = sArr + 1 << endl; // B. pS1 = sArr; pS2 = sArr + cN – 1 ;
// Kuda pokazuje pS1? // Kuda pokazuje pS2? // Možemo li promijeniti vrijdnost od sArr? = " << pS1 << "\t *pS1 = " << *pS1 << "\n" = " << pS2 << "\t *pS2 = " << *pS2 << "\n"
// Kuda pokazuje pS1? // Kuda pokazuje pS2?
// Ispis B: cout << "pS1 = sArr = " << pS1 << "\t *pS1 = " << *pS1 << "\n" << "pS2 = sArr + cN – 1 = " << pS2 << "\t *pS2 = " << *pS2 << "\n" << endl; // C. pS1 = sArr + cN/2 ; pS2 = sArr + cN/2 + 1;
// Kuda pokazuje pS1? // Kuda pokazuje pS2?
92 // Ispis C: cout << "pS1 = sArr + cN/2 = " << pS1 << "\t *pS1 = " << *pS1 << "\n" << "pS2 = sArr + cN/2 + 1 = " << pS2 << "\t *pS2 = " << *pS2 << "\n" << endl;
Primjer 7.6 Odgovorite na pitanja kao i u prethodnom primjeru. Dodatno odgovorite u svezi dijela programa B je li pristup elementima poretka nizanjem tvrdnji u redu? Koju programsku strukturu treba uporabiti za ispis elemenata između proizvoljnog početnog i konačnog elementa poretka? const short int cN = 10; // Cjelobrojni poredak: int iA[cN] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; int * pI = iA; cout << << << <<
// Uvodimo pokazivač koji pokazuje na početni element // poretka iA, dakle na iA[0]
"1-DIM POREDAK: int iA[" << cN << "]\n\n" " iA = " << iA << "\n" "======================================================\n" endl;
// A. cout << "Adresa: iA = " << iA << "\t\tSadržaj: *iA = " << *iA << "\n" << endl; // B. // Pristup k prva dva elementa poretka iA[0], iA[1], // te zadnjem elementu iA[cN-1] preko pokazivača (pI = iA ): cout << "Adresa: pI = " << pI << "\t\tSadržaj: *pI = " << *pI << "\n"; pI++; cout << "Adresa: pI = " << pI << "\t\tSadržaj: *pI = " << *pI << "\n"; << endl; pI = iA + cN – 1; cout << "Adresa: pI = " << pI << "\t\tSadržaj: *pI = " << *pI << "\n"; << endl; // Je li gornji ispis elemenata nizanjem tvrdnji u redu? // Kako ćete to napisati elegantnije i općenitije?
Primjer 7.7 Pristup elementima poretka preko kazaljki i for petlje. Zadan je isti poredak kao i u prethodnom primjeru. Pažljivo proučite korištenje for petlje i pokazivača te odgovorite: a) Što koristimo umjesto cjelobrojnog brojača kao indeks petlje. Kako smo inicijalizirali taj indeks, tj. koja mu je početna a koja konačna vrijednost za koju se «tijelo» petlje (tj. blok tvrdnji petlje) izvršava? b) Koja je vrijednost «indeksa» petlje po izlasku iz nje? Zapazite da u općenitom slučaju valjana vrijednost pokazivača! c) Smijemo li ipak iskoristiti tu vrijednost za izračun broja elemenata u poretku i raspona njegovih adresa, ili treba štogod modificirati? d) Uočite što vraća operator sizeof primijenjen na ime poretka. const short int cN = 10;
93 // Cjelobrojni poredak: int iA[cN] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; cout << << << <<
"1-DIM POREDAK: int iA[" << cN << "]\n\n" " iA = " << iA << "\n" "======================================================" endl;
// A. // Pristup proizvoljnom broju slijednih elemenata poretka // preko pokazivača, korištenjem for petlje: for (int *pI = iA ; pI < iA + cN ; pI++) cout << "Adresa: pI = " << pI << "\t\tSadržaj: *pI = " << *pI << "\n"; cout << "======================================================" << "\n" << endl; // B. // Kuda pokazuje pokazivač pI po izlazu iz petlje? // Je li to dio našeg poretka? Provjera: cout << << << <<
"Vrijednost pokazivača nakon prolaska kroz poredak:" << "\n\n" "Adresa: pI = " << pI "\t\tSadržaj: *pI = " << *pI << "\n" endl;
// C. // Pokazivač pokazuje na prvo prazno mjesto iza poretka! // => Broj elemenata poretka: UINT uNel = pI – iA ; // => Broj bajtova u poretku: UINT uSizeiA1 = uNel * sizeof *pI; // pI – iA = ? UINT uSizeiA2 = (UINT) pI – (UINT) iA; // (UINT) pI – (UINT) iA = ? // Ispis C: cout << "Broj elemenata u poretku: uNel = pI – iA = " << uNel << "\n\n" << "Vel. poretka iA preko razl. kazalj. = (pI–iA)* sizeof *pI = " << uSizeiA1 << "B\n" << "Vel. poretka iA preko razl. adresa = (UINT) pI – (UINT) iA = " << uSizeiA1 << "B\n" << "Vel. poretka iA preko funkcije (oper.) sizeof = sizeof(iA) = " << sizeof(iA) << "B\n" << endl; ///////
Kazaljke i indeksacija. Pošto se identifikator imena poretka interpretira kao adresa početnog elementa, te pošto se njegova vrijednost može pridružiti pokazivačima istog tipa, jezik C/C++ dopušta da se iza svakog pokazivača koristi indeksacija unutar uglatih zagrada, baš kao i kod «legalno» deklariranih poredaka. Tako za u sljedeći programski odsječak: typeArr tArr[n]; typeArr* pT = tArr; // ... ... for (UINT i = 0; i < n ; i++) if ( pT[i] != tArr[i] ) break; // ...
...
94
vrijedi da je pT[i] == tArr[i] za svaki i = 0, 1, … … n – 1, pa će se gornja petlja izvršiti točno n puta, i konačna vrijednost indeksa i će biti n. Očito je da za uporabu indeksa unutar uglatih zagrada iza pokazivača vrijedi sljedeća jednakost: pT[i] == *(pT + i) .
Pitanje je stila što će programer rabiti. Drugim riječima za izračun adrese s koje će se uzeti varijabla tipa typeArr se koristi ista formula (6.1) kao za izračun adrese i-tog elementa poretka s početnim elementom na adresi pT. Tu, naravno, treba strogo voditi računa o svakoj promjeni kazaljke pT. Naime dok prevodilac zabranjuje bilo kakvu aritmetiku s tArr, to nije slučaj i s pravim pokazivačem pT. pT[i]
Sljedeći primjer ilustrira neuobičajenu uporabu indeksacije uz kazaljku koja zahtijeva oprez i dobro poznavanje jezika C/C++. Primjer 7.8* Na korisničkom izvedbenom stogu pohranjene su lokalne varijable. Zatim je definirana kazaljka s adresom zadnje pohranjene varijable. Odgovorite: a) Ako je varijabla i7 pohranjena na adresu ABP , na kojim su adresama sve ostale varijable? b) Provjerite što se ispisuje for petljom. Vrijedi li jednakost pI[i] == *(pI + i)? c) Koju činjenicu možemo ponovo utvrditi o redoslijedu pohrane lokalnih varijabli, te o uobičajenom redoslijedu pohrane elemenata poretka? // ...
...
int main() // Pointers and indexation { const unsigned short int cN = 8; // cN integer variables on the local run-time stack: int i7 = 7, i6 = 6, i5 = 5, i4 = 4; int i3 = 3, i2 = 2, i1 = 1, i0 = 0; int* pI = &i0; cout << << << << << << <<
"// Variables on the run-time stack \n" "//(arranged from lower to higher addresses):\n\n" "int i7 = 7, i6 = 6, i5 = 5, i4 = 4 ;\n" "int i3 = 3, i2 = 2, i1 = 1, i0 = 0 ;\n\n" "int* pI = &i0; \n\n" "Pointers and indices:\n" "=============================================\n";
// Validation: pI[i] == *(pI + i) ?? for (UINT i = 0; i < cN; i++) cout << "pI[" << i << "] = " << pI[i] << "\t*(pI + " << i << ") = " << *(pI + i) << "\n"; cout << "=============================================\n" << endl; }
return 0;
Dinamičko alociranje memorije operatorom new Kao što je već rečeno, lokalne varijable uključujući i one koje definiraju strukture i objekte (npr. poretke) u jeziku C/C++ spremaju se na stog. Stog gradi svaka pozvana funkcija, uključujući i funkciju main. Sva potrebna mjesta za lokalne varijable prevodilac će unaprijed rezervirati, tj. ovako definirani «objekti» ne mogu se mijenjati niti uvoditi u tijeku izvođenja.
95
Jezik C/C++ omogućuje i «dinamičko pridjeljivanje memorije» (engl. dynamic memory allocation), s pomoću operatora new, prema sljedećoj sintaksi: type * pType = new type() ;
Iza operatora new nalazi se identifikator tipa podataka (ili C/C++ strukture definirane sa struct, ili klase, vidi kasnije), koji u objektno orijentiranom jeziku C++ ima ulogu tzv. konstruktora. Konstruktor ima ulogu da se stvoreni objekt ispravno inicijalizira, a o njegovim detaljima bit će riječi kod upoznavanja s klasama. Za ovako stvoreni objekt memorija se alocira u tijeku izvođenja programa u posebnom dijelu memorije, tzv. hrpi (engl. heap). Hrpa je dio radne memorije pod kontrolom mehanizma operacijskog sustava koji se naziva memorijski upravljač (engl. memory manager), i stoji na raspolaganju svim aktivnim programima. Memorijski upravljač ima pregled nad slobodnim dijelovima memorije, tj. njihovim početnim adresama i broju slobodnih blokova. Kad se u C/C++ programu pojavi operator new, prevodilac će u objektni kod ugraditi poziv na mehanizme (također C/C++ funkcije) koji će od operacijskog sustava zatražiti potrebnu veličinu memorijskog bloka. Ukoliko je alokacija memorije izvršena uspješno, tj. ako je bilo dovoljno slobodne memorije na hrpi, operator new vraća pokazivač odgovarajućeg tipa s adresom pridjeljene memorije. Ukoliko pridjeljivanje memorijskog prostora nije uspjelo, vraća se pokazivač vrijednosti NULL. Dodijeljena memorija se miče iz liste slobodne memorije kojom upravlja memorijski upravljač. Svi objekti (varijable, strukture) začeti operatorom new moraju na koncu biti «obrisani», tj. memorija mora biti izmještena (dealocirana, od engl. deallocation) uporabom operatora delete. Termin brisanje se ne smije shvatiti doslovno, radi se samo o povratu dotada zauzete memorije natrag, u listu slobodne memorije. Sadržaj memorije se pritom ne dira, već će biti «prebrisan» novim sadržajem pri ponovnoj alokaciji. Na taj se način obnavlja memorijska hrpa. Operator delete u C++ poziva tzv. destruktor objekta (vidi kasnije) koji može vršiti određene radnje pri prestanku postojanja objekta. Zasada je dovoljno reći da će se uporabom operatora delete za uobičajene tipove podataka obaviti upravo opisano oslobađanje memorije. U našem slučaju, kad nam gore kreirani objekt tipa type više ne treba, odnosno prije nego se napušta funkcija unutar koje je stvoren, moramo ga izmjestiti na sljedeći način: delete pType ;
Propuštanje izmještanja objekata dovodi problema koji se naziva curenje memorije (engl. memory leakage), tj. nakon nekog vremena uporabe takvog programa dovodi do manjka memorije za druge programe. Pošto memorija nije bila oslobođena na vrijeme i vraćena na raspolaganje memorijskom upravljaču, ona je nepovratno izgubljena čak i po prestanku rada spornog programa, sve do resetiranja računala. Stoga vrijedi nezaobilazno pravilo: svi objekti kreirani operatorom new moraju biti «obrisani» operatorom delete. Kreiranje s pomoću operatora new namijenjeno je prvenstveno kreiranju objekata pri ostvarenju dinamičkih struktura podataka kao što su vezane liste (engl. linked lists), stabla (engl. trees) i sl., koje su nužne pri realizaciji operacijskih sustava, prevodioca, uređivača teksta i drugih složenih programa. Iako na ovaj način možemo kreirati i dinamičke varijable jednostavnih tipova, to najčešće nema smisla. Npr. sljedeća tvrdnja dinamički definira varijablu tipa double i pridjeljuje joj vrijednost broja “google” ( 1.0 × 10100 ): double * pD = new double(1.0e100);
Ovu varijablu moramo obavezno brisati tvrdnjom: delete pF;
prije napuštanja bloka u kojem je objavljena. Istu bismo stvar postigli uporabom automatske (lokalne) varijable objavljene kao: double dD =
1.0e100;
96
o čijem brisanju ne moramo voditi računa. Dinamička alokacija poredaka. Počevši od inačice Microsoft© Visual C++ 5.0 prevodilac podržava dinamičku alokaciju poredaka operatorom new, gdje je veličinu poretka moguće odrediti u vrijeme izvođenja, kao u sljedećem primjeru: int n; // ... ... type * pTArr = new type[n];
Primijetimo da varijabla n nije kvalificirana kao konstanta, te da je po volji možemo zadati u toku izvođenja. Elementima ovog poretka možemo pristupati navođenjem indekasa u uglatoj zagradi iza imena pokazivača, tj. kao pTArr[i], i = 0, 1, 2, … … , n – 1. Pošto je pTArr pravi pokazivač na njemu je dozvoljena uobičajena aritmetika pokazivača, no jasno je da ga nije uputno mijenjati, odnosno da se o njegovoj promjeni mora strogo voditi računa. Npr. nakon njegove inkrementacije za jedan tvrdnjom: pTArr++, pisanjem pTArr[0] navodimo u stvari 1-vi, a ne 0-ti element. Zato je najbolje da ulogu promjenjivog pokazivača preuzme drugi pokazivač. U sljedećem su odsječku elementi gore objavljenog poretka dohvaćeni na dva različita načina: // ... ... type * pT = pTArr; *(pT+= 2) = pTArr[1] ; // <=> pTArr[2] = pTArr[1]
Pošto pTArr nismo mijenjali, i dalje ga možemo koristiti kao «ime» poretka. Prije nego pokazivač pTArr izađe iz dosega, dakle prije kraja funkcije ili bloka u kojem je deklariran, nužno moramo «obrisati» poretke, tj. izmjestiti ga (dealocirati), što se jednostavno čini na sljedeći način: delete [] pTArr ;
Primjer 7.9 Potrebno je kreirati poredak tipa int dinamički, uporabom operatora new. Veličinu poretka unosi korisnik sa tastature. Program dojavljuje slučaj kad je dodjela memorije neuspješna, te ispisuje indeks, adresu i vrijednost prvog i posljednjeg elementa poretka. Prije završetka funkcije obavezno izmjestite poredak operatorom delete. Prilikom testiranja programa ustanovite koliko memorije stoji na raspolaganju na hrpi unašanjem velikih brojeva za dimenziju poretka (npr. 106, 107, 108, 109 ). // Dinamička kreacija poretka UINT uN; char c; do {
cout << "\n1-dim poredak iA kreiran dinamicki\n"; do { cout << "\nUnesite dimenziju n > 0, n = "; cin >> uN; } while(uN == 0); cout << endl; int *piA = new int[uN]; if (piA == NULL) cout << "Alokacija neuspjela! Premalo slobodne memorije!!!\n"; else { UINT uI = 0; cout << "Indeks\t\tAdresa el.\t\tVrijednost el. \n" << "======================================================\n";
97 cout << uI << "\t\t" << piA << "\t\t" << *piA << "\n"; uI = uN – 1; cout << uI << "\t\t" << piA + uI << "\t\t" << *(piA + uI) << "\n" << endl; } cout << " \nPonovo? (d/n): "; cin >> c; // Obavezna dealokacija svega kreiranog s new: delete [] piA; }while (c != 'n');
Zadatak 7.10* Istražite kako se ponaša računalo prilikom curenja memorije. U prethodnom zadatku uklonite tvrdnju delete prevađajući je u komentar. Zatim nekoliko puta uzastopce unašajte najveće dimenzije poredaka koji su se u primjeru 7.8 normalno alocirali. Što primjećujete? Što se događa s računalom? Kako se ponašaju ostali programi? Što morate učiniti da se rad računala normalizira?
Navodnički (referencijski) tip Varijabla navodničkog tipa ili navod, a koristi se i riječ referencija, kraće referenca (od engl. reference type, ili reference), je veličina koja predstavlja adresu varijable navedenog tipa (objekta), ali se sintaktički ponaša kao sama varijabla (objekt). Dakle, u skladu s imenom, prevodilac navodničkom tipu pristupa posredno, te na mjestu objave smješta adresu varijable, a ne njenu vrijednost. S druge strane, prilikom rada s navodničkim varijablama, prevodilac radi s vrijednostima na koje ta adresa pokazuje, a takva je i sintaksa koja se rabi. Ona sugerira programeru da radi izravno s vrijednostima takvih varijabli (vidi niže). Očita je funkcionalna sličnost s pokazivačima, jer se u suštini radi o pristupu varijabli preko njene adrese. Međutim, postoji više opravdanja za uvođenje ovog tipa. Njegova najčešća uporaba je pri pozivu funkcijskih argumenata «po navodu» (vidi odjeljak 7.3), što je uobičajeno i u drugim programskim jezicima (FORTRAN, Pascal). Tu će posebnost navodničkog tipa pojednostaviti sintaksu uporabe argumenata u tijelu funkcije. Također, u odnosu na pokazivački tip, postoje i razlike u inicijalizaciji navoda, dozvoljenim operacijama na njima, te općenito načinu uporabe. Deklaracija navodničkog tipa se obavlja uporabom operatora & (koji u svojoj prefiksnof formi predstavlja operator «adresa od») iza specifikacije tipa. Npr. uz općeniti tip type, slično kao i kod pokazivača, možemo s type& definirati navodnički tip, odnosno varijablu koja će biti "navod na tip type". Npr. tvorbu int& možemo čitati kao "int navod" ili "int ref". Tako je sljedećom deklaracijom: type tVar1, tVar2 ; // ... ... type& rT = tVar1 ;
objavljen navod rT na tip type, i odmah inicijaliziran na vrijednost adrese varijable tVar1 istog tipa. Za razliku od pokazivača, navodnički tipovi se moraju inicijalizirati odmah po objavi. To znači da varijabla čiju adresu ćemo pridružiti određenom navodu već mora biti objavljena, tj. već ima rezervirano mjesto i svoju adresu, tako da je prevodilac može pridijeliti određenom navodu. Također, dodatna razlika je i to što se reference ne mogu preusmjeravati na druge objekte. Dok god je navod dogledljiv (engl. ”under the scope”) on ima konstantnu vrijednost adrese, tj. onu od početne inicijalizacije. Shodno tome, nikakva aritmetika nije dozvoljena na adresi predstavljenoj navodom, što je također bitna različitost u odnosu na pokazivački tip.
98
Sintaktička razlika se uočava i iz objave, a postoji i u uporabi navodničkog tipa. Da bismo pristupili sadržaju na koji pokazuje neka kazaljka moramo je odnavesti (dereferencirati). S druge strane, kod navoda jednostavno napišemo njegovo ime i prevodilac će raditi sa sadržajem adrese koju je pridijelio navodu. Tako će u pridruživanju: rT = tVar2 ;
vrijednost varijable tVar2 biti pridružena sadržaju na koji referira rT, a ne adresi. Također, pošto je rT bio inicijaliziran da navodi na tVar1 (tj. ima ulogu adrese od tVar1), u drugom pridruživanju će na tu adresu biti pospremljena vrijednost od tVar2. Dakle rezultat je da će vrijednosti tVar1 i tVar2 biti jednake (vidi Primjer 7.11). Da smo to isto htjeli napraviti s kazaljkama, postupili bismo na sljedeći način: type tVar1, tVar2 ; // ... ... type* pT = &tVar1 ; *pT = tVar2;
Glavna je uporaba navodničkog tipa pri prenošenju argumenata funkcije kod poziva po navodu (engl. call by reference), za razliku od uobičajenog poziva po vrijednosti (engl. call by value). Također, navodnički tip se često koristi kao povratni tip funkcije u slučajevima kad ona vraća složeni tip podataka (poredak, strukturu i sl.). Stoga će daljnje razmatranje navodničkog tipa u tom kontekstu bit dano u poglavlju o funkcijama (pogl. 6, također vidi pogl. 9). Napomena o imenima navodničkog tipa: Iako su ovdje i u primjerima koji slijede imena varijabli navodničkog tipa naglašena početnim slovom r, u praksi to nije potrebno raditi. Za razliku od pokazivačkog tipa gdje je početno slovo p koristan podsjetnik programeru da radi s adresama, kod navoda je to prepušteno prevodiocu. Kao što je već naglašeno, sintaksa sugerira programeru da radi s vrijednostima varijabli, pa u imenima ne treba naglašavati da se radi o navodničkom tipu. Primjer 7.11 Promotrite sljedeći programski odsječak, odgonetnite vrijednosti svih varijabli, odgovorite na pitanja, te predvidite ispis. int i1 = 10, i2 = 20; // Navodnički tip (The Reference Type) // Obavezna inicijalizacija pri objavi: int& rI = i1;
// Što prevodilac bilježi na mjestu navoda rI? // Što se "sintaktički" podrazumijeva pisanjem rI? // rI = ?
// Ispis A: cout << "Ispis nakon: int i1 = 10, i1 = 20; int& rI = i1;\n" << "===================================================\n" << "i1 = " << i1 << "\t\ti2 = " << i2 << "\t\t rI = " << rI << "\n" << endl; rI = i2;
// Što se mijenja – adresa pridijeljena navodu rI ili // sadržaj na toj adresi? // rI = ?, i1 = ?, i2 = ?
// Ispis B: cout << "Ispis nakon: rI = i2;\n" << "===================================================\n" << "i1 = " << i1 << "\t\ti2 = " << i2 << "\t\t rI = " << rI << "\n" << endl; //////////////////////
99
Primjer 7.12* U ovom primjeru, koji se nastavlja na prethodni, pobliže promatramo sintaktičke osobitosti navodničkog tipa i njegov način pohrane u memoriji. Prve tri varijable objavljene su i začete kao i gore. Kao što je rečeno ranije u ovom poglavlju, sve lokalne varijable smještaju se na korisnički izvedbeni stog (vidi odjeljak o programskog modelu memorije i o smještaju lokalnih varijabli, te Primjer 7.11). U našem primjeru, najprije je na stog stavljena cjelobrojna varijabla i1. Neka to bude na adresi AS . Zatim je na adresu AS – 4 stavljena varijabla i2, a poslije nje na adresu AS – 8 dolazi referenca rI. Nakon toga smo definirali još četiri varijable pokazivačkog tipa, koje se po istom načelu smještaju na lokalni stog. Kazaljka prI je definirana kao prI = &rI, što bismo uobičajeno čitali kao «adresa od rI». Međutim, važno je primijetiti da prevodilac u prI neće pohraniti adresu na kojoj je smještena referenca rI, kao što bismo mogli očekivati, već će u duhu sintaktičke posebnosti navodničkog tipa kazaljci prI pridružiti adresu koja je smještena u rI. Stoga da dobijemo adresu od rI, poznavajući smještaj lokalnih varijabli na stogu, uvodimo kazaljku prA koja sadrži adresu za 4 manju od pI1. Na tako dobivenoj adresi se zaista nalazi navod rI. Ukoliko ste razumjeli prije izneseno gradivo ove vježbe, sada ćete lako odgovoriti na sljedeća pitanja vezana uz pojedine dijelove programskog koda. a) Objasnite sve vrijednosti ispisanih kazaljki. Skicirajte lokalni stog, odredite kolika je vrijednost adrese AS prve i adresa ostalih varijabli u konkretnom slučaju, te smjestite varijable. Na kojoj je adresi smješten navod rI? Što je upisano na toj adresi (za ispis sadržaja uporabljen je izlazni «manipulator» hex koji sav naknadni ispis mijenja u heksadekadsku formu, vidi pogl. 2). Napomena: zaključke možete provjeriti i pokretanjem ispravljača (engl. “debugger”), uz prethodnu postavku stojnih točaka (engl. break point), te unutar izbornika pogleda (view) odabirom ispravljačkog prozora (debug window) kao zbirnog obratnika (disassembly). b) Što se zaista promijenilo nakon pridruživanja rI = i2? Je li došlo do promjena sadržaja na adresi na kojoj je pohranjena referenca rI? Kako to da se promijenila vrijednost od i1? Objasnite detaljno što se događa. int i1 = 10, i2 = 20;
// Lokalne varijable i1, i2, duljine 4B.
// A. // Navodnički tip (reference type) int& rI = i1; // Lokalna varijabla rI, tipa “int ref”. int *pI1 = &i1, *pI2 = &i2; // Kazaljke na i1 i i2 respektivno. int *prI = &rI; int *prA = pI2 - 1 ;
// Kazaljka prI pokazuje na što? // Kazaljka prA pokazuje na što?
// Ispis A: cout << "a: int i1 = " << i1 << ", i2 = " << i2 << "; int& rI = i1;\n" << " int *pI1 = &i1, *pI2 = &i2; int* prI = &rI;\n" << " int* prA = pI2 – 1 ;\n" << "========================================================\n" << "i1 = " << i1 << "\t\ti2 = " << i2 << "\t\t rI = " << rI << "\n\n" << "pI1 = " << pI1 << "\n" << "pI2 = " << pI2 << "\n" << "prI = " << prI << "\n" // prI = ??? << "prA = " << prA << "\n" // prA = ??? << "*prA = " << hex << *prA << dec << "\n" // *prA = ??? << endl; // B. rI = i2;
// rI = ?, i1 = ?, i2 = ?
// Ispis B: cout << "b: rI = i2;\n" << "========================================================\n" << "i1 = " << i1 << "\t\ti2 = " << i2 << "\t\t rI = " << rI << "\n\n"
100 << << << << << <<
"pI1 = " << pI1 << "\n" "pI2 = " << pI2 << "\n" "prI = " << prI << "\n" "prA = " << prA << "\n" "*prA = " << hex << *prA << dec << "\n" endl;
//////////////////////
// prI = ??? // prA = ??? // *prA = ???
101
Poglavlje 8.
C (C++) funkcije
Već u uvodnom poglavlju je naglašeno da su funkcije osnovne programske cjeline jezika C/C++. Tako svaki C/C++ ima jednu i samo jednu glavnu funkciju pod nazivom main(), s kojom smo se već upoznali. Nadalje, svaki samostalni i dobro definirani programski zadatak, radnju i sl., nastojat ćemo programirati za općeniti slučaj i organizirati je kao C/C++ funkciju koju potom možemo pozivati i iz drugih programa. Time se ostvaruje pretpostavka za ponovnu uporabu (engl. reusability) programa i njegovo učinkovitije korištenje.
Programi, potprogrami, procedure i funkcije Prilikom izrade računalnih programa stalno se javlja potreba za ponovnim korištenjem određenih dijelova programskog koda i programskih cjelina. Tako smo i u dosadašnjim primjerima intenzivno koristili funkcionalnost operatora kopiranja << i >> uz objekte cout i cin za ispis na prikazniku i unos sa tastature. Ti operatori su bili objavljeni u zaglavnoj datoteci iostream.h, a ostvareni u datoteci iostream.cpp. Njihova je realizacija i uporaba slična kao i za funkcije, uz razlike koje su jedino sintaktičke naravi.* Također, u pogl. 2 koristili smo ispisnu funkciju printf(). Svaki put kad smo ih koristili izvršen je poziv na relativno složeni programski kôd koji je obavio traženu zadaću, a da mi nismo trebali znati ništa o tome kako je on ostvaren, niti koje sve računarske resurse koristi. U tome je sadržana ideja potprograma kao programske cjeline namijenjene rješavanju određenog zadataka. Korisnik mora znati samo koje ulazne podatke mora «poslati», te kakve rezultate treba očekivati. Radi se o temeljnom načelu skrivanja informacija (engl. information hiding), koje se treba poštivati u najvećoj mogućoj mjeri. Količina informacije koja se izmjenjuje između pozivnog programa i pozvanog potprograma mora biti minimalna, tj. upravo ona koja je dovoljna za obavljanje zadatka. Ništa drugo ne mora i ne smije «znati» program o potprogramu i obrnuto. Potprogram liči na softversku crnu kutiju, čija je osobitost da znamo koji su potrebni ulazni podaci da bi se izvršila određena zadaća, ali ništa ne znamo, odnosno ne trebamo znati, o tome kako ta crna kutija radi. Zbog očite važnosti koncepta potprograma, procesori su već zarana osiguravali strojne instrukcije za podršku potprograma. Tako su u zbirnim jezicima postojale tvrdnje za grananje (skok) na tzv. unutarnji potprogram, a kasnije i za poziv (engl. call) zasebnih i nezavisnih programskih modula. Viši programski jezici su razlikovali više inačica potprograma. Npr. jezik FORTRAN, namijenjen za znanstvenu uporabu i intenzivne numeričke proračune rabi tzv. unutarnje potprograme (engl. subroutine), vanjske potprograme (tzv. naredba call), a poznavao je i funkcije, specifične po tome da, baš kao i matematičke funkcije, vraćaju konkretnu vrijednost koja se izravno pridružuje varijabli. Jezik Pascal, čuven po svojoj formalnoj korektnosti i čvrstom «proceduralnom pristupu», također posjeduje funkcije, te procedure kao zasebne programske cjeline. Oba ova jezika, kao i većina ostalih, imali su i koncept glavnog programa (engl. main program), unutar kojeg se organizira „glavna aktivnost“ glede deklariranja varijabli, izvršavanja programskih tvrdnji, te pozivanja unutarnjih i vanjskih potprograma i funkcija.
*
Operatori se u C/C++ objavljuju ključnom riječi operator. Ona, prema definiciji, objavljuje funkciju kokja opisuje nad kojim tipom podataka i što taj operator radi. Dakle i formalno se operatori svode na funkcije, no zadržava se način pisanja specifičan za uporabu operatora (vidi pog. 2).
102
Unutarnji potprogrami su dijelovi programskog koda koji se mogu opetovano pozivati bez potrebe za njihovim ponovnim pisanjem, kod kojih je sveza s pozivnim programom (dakle onim iz kojeg je izvršen poziv potprograma) vrlo velik. Za takve je potprograme bilo tipično da koriste iste varijable kao i programi iz kojih su pozvani. To je ujedno značilo da ih nije bilo lako premještati i koristiti u drugim programima, jer nisu činili samostalne cjeline. S druge strane, vanjski potprogrami djeluju kao zasebne programske cjeline, s vlastitim lokalnim varijablama neovisnim od „ostalog svijeta“. Ovakva nezavisnost ih čini uporabljivim iz svih programa, no zato je potrebno osigurati prijenos argumenata, odnosno varijabli koje služe kao ulazne veličine, jednako kao i rezultata, odnosno izlaznih veličina. O tome će detaljnije biti riječi u odjeljku o prijenosu argumenata. C funkcije. Tvorci jezika C uočili su da se sve „zasebne programske cjeline“ mogu elegantno svesti na funkcije (engl. functions). I glavni program je organiziran kao funkcija, ali posebnog i jedinstvenog imena main(). Razlog za takav pristup leži u činjenici da ne postoji suštinska razlika između pozivnog programa A (onog iz kojeg pozvan program B), i pozvanog programa B (onog kojeg je program A pozvao), osim u možebitnom prvenstvu izvršavanja. Posebice je to očito kod rekurzivnih poziva, gdje «potprogram», poziva samog sebe. Na koncu, i glavnu funkciju main() svakog C / C++ programa poziva neka funkcija operacijskog sustava, u okviru mehanizma punioca (engl. loader). Njemu je zadaća da učita izvršnu verziju programa u radnu memoriju i potom «prenese kontrolu izvođenja» s operacijskog sustava na strojni kod dobiven prevođenjem izvorne verzije programa, što je ekvivalentno pozivu vanjskog potprograma. Stoga se programiranje u jeziku C/C++ ostvaruje unutar jedinstvene programske cjeline — funkcije, i govorimo o funkcijskom programiranju (engl. functional programming). Po uzoru na matematičke funkcije, na temelju nekih ulaznih veličina ili argumenata (parametera), određuje se rezultat ili više rezultata djelovanja funkcije. Djelovanje funkcije je ostvareno izvršavanjem tvrdnji određenih samom funkcijom, što uključuje i pozive drugih ili iste funkcije. Osnovna je ideja je da programiranje organiziramo tako da veći problem razdijelimo na jednostavnije, smislene i dobro definirane cjeline, koje ćemo programirati unutar C funkcija. Time smo ujedno ilustrirali metodu oblikovanja programa koja se naziva oblikovanje od vrha k dnu (engl. top-down design).* Tu je bitno je uočiti jednostavnije dijelove problema, i nastaviti dijeljenje sve dok ti dijelovi nisu relativno lako riješivi. S druge strane, kad za mnoge jednostavne dijelove različitih programa napišemo funkcije koje su općenito primjenjive, tada njihovim sastavljanjem možemo graditi komplesna rješenja. To je načelo oblikovanja od dna prema vrhu (engl. bottom-up design). Ova dva načela se mogu i kombinirati, što je i najčešći pristup u praksi. (Vidjeti također naputke za programiranje u pogl. 13xxx). Na koncu, kad smo jednostavnije dijelove problema riješili s pomoću C-funkcija, tada ćemo u glavnoj funkciji main() organizirati rješenja i vrlo komplesnih problema pozivanjem tih funkcija, od kojih će mnoge i same pozivati druge funkcije. Pri tom koristimo velik broj funkcija koje su već napisane i nalaze se u biblioteci standardnih ili dodanih funkcija. Velik dio vještine programiranja u jeziku C / C++ je upravo poznavanje velikog mnoštva funkcija koje su već napisane i stoje programerima na raspolaganju. Također, programer rabi i vlastita rješenja ostvarena kroz dobro definirane funkcije. Bez toga bi programiranje kompleksnih problema bilo upravo nezamislivo. C++ funkcije. Na konceptu vezanja funkcija uz određene tipove podataka u okvirima tzv. klasa, jezik C++ se izdvaja od svog prethodnika, jezika C. Osnova paradigme objektnog programiranja se upravo sastoji u tome da se svojstva funkcija dosljednije iskoriste, da im se poveća fleksibilnost i po*
Usporedite ovu metodu s latinskom uzrečicom: divide et impera (podijeli pa vladaj).
103
novna iskoristivost, te da se na koncu pojednostavi njihova uporaba za korisnika programera. U tom smislu se funkcije uvijek vežu uz klase, pa one mogu djelovati samo na objekte određene klase. Primjer takvog djelovanja smo već susretali koristeći izlazni objekt cout, izlazni objekt cin, kojima smo posredstvom operatora kopiranja << odnosno >> prosljeđivali ispis na ekran odnosno unos s tastature. U osnovi, operatori odgovaraju funkcijama uz sinstaktičke osobitosti, koje pojednostavljuju uporabu. S druge strane, jezik C++ u sebi sadrži kao podskup jezik C. Dakle, pisanje funkcija u C stilu, van klasa i dalje je moguće. Naravno, u tom slučaju napuštamo obrazac objektnog programiranja, te pišemo standardne proceduralne, ili funkcijske programe.
Formalni opis C/C++ funkcija prema C stilu. Definicija C/C++ funkcija prema C stilu. Funkcije se u C stilu definiraju prema sljedećoj sintaksi: returnType functionName ( argType1 argName1, argType2 argName2, … , argTypeN argNameN) { Statement1; Statement2; ... ... ... ... return retVar;
// retVar or expression of type returnType // No return variable retVar if returnType is void!
}
Na početku se definira tip funkcije returnType koji mora biti jednak tipu varijable koju vraćamo tvrdnjom return. Vrijednost koju funkcija vraća uobičajena je r-vrijednost koju u pozivnom programu možemo izravno pridružiti nekoj l-vrijednosti (npr. varijabli) s pomoću operatora pridruživanja, ili testirati, i sl. Ukoliko nema potrebe da funkcija vrati vrijednost, već njezina zadaća odgovara potprogramu, odnosno proceduri, koja rezultate prosljeđuje na drugi način, tada će tip funkcije biti void (od engl. void = hrv. ništavan, prazan, nepostojeći). U tom slučaju se u tvrdnji return ne smije navesti povratna varijabla ili izraz, odnosno cijela tvrdnja return se može izostaviti. Nakon tipa funkcije navodi se identifikator imena funkcije functionName. Ime funkcije odabiremo da bude čim sugestivnije i da čim bolje odražava radnju koju funkcija obavlja, nastojeći da ono bude kratko, pregledno i univerzalno razumljivo. Stil jezika C / C++ je uporaba malih slova, pa nazivi funkcija tradicionalno počinju malim slovima. Imenovanje funkcija vrlo je važno, i o tome će biti još riječi u sljedećim pasusima. Iza imena funkcije unutar okruglih zagrada slijedi tzv. lista argumenata ili parametara, (engl. argument list) odijeljenih zarezima. Za svaki argument rednog broja i = 1, … , N mora biti definiran njegov tip argTypei i ime argNamei , kao u gornjem primjeru. Upravo pod imenom argNamei ćemo i-ti argument referirati u tijelu funkcije. Argumenti koji se tu navode unutar okruglih zagrada su tzv. formalni argumenti (engl. formal arguments). Lako ih je prepoznati po tome što sintaksa sliči deklaraciji varijabli. Razlika je što ispred svakog imena argumenta mora biti naveden i njezin tip, neovisno o tome je li prethodni argument bio istog tipa, i u tome što se između argumenata navode zarezi. Poslije toga slijedi tijelo funkcije (engl. function body) u kojem se nalazi tvrdnja, odnosno blok tvrdnji. Vrijedi standardno pravilo da se blok može ispustiti ako se radi o samo jednoj tvrdnji. U tijelu funkcije deklariramo lokalne varijable te pišemo tvrdnje kojima ćemo ostvariti željenu zadaću funkcije. Kao što je već rečeno, za funkcije tipa različitog od void, obavezno je vratiti vrijednost tipa
104
jednakog tipu funkcije. To se uobičajeno vrši na kraju tijela funkcije, pošto se kod poslije tvrdnje return nikad neće izvršiti. Funkcije istog imena FunctionName smatraju se različitim ako su im različite liste argumenata po bilo kojoj osnovi. Dovoljno je da postoji barem jedna razlika u tipu ili broju argumenata. Drugim riječima možemo definirati više funkcija istog imena koje djeluju na različitima argumentima. Pošto u kompleksnom programiranju susrećemo vrlo velik broj funkcija, ovo je važna pogodnost koja smanjuje potrebu za variranjem imena funkcija. Međutim, nije dozvoljeno definirati novu funkciju koja se od već objavljene razlikuje samo u povratnom tipu. U skladu s Mađarskom notacijom, prvo slovo imena može označavati tip funkcije. Npr. ako je t prvo, ili karakteristično slovo tipa podataka T, tada je dobro sročeno ime tFunctionName. Moguće je da će postojati funkcija koja obavlja istu ili sličnu radnju, ima iste argumente, te vraća vrijednost drugog tipa podataka. U tom slučaju je opravdano i korisno kreirati ime na navedeni način. S druge srane, napomenimo odmah da je označavanje tipa funkcija u imenu manje uobičajeno i manje praktično nego što to vrijedi za varijable. Npr. ako imamo više funkcija koje sve rade istu zadaću ali s argumentima različitog tipa, najbolje je, kao što je već sugerirano gore, da su njihova imena identična. Također, ukoliko je vrijednost koju funkcija vraća sporedna, i ne predstavlja glavni rezultat djelovanja funkcije, što je čest slučaj kad preko return vraćamo vrijednost „zastavica“ (engl. flags) kao indikatora rada funkcije, tada je također nećemo isticati u u imenu funkcije. Ponekad je iz argumenata funkcije moguće odgonetnuti prirodan tip vrijednosti koji funkcija vraća. Na programeru je da pažljivo procijeni potrebu za označavanjem tipa u imenu funkcije, vodeći računa o navedenim smjernicama. Prototip funkcije. Primijetimo da su nužne i dovoljne informacije koje korisnik mora poznavati o nekoj funkciji dane njenim tipom, imenom i listom argumenata. Taj početni dio objave naziva se prototip (engl. prototype) funkcije. On se, dakle, od definicije razlikuje samo tome što se ispušta tijelo funkcije, tj. iza okrugle zagrade liste argumenata dolazi točka zarez: // Prototype of the function FunctionName: returnType FunctionName ( type1 arg1, type2 arg2, … … , typeN argN );
U duhu uvodnih razmatranja ovog poglavlja, jasno je da korisnik ništa ne mora znati o tijelu funkcije, ali mora poznavati njen prototip. Navođenje prototipa zvat ćemo objava ili deklaracija funkcije (engl. function declaration), slično kao kod objave arijabli. Njeno puno određenje, koje uključuje i dio objave (bez točke zaera) i tijelo funkcije, zvat ćemo definicija funkcije. (engl. function definition). Deklaracije funkcija se po potrebi združuju u zasebne zaglavne datoteke, o kojima je već bilo riječi (vidi pogl. 2 i pogl. 3). Kao što smo već vidjeli, da bi se rabila određena funkcija dovoljno je direktivom #include uključiti tu zaglavnu datoteku. Korisnik čitanjem prototipa funkcija i preciznih komentara doznaje sve što mu je potrebno da funkcije uspješno koristi.* Prevodilac će, znajući gdje se nalaze knjižnice s datotekama definicija funkcija, a to je najčešće u direktorijima u kojima se nalazi i on sam, na mjestu navođenja funkcije ugraditi programski kôd za poziv funkcije. Poziv C/C++ funkcija. Stvarni i formalni parametri. Poziv gore definirane općenite funkcije unutar nekog programskog odsječka odvija se na sljedeći način: Statement;
*
Precizni, detaljni i koncizni komentari obvezatni su u profesionalnoj implementaciji funkcija. Oni moraju opisati što funkcija radi, sugerirati gdje s može koristiti, navesti posebne slučajeve, i sl.
105 ... ... returnType retVar = FunctionName ( var1, var2, … … , varN ) ; ... ...
pod uvjetom da je returnType različit od void. Ukoliko ne trebamo povratnu vrijednost funkcije, tj. ako se rezultati djelovanja funkcije ostvaruju i na drugi način (vidi dalje), tada je dovoljno samo navesti ime funkcije i listu stvarnih argumenata. Jednako postpuamo u slučaju kad je returnType tipa void. Tada jednostavno „pozivamo“ funkciju, odnosno navodimo je bez pridruživanja njene povratne vrijednosti: Statement; ... ... FunctionName(var1, var2, … … , varN) ; ... ...
Dakle, svako navođenje imena funkcije s listom argumenata smatra se pozivom funkcije. Često se naglašava da se radi o «stvarnom pozivu», varijable navedene u listi argumenata su stvarni ili aktualni argumenti (engl. actual arguments), ili parametri koje koristimo u tijelu funkcije. Oni se po tipu i broju moraju slagati s formalnim argumentima navedenim u objavi (prototipu) funkcije. Formalni argumenti, dakle, služe za definiranje funkcije, a prilikom njenog stvarnog poziva oni će poprimiti vrijednosti aktualnih argumenata. Pošto u trenutku pisanja još ne znamo vrijednosti stvarnih argumenata, već ih zamišljamo (na temelju tipa), ovi se argumenti nazivaju još i zamišljeni ili fiktivni argumenti (engl. ficitve arguments). Aktualni argumenti se iz pozivne funkciju prenose u pozvanu funkciju preko stoga. Smještaju se unutar tzv. pozivnog okvira stoga (engl. call stack frame), neposredno prije lokalnih varijabli. Pošto stog raste prema manjim adresama, argumenti će, posljedično, biti smješteni na adresama neposredno većim od adresa lokalnih varijabli funkcije. Primjer 8.1 Potrebno je ostvariti funkciju pod imenom ipow (od engl. power function = hrv. potencija), koja za bazu tipa int računa cjelobrojnu n-tu potenciju, gdje je n ≥ 0 tipa unsigned short n int. Rezultat m neka je standardnog cjelobrojnog tipa (int ) . U glavnoj funkciji programa korisnika se pita za broj i stupanj (cjelobrojni eksponent) potencije, te ispisuje rezultat. Beskonačna petlja unutar koje se to odvija, prekida se pritiskom na Ctrl+Break. Napomena. U matematici se n-ta potencije baze b smatra funkcijom baze, tj. pišemo pn (b) = bn . Dakle, varijabla je b , a cjelobrojni eksponent n se tu smatra parametrom. Tako npr. govorimo o drugoj potenciji različitih brojeva b, što daje kvadratnu funkciju. Općenito, govorimo o n-toj potenciji varijable b . Prilikom programske implementacije, prirodno je da i bazu b i eksponent n tretiramo kao argumente funkcije, što poopćuje gornju matematičku definiciju. a) Moraju li se formalni i aktualni argumenti slagati po svojem nazivu? Po čemu se moraju slagati? b) Daje li ova funkcija dobar rezultat za potenciju n = 0? Obrazložite to na temelju poznavanja načina izvršavanja for petlje, a zatim i provjerite izvođenjem glavnog programa. Daje li ova funkcija dobar rezultat za 00 ? Najprije se prisjetite koliki bi taj rezultat trebao biti! Testirajte funkciju za negativne brojeve. c) Potencije u n rastu kao eksponencijalna funkcija, tj. vrlo brzo. Testirajte rezultate za 10n gdje je n = 7, 8, 9, 10, pa zatim za 2n uz n = 10, 20, 30, 31, 32, … , te za (–2)n za iste vrijednosti od n. Što primjećujete? Je li ova «jednostavna i elegantna» funkcija ujedno i robustna? Dojavljuje li se kako pozivnoj funkciji da je rezultat pogrešan? Objasnite!
106 // Potrebna uključenja (#include): // ... ... // Funkcija cjelobrojne potencije: int ipow(int iK, unsigned short int usL) { int iPw = 1; for (unsigned int i = 0; i < usL; i++) iPw *= iK; return iPw; } int main() { const short int cNmax = 128; short int iN; int iM; cout << << << <<
// Maksimalni eksponent. // Eksponent iN potencije iM^iN. // Baza iM potencije iM^iN.
"Racun potencije: m^n; int m, unsigned short int n.\n\n" "(Stop = 'Ctrl+Break')\n" "======================================================\n" endl;
// Beskonačna petlja (zaustavlja se "izvana" s Ctrl+Break) while(true) { cout << "m = "; cin >> iM; cout << "n = "; cin >> iN; // Ulazni filtar. Uvjet na eksp. iN : 0 <= iN <= cNmax. while( (iN < 0) || (iN > cNmax) ) { cout << "n( >= 0 ) = "; cin >> iN; } cout << "\n(" << iM << ")^" << iN << " = " << ipow(iM, iN) << "\n================================\n" << endl; } }
return 0;
Zadatak 8.2 Funkcija cjelobrojne potencije
pow
s proširenim opsegom vrijednosti.
Poboljšajte prethodnu cjelobrojnu funkciju ipow tako da proširite raspon njenih korektnih vrijednosti. Argumenti ove funkcije identični su kao i kod prethodne, (baza tipa int, cjelobrojnu n-ta potencija, n ≥ 0 tipa unsigned short int ). Rezultat mn neka je sada maksimalnog cjelobrojnog tipa za računarsku platformu na kojoj radite. Razmislite, ako koristimo unutar istog programa obje funkcije, i prethodnu ipow i ovu novu, bi li njihova imena smjela biti jednaka? Na temelju prethodnog odgovora opravdajte izbor jednostavnog imena: pow. Glavnu funkciju možete preuzeti iz gornjeg primjera, uz minimalne promjene. // Skica rješenja. // Funkcija cjelobrojne potencije proširenog opsega: long long int pow(int iK, unsigned short int usL) { long long int lPw = 1; // ... ... ...
}
107
Primjer 8.3 Funkcija cjelobrojne potencije dpow uz bazu tipa double. Potrebno je ostvariti funkciju pod imenom dpow koja za bazu tipa double računa cjelobrojnu potenciju. Stupanj potencije (eksponent) sada može biti i negativan. Unutar glavne funkcije programa korisnika se pita za broj i potenciju, te ispisuje rezultat. double dpow(double dX, short int n) { double dPw = 1. ; // Varijabla za pohranu iznosa potencije. bool bSign = false; // Predznak potencije (eksponenta) '-' = true. if (n < 0) { bSign = true; n *= -1; } for (unsigned i = 0; i < (unsigned short int) n; i++) dPw *= dX; if (bSign) dPw = 1./dPw; }
return dPw;
Zadatak 8.4 Napravite glavnu funkciju koja poziva prethodnu funkciju dpow. Korisniku se omogućava da proizvoljan broj puta unosi broj x tipa double, cjelobrojni eksponent n proizvoljnog predznaka, i da dobije ispisanu potenciju xn . Dodatni zadaci: a) Podesite točnost izlaznog ispisa s pomoću rukovaoca setprecision(int n) (vidi pogl. 2) na maksimalnu vrijednost za tip double. b) Testirajte funkciju za velike iznose koji se približavaju donjoj i gornjoj granici apsolutnih vrijednosti za tip double. c) Kakav je ispis u slučaju da se premaši maksimalan, odnosno minimalan broj? Što će se ispisati u slučaju da je x = 0, a n = – 1, tj. za 0 – 1 ? Zadatak 8.5* Proanalizirajte sljedeću glavnu funkciju koja poziva funkciju dpow. Podaci se unašaju preko funkcije scanf koja omogućuje višestruki formatirani unos, a ispisuju preko funkcije printf koju smo već susreli u pogl. 2. U izborniku Pomoć potražite detalje o ove dvije funkcije. Naučite osnove o načinu formatiranja ispisa i unosa. Primijetite da se taj format navodi u početnom C nizu. Na mjestu gdje se navede znak % bit će redom ispisana (upisana) vrijednost varijable iz liste iza C niza. Broj znakova % očito ne smije biti veći od broja varijabli (izraza) u listi. Iza znaka % se navode detalji formata, npr. l za int i double, f za float, zatim slijede pobliže oznake ispisa, kao d za decimalni, o za oktalni, x i X za heksadekadski ispis, itd. Provjerite mnoge odlike formata ove funkcije unošenjem ključnog pojma «Format Specification Fields” u tražilicu MSDN knjižnice. int main() { double dX; short int n; cout << << << <<
"Racun potencije: x^n, doulbe x, short int n, n je el. iz Z\n" "Stop = Ctrl+Break\n" "======================================================\n" endl;
108 // Beskonačna petlja: while(true) { printf("\nx = "); scanf("%lf", &dX); printf("n = "); scanf("%hd", &n); printf("\n%le ^%hd = %le\n", dX, n, dpow(dX, n)); printf("=====================================\n"); } return 0;
} Zadatak 8.6 Binomni koeficijenti (kombinacije). Napišite program koji izračunava binomne koeficijente, odnosno kombinacije:
⎛n⎞ n(n − 1)(n − 2) ... ... (n − k + 1) n! ⎜⎜ ⎟⎟ = C nk ≡ = . 1 × 2 × ... ... × k k!(n − k )! ⎝k ⎠ ⎛n⎞
Binomni koeficijent ⎜⎜ ⎟⎟ označava k-ti koeficijent (k = 0, 1, 2, … … , n) u razvoju n-te potencije k
⎝ ⎠
*
binoma :
(a + b )n
⎛ n⎞ ⎛n⎞ ⎛n ⎞ 2 n − 2 ⎛ n ⎞ 1 n −1 ⎛ n ⎞ 0 n ⎛n⎞ ⎟⎟a b + ⎜⎜ ⎟⎟a b + ⎜⎜ ⎟⎟a b = ⎜⎜ ⎟⎟a n b 0 + ⎜⎜ ⎟⎟a n −1b1 + ⎜⎜ ⎟⎟a n − 2 b 2 + ... ... + ⎜⎜ ⎝0⎠ ⎝1 ⎠ ⎝2⎠ ⎝ n − 2⎠ ⎝ n − 1⎠ ⎝n⎠ n(n − 1) n − 2 1 n(n − 1) n − 2 2 = a n + na n −1b1 + a b + ... ... + a b + na n −1b1 + b n . 2 2
C nk je ujedno i broj kombinacija k-tog reda od n-elementa, odnosno broj podskupova od k elemena6 ta koji se mogu sastaviti od skupa od n elemenata. Tako npr. C 45 predstavlja broj kombinacija «lo7 ta 6 od 45», a C 39 «lota 7 od 39». Za binomne koeficijente vrijedi simetrija, tj. C nk = C nn − k , pa je:
⎛n⎞ ⎛n⎞ ⎜⎜ ⎟⎟ = ⎜⎜ ⎟⎟ = 1, ⎝0⎠ ⎝n⎠
⎛n⎞ ⎛n ⎞ ⎜⎜ ⎟⎟ = ⎜⎜ ⎟⎟ = n , ⎝1 ⎠ ⎝ n − 1⎠
⎛n⎞ ⎛n ⎞ n(n − 2) ⎜⎜ ⎟⎟ = ⎜⎜ ⎟⎟ = , itd. 2 ⎝ 2⎠ ⎝ n − 2⎠
Primijetimo da je izračun binomnih koeficijenata preko faktorijela (prvi izraz u gornjoj formuli), numerički vrlo neprikladan. Naime prilikom izračuna faktorijela, posebice n! ( jer je n ≥ k ) ti međurezultati mogu biti vrlo veliki i premašivati opseg brojeva. S druge strane, pošto se iznos binomnog koeficijenta dobiva kao rezultat njihovog djeljenja, doći će do kraćenja velikih cijelih brojeva i rezultati mogu biti bitno manji. Stoga će dobar algoritam za izračun binomnog koeficijenta uvijek koristiti drugu formulu. Da ilustriramo to primjerima, broj kombinacija igre lota određuju se prema sljedećim konkretnim formulama:
*
Za detalje, te za obrazloženje kako se binomni koeficijenti za n-tu potenciju elegantno računaju s pomoću Pascalovog trokuta konzultirajte matematičku literaturu.
109
⎛ 45 ⎞ 45 × 44 × 43 × 42 × 41 × 40 6 , C 45 = ⎜⎜ ⎟⎟ = 1× 2 × 3 × 4 × 5 × 6 ⎝6 ⎠ ⎛ 39 ⎞ 39 × 38 × 37 × 36 × 35 × 34 × 33 . C 397 = ⎜⎜ ⎟⎟ = 1× 2 × 3 × 4 × 5 × 6 × 7 ⎝7 ⎠ Program treba pozivati funkciju tipa int pod nazivom binomcoef(), koja računa binomne koeficijente tako da se pri računu ostvaruje maksimalno kraćenje. U programu je potrebno ostvariti pregledan unos i kontrolu veličina k i n ( k ≤ n ), te omogućiti korisniku da ponavlja izračun proizvoljan broj puta. Naputak. Pokušajte ostvariti vlastito rješenje. pažljivo proučite navedeno rješenje funkcije koju ćete pozivati iz programa. Odgovorite može li se zbog greške cjelobrojnog dijeljenja desiti da funkcija vrati pogrešan rezultat? Razmotrite to na jednostavnijim primjerima, a zatim pokušajte izvesti opći zaključak. // Binomial Coefficient C(n, k) // int binomcoef(int n, int k) { if (2*k > n) k = n – k; if (k < 0) return 0; int iR = 1;
// Partial result iR
for (int i = 1; i <= k; i++) { iR = iR * n / i ; n--; } }
return iR;
///////////////////////////////// // Main program: ///////////////////////////////// // ... ... ...
Prijenos argumenata funkcije «po vrijednosti» i «po navodu» U uvodnom izlaganju o C funkcijama rečeno je da su to zasebne, nezavisne i dobro definirane programske cjeline, namijenjene rješavanju pojednihi dijelova problema, algoritama i sl. Poziv funkcija u jeziku C / C++ ostvaruju se s pomoću strojnih instrukcija za poziv vanjskih potprograma. Činjenica da se radi o zasebnim programskim cjelinama zahtjeva da se na odgovarajući način riješi razmjena ulaznih i izlaznih vrijednosti u funkcije i iz njih. Ranije je već je natuknuto da se argumenti funkcije prenose preko stoga. Prevodilac će u pozivnom programu ugraditi strojne instrukcije koje argumente funkcije stavljaju na stog u skladu s listom stvarnih argumenata. Podsjetimo se, lista stvarnih argumenata po redoslijedu i tipu mora biti podudarna s listom formalnih argumenata. Pozvana funkcija će na temelju liste formalnih argumenata «znati» gdje je (ispod stožnog okvira) smješten koji argument. Prijenos argumenata po vrijednosti (poziv po vrijednosti, ”call by value”). Normalni mehanizam prijenosa argumenata u jeziku C/C++ je prijenos po vrijednosti, (engl. pass by value) ili poziv po
110
vrijednosti (engl. call by value).* Vanjske potprograme ili procedure smo već spomenuli u uvodu. Kod ovakvog prijenosa se vrijednost stvarnog argumenta, dakle varijable navedene u pozivu funkcije, „kopira“ na stog. Pozvana funkcija radi isključivo s tom kopijom vrijednosti, i nema nikakav pristup originalnoj varijabli koja je poslužila kao aktualni argument, niti nema informaciju (adresu) gdje se originalna varijabla nalazi. To vrijedi za sve stvarne argumente koje se prenose po vrijednosti. Funkcija koristi te vrijednosti argumenata za potrebne izračune, ali se te iste vrijednosti «brišu» sa stoga po završetku izvođenja funkcije (tj. stožni okvir i mjesto ispod njega gdje su bili argumenti se «napušta» i argumenti postaju izgubljeni — bit će prebrisani pri eventualnom novom rastu stoga). Dakle, ako u funkciji koristimo prijenos argumenata po vrijednosti, kao što je to bilo u svim primjerima do sada, jedini način povrata rezultata funkcije je preko njezine vrijednosti, tj. tvrdnjom return. Iz toga proizlazi zaključak da funkcije tipa void koje primaju argumente prenesene po vrijednosti, ne mogu vratiti rezultat u pozivnu funkciju. Očito je da takve funkcije ima smisla pisati jedino ako one obavljaju zadatak koji ne traži povratni rezultat, kao npr. funkcije ispisa ili sl.† Normalni mehanizam prijenosa argumenata po vrijednosti predstavlja elegantan način međudjelovanja pozivne i pozvane funkcije uz maksimalno poštivanje načela skrivanja informacije. U tom slučaju nema načina da pozvana funkcija promijeni varijable pozvane funkcije, te tako namjerno ili nenamjerno, dobronamjerno ili nedobronamjerno utiče na varijable drugih funkcija koje je koriste i pozivaju. Neželjene posljedice promjena stanja varijabli uobičajeno se nazivaju „popratnim efektima“ (engl. side effects). Ukoliko nekoj funkciji prenosimo argumente isključvio po vrijednostima, popratne pojave su onemogućene. Stoga se ovakav tip prijenosa argumenata, koji je u jezicima C/C++ i sintaktički najjednostavniji, treba koristiti uvijek kad on funkcionalno udovoljava potrebama. Prijenos argumenata po navodu (poziv po navodu, ”call by reference”). Iako elegantan i računarski opravdan, prijenos argumenata po vrijednosti nije uvijek i funkcionalan, jer ne udovoljava mnogim potrebama koje se javljaju u programiranju. Tako u mnogim slučajevima postoji opravdana potreba da se izravno utiče na varijable u pozvanoj funkciji. Očiti je primjer rad s poredcima u kojima želimo utjecati na njihove elemente. Čak i kad ne bi trebalo mijenjati elemente polja, već samo izvršiti neke radnje na temelju vrijednosti elemenata polja, bilo bi vrlo neracionalno prenositi vrijednosti svih elemenata preko stoga. To bi značilo i zauzeće dodatne memorije, i utrošak dodatnog vremena za provođenje operacija čitanja s jednog mjesta glavne memorije na drugo. Pošto se u općenitom slučaju mogu očekivati poretci s vrlo velikim brojem elemenata, takvo rješenje bi bilo neprihvatljivo. Zbog ovih i sličnih primjera programski jezici uvode koncept prijenosa argumenata po navodu, ili kraće poziv po navodu (engl. call by reference). Ovaj koncept postoji i u jezicima C i C++. Kod ovakvog prijenosa argumenata na stog se ne stavlja vrijednost argumenta nego njegova adresa. Iako bi se to moglo ostvariti prijenosom kazaljki, elegantnije je koristiti navodnički tip (vidi kraj pogl. 7). Za njega vrijedi da prevodilac uvijek referira adresu varijable, dok korisnik koristi uobičajenu sintaksu. Funkciju koja koristi prijenos argumenata arg1, arg2, … tipa type1, type2, … po navodu, za koju smo zbog jednostavnosti odabrali tip void, može se skicirati na sljedeći način:
*
Drugi naziv, uobičajen u računarstvu, potiče od naziva CALL za strojne instrukcija poziva vanjskih potprograma (od engl. call = hrv. poziv).
†
Čak i u slučaju takvih zadaća, funkciji se pridjeljuje neki diskretni tip (npr. int), te se vrijednost funkcije koristi za povrat tzv. «zastavice», tj. vrijednosti koja se koristi kao pokazatelj o ishodu izvršenja funkcije. Prisjetite se funkcije main koja je uobičajeno tipa int, te vraća vrijednost 0 operacijskom sustavu kao zastavicu uspješnog izvršenja programa.
111 void functionExample_CallByReference(type1& arg1, type2& arg2, ... ... ) { // ... ... // Local variables: type1 var1; type2 var2; // ...
...
arg1 = var1; arg2 = var2; // ...
...
}
Ovdje se pretpostavlja da su za tipove type1 i type2 definirani operatori prilagodbe tipa, tj. prilagodbe s tipa typeLocal na type1, odnosno type2. Prilikom poziva ove funkcije, u stvarnoj listi argumenata jednostavno pišemo varijable odgovarajućeg nenavodničkog tipa: // ... ... // Pozivna funkcija, lokalne varijable: type1 var1; type2 var2; // ... ... functionCallByReference(var1, var2, ... ... ); // ... ...
Prilikom realizacije poziva funkcije, vrši se pridruživanje stvarnih argumenata onim formalnim, tj. u našem slučaju se radi o inicijalizaciji navodničkih tipova: type1& arg1 = var1; type2& arg2 = var1;
koja navodu arg1 (arg2) pridjeljuje adresu var1 (var2). Daljnje korištenje navoda arg1 i arg2 u tijelu funkcije functionCallByReference(), efektivno se svodi na izravno mijenjanje var1 i var2 u pozivnoj funkciji. Čitatelj se lako može uvjeriti u gornje izlaganje pažljivim razmatranjem primjera 7.6 i onih koji slijede. Ostvarenje funkcionalnosti prijenosa argumenata po navodu s pomoću kazaljki. Gornju funkcionalnost prijenosa argumenata možemo ostvariti i uporabom kazaljki. Funkcija koja će ostvarivati istu funkcionalnost kao i gornja je sljedećeg oblika: void functionExample_PointerArguments(type1* pArg1, type2* pArg2, ... ... ) { // ... ... // Local variables: type1 var1; type2 var2; // ...
...
pArg1* = var1; pArg2* = var2; // ...
...
}
Kod poziva ove funkcije, u stvarnoj listi argumenata moramo pisati adrese na varijable odgovarajućeg tipa. Ovdje smo to ostvarili uporabom operatora & (adresa od) izravno u listi aktualnih argumenata:
112 // ... ... // Pozivna funkcija, lokalne varijable: type1 var1; type2 var2; // ... ... functionCallByReference( &var1, &var2, ... ... ); // ... ...
Pored za argumente, navodnički tip se može koristiti i za povratnu vrijednost funkcije. Tu vrijedi isto upozorenje kao i za vraćanje pokazivačkog tipa: ne smije se vraćati adresa privremenih (lokalnih) varijabli, već samo onih koje se nalaze u statičkom dijelu memorije ili su kreirane operatorom new i smještene na memorijskoj hrpi. Povrat rezultata funkcije preko navoda je, slično kao i povrat preko kazaljke, znatno učinkovitiji od prenošenja cijelog objekta. Primjer 8.7 Potrebno je ostvariti funkciju sort2() koja uređuje dvije varijable u rastućem (nepadajućem) nizu. a) Promotrimo najprije sljedeću funkciju s pozivom po vrijednosti: // a) Call by Value: void sort2(int i1, int i2) { int iTmp; // Temporary variable if (i1 > i2) // Swap i1 i i2! { iTmp = i1; i1 = i2; i2 = iTmp; } /*
cout << "Ispis unutar funkcije sort2(), 'call by value':\n" << "i1 = " << i1 << "\ti2 = " << i2 << "\n" << endl;
*/ }
Neka je funkcija pozvana iz sljedećeg odsječka programa: // ... ... int iA = 2, iB = 1; sort2(iA, iB); cout << "Ispis iz pozivnog programa:\n" << "iA = " << iA << "\tiB = " << iB << "\n" << endl;
Testirajte funkciju izvođenjem programskog odsječka i odgovorite je li ona izvršila željenu zadaću? Objasnite što se događa? Da se uvjerite gdje je problem, maknite komentare oko ispisa unutar funkcije, i vidite vrijednosti argumenata i1 i i2 unutar funkcije. Jesu li oni ispravno «sortirani»? Je li se zamjena vrijednosti varijabli i1 i i2 unutar funkcije ikako odrazila na varijable iA i iB? Što dakle, moramo napraviti da bi vrijedilo iA <= iB ? b) Očito je da prijenosom po vrijednosti ne možemo utjecati na varijable pozivnog programa. Funkciju preuredimo da prima argumente po navodu:
113 // b) Call by Reference: void sort2(int& i1, int& i2) { int iTmp; // Temporary variable if (i1 > i2) // Swap i1 i i2! { iTmp = i1; i1 = i2; i2 = iTmp; } } // ... ... // Poziv funkcije je oblika: sort2(iA, iB);
Primijetimo da se tijelo funkcije upoće nije mijenjalo, naravno, zbog tome prilagođene sintakse navodničkog tipa. Pozovite novu funkciju s pozivom po navodu iz testnog programa i komentirajte rezultate! Ima li potrebe ispisivati rezultate unutar funkcije? Ponovite na koji način pozvana funkcija utiče na varijable u pozivnoj funkciji? c) Potrebno je ostvarite istu funkcionalnost kao pod (b), ali bez prijenosa po navodu. Objasnite kako je to napravljeno u sljedećem primjeru. Ujedno odgovorite smatraju li se funkcije pod (a), (b) i (c) istim (u kom slučaju bi došlo do konflikta imena, odnosno višestruke definicije), ili različitim? // b) Pointer arguments: void sort2(int* pI1, int* pI2) { int iTmp; // Temporary variable
}
if (*pI1 > *pI2) // Swap integers! { iTmp = *pI1 ; *pI1 = *pI2 ; *pI2 = iTmp ; }
// ... ... // Poziv funkcije je oblika: sort2( &iA, &iB );
Zadatak 8.8 Preuredite gornju funkciju sort2() tako da korisnik može birati način sređivanja u uzlaznom ili silaznom niz. Odaberite prikladnu varijablu s kojom će korisnik to odabirati. Također, neka je funkcija tipa bool koja vraća vrijednost false ako nije trebala zamjena, odnosno true ako je zamjena bila izvršena. Na koncu ponovite zadatak i ostvarite funkcije za sređivanje dvije varijable tipova s pomičnom točkom. Jesu li potrebne velike preinake da se to ostvari? Smiju li sve ove funkciji nositi isto ime? Je li bolje u imenu funkcije naglasiti koji tip varijabli ona sortira, ili je bolje koristi jedno te isto ime? Gdje se u tom slučaju vidi razlika u tipu argumenata? Objasnite. Zadatak 8.9 Iako se sortiranje (sređivanje) proizvoljnog broja podataka vrši općenito njihovom pohranom u strukturu poretka (vidi pogl. 8), radi vježbe preradite zad. 3.4 tako da ostvarite funkcije sort3() koja sređuje vrijednosti tri varijable za tipove int, float i double, u skladu sa zahtjevima pos-
114
tavljenim u prethodnom zadatku. Konstruirajte glavnu funkciju iz koje se vrši unos varijabli, poziva funkcija i potom ispisuju varijable sređene po iznosu. Zadatak 8.10 Unaprijeđena funkcija pow s kontrolom ispravnosti rezultata. Potrebno je unaprijediti funkciju pow za cjelobrojnu potenciju iz zadatka 8.2xx tako da ona preko zastavice iFlag = ±1 dojavljuje da je došlo do prekoračenja opsega brojeva. Ukoliko je rezultat u redu zastavica ostaje na vrijednosti 0. Ukoliko je došlo do prekoračenja, tj. ukoliko je rezultat izvan opsega danog tipa, funkcija vraća maksimalni (pozitivni) cijeli broj ako je rezultat trebao biti pozitivan i postavlja zastavicu iFlag = 1, odnosno minimalni (negativni) broj ako je rezultat trebao biti negativan i zastavicu postavlja na iFlag = –1. Promotrite donje rješenje te: a) Objasnite kako se prenašaju pojedini argumenti funkcije. Smijemo li istovremeno koristiti staru inačicu funkcije s dva argumenta, i novu inačicu s tri, pod istim imenom pow? b) Objasnite detaljno kako je ostvarena kontrola točnosti rezultata, tj. kako je ustanovljeno je li došlo do prekoračenja opsega cijelih brojeva. Ako je došlo do prekoračenja, kako je ustanovljen točan predznak rezultata, iako ovaj, očito ne može biti izračunat. Razmislite je li «računarski skupo» ovo rješenje (obratite pažnju na broj i vrstu operacija koje se stalno ponavljaju) u usporedbi s rješenjem bez kontrole rezultata? c) Napravite glavnu funkciju koja će unositi broj i cjelobrojni eksponent te pozivati funkciju ipow proizvoljan broj puta prema želji korisnika. U slučaju pogrešnog rezultata program će ispisivati da je došlo do greške, odnosno da je rezultat konkretno veći, odnosno manji od ispisanog broja, već prema predznaku rezultata. Npr. za slučaj potencije (–13)9 ispis mora biti: ERROR! n^m = (-13)^9 < -2147483648 ,
a za (13)9 : ERROR! n^m = (13)^9
>
2147483648 .
// Funkcija pow() sa zastavicom ispravnosti rezultata long long int pow(int iX, unsigned short int n, int& iFlag) { const int iMin = -2147483647 – 1, iMax = 2147483647; iFlag = 0; long long int lPw = 1; long long int lPwOld = lPw; for (UINT i = 0; i < n; i++) { lPw *= iX ; if (lPw / iX == lPwOld) lPwOld = lPw; else { if (iX < 0 && (n/2)*2 < n) { iFlag = -1; lPw = iMin; } else { iFlag = 1; lPw = iMax; } break;
115 } }
}
return lPw;
Prijenos poredaka. Kao što je već i natuknuto, poziv po navodu koristi se i u slučaju kad su argumenti funkcije poredci (nizovi). Očito je da bi bilo potpuno neracionalno i neučinkovito sve vrijednosti elemenata poretka kopirati na stog. Zbog toga se u jeziku C/C++ «prijenos» poretka i sintaktički pojednostavljuje, u smislu da se i bez specifikacije navodničkog tipa kod poredaka podrazumijeva poziv po navodu. Tako je u sljedećem odsječku programskog koda objavljena funkcija u kojoj se poredak tipa typeArr, označeno u listi formalnih argumenata praznom uglatom zagradom, pod imenom tArr prenosi po navodu: void function1(typeArr tArr[], unsigned short int uArrDim) { unsigned int i = 0; // ... ... tArr[0] = 1 ; tArr[i] = i + 1; tArr[uArrDim] = n; // ... ...
// 0-th array element // i-th array element // (n - 1)-th array element
} // ... ... const unsigned short int n; typeArr tArr[n]; // ... ... function1(tArr, n); // ... ...
Prevodilac će na stog staviti adresu poretka, tj. adresu početnog elementa. Prevodilac zna da se radi o poretku i poznaje njegov tip, ali nema načina da zna njegovu dimenziju. Ukoliko nam je dimenzija poretka potrebna, što je najčešće slučaj, moramo je prenijeti funkciji. U našem primjeru je za to zadužen formalni argument uArrDim. Prijenosom poretka po navodu omogućeno je u tijelu funkcije uobičajeni način pisanja njegovih elementa, u našem slučaju kao tArr[i] , za i-ti element poretka. Znajući da je identifikatoru imena poretka pridružena vrijednost adrese njegovog početka, očito je da bismo gornji prijenos poredaka mogli ekvivalentno ostvariti na sljedeći način: void function1(typeArr* tArr, unsigned short int uArrDim) { unsigned int i = 0; // ... ... tArr[0] = 1 ; tArr[i] = i + 1; tArr[uArrDim - 1] = n; // ...
...
} // ... ... const unsigned short int n; typeArr tArr[n]; // ... ... function1(tArr, n); // ... ...
// 0-th array element // i-th array element // (n - 1)-th array element
116
Pritom je važno napomenuti da u oba navedena načina prijenosa poredaka kao argumenta, ime poretka funkcija interpretira kao pravu kazaljku. Za njega ne vrijedi zabrana aritmetičkih operacija, kao što je to slučaj u pozivnoj funkciji gdje je poredak deklariran, i gdje ga prevodilac tretira na poseban način. Drugim riječima u oba slučaja prevodilac bi dozvolio u tijelu funkcije functioin1 napisati: unsigned int i = 0; // ... ... tArr[i] = i ; tArr++ ; // ...
// i-th array element // WARNING: DON’T DO THAT!!! // tArr[i] = (i + 1)-th element!!!
...
Jasno je da nakon bilo kakve aritmetičke promjene pokazivača početka poretka dolazi do poremećaja pri radu s indeksima. Dakle, ako se elementima poretka pristupa preko indekasa, aritmetika s pokazivačem početka poretka se mora smatrati zabranjenom. Ona se može dopustiti jedino ako se elementima poretka nikad ne pristupa preko indeksa, te ako se strogo vodi računa o hodu pokazivača kroz poredak. Argumenti glavne funkcije main: retType main( int* argc; char* argv[] ). (Xxx dodati, dovršiti…) Primjer 8.11 Potrebno je napisati funkciju tipa float koja vraća srednju vrijednost niza od n brojeva istog tipa (vidi rješenje). Na sličan način ostvarite i funkciju za standardnu devijaciju standDev(), pazeći da se ne ponavlja već odrađeni izračun. Zatim ponovite zadatke 6.1xx i 6.2xx rabeći ove funkcije. // Funkcija za izračun srednje vrijednosti: float meanValue(float fX[], unsigned int n) { float fS = 0.f; for(float *pF = fX; pF < fX + n ; pF++) fS += *pF; }
return fS / n;
Zadatak 8.12* Da provjerite jeste li u potpunosti razumjeli pokazivački i navodnički tip, te mehanizam prijenosa argumenata funkcija, razmotrite kako je pozvan poredak u ovom primjeru. Iako su prije prikazani načini poziva poredaka jednostavniji, razumijevanje donjeg koda je dobra vježba. a) Kako je prenesen formalni argument s1, a kako n? b) Prilikom pridruživanja short int* pS = &s1 koja je adresa pridružena kazaljci pS? Ona na kojoj je pohranjena referenca s1 ili ona na koju se s1 odnosi? c) Preinačite funkciju i njezin poziv tako da se poredak poziva na prethodna dva načina, tj. i) navođenjem imena i praznih uglatih zagrada, te ii) preko kazaljki. Usporedite! #include #include void strangeArrayCallByRef(short int& s1, unsigned short int n) { short int* pS = &s1; // Kuda pokazuje pS? for (UINT i = 0; i < n; i++) if (i < 0x8 || i > 0xf7) cout << "S["<< i << "] = " << pS[i] << "\n";
117 } int main() { const unsigned short int cN = 0x100; short int sX[cN]; // Inicijalizacija elementa sX[i] na vrijednost indeksa i: for (UINT i = 0; i < cN; i++) sX[i] = i; // Poziv funkcije: strangeArrayCallByRef(sX[0], cN); short int *pS1, *pS2; pS1 = &sX[0]; pS2 = &sX[cN-1];
// Provjera adresa
// Adresa početnog elementa // Adresa zadnjeg elementa
// Raspon adresa za poredak sX[0x100] cout << "\n" << "pS1 = &sX[ 0] = " << pS1 << "\n" << "pS2 = &sX[cN-1] = " << pS2 << "\n" << endl; return 0;
} Naputak o pokazivačkom i navodničkom tipu funkcije. Funkcije mogu biti pokazivačkog ili navodničkog tipa, odnosno vraćati vrijednosti tih tipova. To je puno učinkovitiji prijenos rezultata od prijenosa samih složenih podatkovnih tipova, struktura (definiranih kao C-struct vidi pogl. 9), odnosno C++ objekata. Naravno, to je potrebno samo onda kad se taj rezultat ne pojavljuje kao argument funkcije, u kom slučaju će on biti poznat pozivnoj funkciji preko navoda, ili preko kazaljki. U oba slučaja, i kad je rezultat funkcije kazaljka i navod, treba voditi računa da se ne vraća vrijednost adrese lokalnih, odnosno automatskih varijabli, koje nestaju sa stoga s izvršenjem funkcije. Prevodilac će upozoriti na svaki takav slučaj, ali ga neće označiti kao grešku. Navod kao rezultat funkcije se rabi uglavnom pri realizaciji operatora (preopterećenje operatora, engl. operators' overload) zbog svoje sintaktičke osobitosti. Pokazivački tip funkcije koristan je u nizu slučajeva rada s dinamičkim strukturama, a može poslužiti i kad je potrebno vratiti ime neke funkcije koje se interpretira kao kazaljka na adresu gdje je ta funkcija pohranjena. Za detalje je potrebno proučiti MSDN knjižnicu pojmova, a kao primjer proučite tip funkcije getline, te operatora operator<< i operator>> za klase basic_ostream i basic_istream. Jednostavno unesite ova imena kao ključne riječi u indeksu izbornika pomoć i pokušajte obrazložiti objašnjenja namijenjena profesionalnim C/C++ programerima.
Globalne i lokalne varijable Prilikom razmatranja blokova tvrdnji (vidi odjeljak 3.2), već je istaknuto da varijabla ima dogled (engl. scope) u njoj unutarnje blokove — dakle u blokove koji se nalaze unutar bloka u kojem je ta varijabla deklarirana. To vrijedi ukoliko u tim unutarnjim blokovima nije deklarirana lokalna varijabla s istim imenom. Za razliku od toga, izlaskom iz bloka u kojem je varijabla deklarirana, dakle u sebi vanjskom bloku, varijabla ne postoji, odnosno nema doseg. To je u skladu s idejom da se izlaskom iz bloka briše cijeli sadržaj na stogu koji njemu odgovara, uključujući i sve lokalne varijable.
118
Globalne varijable. Specifično, varijable koje su deklarirane izvan funkcija, nazivaju se globalne varijable (engl. global variables). U skladu s gornjim načelom, one imaju dogled unutar svih funkcija definiranih u istoj datoteci, odnosno unutar svih programa koji uključuju odgovarajuću zaglavnu datoteku direktivom #include. Koristimo li globalnu varijablu unutar neke funkcije, tu je deklariramo pridjevom extern. Tako na eksplicitan način izražavamo da se radi o istoj varijabli koja je negdje objavljena kao globalna. Međutim, ukoliko je varijabla objavljena u izvornoj programskog datoteci prije nego što je rabimo unutar funkcija, ili je datoteka objave globalne varijable uključena direktivom #include, tada se extern može ispustiti (vidi prim. 8.12). Zato je uobičajeno da se globalne varijable objave na početku datoteke čime se izbjegava potreba za njihovom ponovnom objavom unutar funkcija. Često se, radi preglednosti, globalne varijable deklariraju u zaglavnim (.h) datotekama zajedno s prototipovima funkcija. U tom slučaju se njena uporaba u odgovarajućoj izvornoj (.cpp) datoteci mora objaviti pridjevom extern. Memorijska područja za pohranu C/C++ varijabli. Globalne varijable smještaju se u posebno, tzv. statično memorijsko područje (engl. static data area), jednako kao i lokalne varijable koje su objavljene ključnom riječi static. Uz dosada spomenuti izvedbeni stog za lokalne varijable, i hrpe slobodne memorije za dinamičke varijable, rezimiramo tri memorijska područja u koja jezik C/C++ posprema varijable: 1. Izvedbeni stog (engl. run-time stack) za lokalne ili automatske varijable — područje memorije u okviru sustavskog stoga. Automatske varijable nestaju izlaskom iz bloka (funkcije) u kojem su deklarirane. 2. Statičko područje (engl. static data area) za globalne varijable (i lokalne objavljene sa static) — područje u okolini izvršnog koda programa. Varijable ostaju «statične», tj. ne nestaju izlaskom iz bloka. 3. Hrpa ili slobodna memorija (engl. heap, free store), područje slobodne memorije pod kontrolom memorijskog upravljača (engl. memory manager) za pohranu dinamički kreiranih varijabli, poredaka, struktura i općenito C++ objekata. Obavezno je uklanjanje ovakvih objekata operatorom delete. Već spomenuto načelo skrivanja informacije očito je u nesuglasju s postojanjem globalnih varijabli. Stoga njihovu uporabu treba svesti na najmanju moguću mjeru, odnosnu dopustiti ih u nekoliko iznimnih slučajeva. Npr. varijable deklarirane ključnom riječi const kao konstante koje koriste mnoge funkcije programa možemo opravdati. U nekim slučajevima mogla bi biti opravdana globalna definicija često korištenog pobrojanog tipa i pripadnih varijabli. Također, iznimno bi se djelovanje većeg broja funkcija nad istom varijablom moglo opravdati deklaracijom te varijable kao globalne. Međutim, ponovo vrijedi da se i u upravo spomenutim, a i u svim ostalim prilikama uvođenja globalnih varijabli najprije pokuša zamijeniti ih lokalnim varijablama i odgovarajućim prijenosom argumenata između funkcija. Globalne varijable sprječavaju ponovnu uporabljivost programskog koda, a funkcije koje ih koriste manje općenitima. Pogotovo se postojanje globalnih varijabli kosi s načelima objektnog programiranja u C++, gdje gotovo da ne postoji opravdanje za njihovu uporabu. Jednostavan primjer koji slijedi ilustrira dogled globalnih varijabli (usporedite s prim. 3.1), a ujedno i pokazuje neopravdanost njihove uporabe. Primjer 8.13 Globalne i lokalne varijable. Odgovorite na sljedeća pitanja: a) Objasnite koje su varijable globalne, a koje lokalne, posebno za funkciju ispisVar, te za glavnu funkciju main(). b) Kakva je varijabla deklarirano lokalno koja ima isto ime kao i varijabla deklarirana globalno? c) Možemo li mijenjati globalnu varijablu deklariranu s const? Koja je prednost takve objave?
119
d) Objasnite redom sve ispise. e) Kako je moguće da se u drugi ispis iz funkcije ispisVar() promijenio u odnosu na prvi, iako ta funkcija ništa ne čini s varijablama? Izaziva li to pomutnju i nepreglednost? e) Što općenito zaključujete o korištenju globalnih varijabli koje nisu konstante? Razmotrite također sljedeći zadatak. #include // Globalne varijable – opravdane!? const float fPi = 3.141593f; const int iMin = -2147483647 - 1, iMax = 2147483647; enum ourFunctions{funcMain, funcInputVar, funcOutputVar}; // Globalne varijable – jesu li opravdane! unsigned int i1 = 1; // iCnt = 0; // Globalni brojač float f1 = 1.111111f; // ourFunctions func; // Globalna idefnitifikacijska varijabla // Korištenje globalnih varijabli u funkciji void outputVar() { // Zahtijeva globalne konstante: fPi, iMin, iMax: extern const float fPi; // Može se ispustiti ako je glob. var. deextern const int iMin, iMax; // klarirana u istoj izvornoj (.cpp) dato// teci prije njene uporabe unutar fukcija // Mijenja globalne varijable: iCnt, currentFunction, // Redeklaracija (“prekrivanje”) globalnih varijabli lokalnima: int i1 = 111; // redefinicija globalne varijable, dojava stanja: currentFunction = funcOutputVar; // Inkrementiranje globalne varijable iz više izvora, IZBJEGAVATI! iCnt++; // redefinicija globalnih varijabli, PROBLEMATIČNO!!! f1 *= 10.f; cout << "Ispis #" << iCnt << " iz funkcije: " << currentFunction << " [0 = main(), 2 = outVar()]\n" << "==========================================================\n" << "Pi = " << fPi << "\tiMin = " << iMin << "\tiMax = " << iMax << "\n" << "f1 = " << f1 << "\t i1 = " << i1 << "\tiCnt = " << iCnt << "\n" << endl; } int main() { // redeklaracija (“prekrivanje”) globalnih varijabli: int i1 = 22; // redefinicija globalne varijable, dojava stanja: currentFunction = funcMain; // Inkrementiranje globalne varijable iz više izvora, IZBJEGAVATI! iCnt++;
120 cout << "Ispis #" << iCnt << " iz funkcije: " << currentFunction << " [0 = main(), 2 = outVar()]\n" << "==========================================================\n" << "Pi = " << fPi << "\tiMin = " << iMin << "\tiMax = " << iMax << "\n" << "f1 = " << f1 << "\t i1 = " << i1 << "\tiCnt = " << iCnt << "\n" << endl; outputVar();
// Ispis preko funkcije outputVar
// redefinicija globalne varijable, dojava stanja: currentFunction = funcMain; // Inkrementiranje globalne varijable iz više izvora, IZBJEGAVATI! iCnt++; cout << "Ispis #" << iCnt << " iz funkcije: " << currentFunction << " [0 = main(), 2 = outVar()]\n" << "==========================================================\n" << "Pi = " << fPi << "\tiMin = " << iMin << "\tiMax = " << iMax << "\n" << "f1 = " << f1 << "\t i1 = " << i1 << "\tiCnt = " << iCnt << "\n" << endl; outputVar();
// Ispis preko funkcije outputVar
return 0; }
Uporaba globalnih varijabli u jeziku C. Prethodni primjer sugerira moguću «opravdanu» uporabu globalnih varijabli u kontekstu funkcionalnog, odnosno proceduralnog programiranja. Uporaba globalnih konstanti je očita — sve funkcije imaju pristup veličinama koje se ne mogu mijenjati. Uporaba globalnih varijabli koje nisu konstante može naći opravdanje u dva slučaja iz gornjeg primjera: i) Promjenom sadržaja globalne varijable možemo ostaviti trag o tome koja se zadnja funkcija izvršavala. U našem slučaju to smo ostvarili globalnom varijablom currentFunction pobrojanog tipa ourFunctions. ii) Inkrementacijom globalne varijable iCnt iz različitih izvora ostvarili smo pobrojavanje svih ispisa. Izbjegavanje globalnih varijabli. Iako prethodni zadatak nalazi opravdanje za uporabu globalnih varijabli, potrebno je uočiti sljedeće probleme koji nastaju njihovom uporabom: 1. Nezavisnost i samodostatnost funkcija je izgubljena. U našem primjeru funkcija outputVar redefinira i inkrementira globalne varijable. Prilikom njene ponovne uporabe na drugom mjestu, te varijable moraju biti uključene u kompilaciju. Pošto to nije jasno iz prototipa funkcije, programer to mora posebno navoditi u komentarima. Jednako, mora se objašnjavati što predstavljaju rabljene globalne varijable. Sve to zajedno smanjena ponovna uporabljivost ove funkcije. 2. Mogućnošću redefinicije globalnih varijabli umanjuje se jasnoća i čitljivost koda. Programer mora voditi računa o radu ne samo funkcije koju piše nego i o njenom odnosu s drugim funkcijama, što je protivno načelu skrivanja informacije. 3. Bitno se otežava korekcija i uvođenje promjena u softveru. Pretpostavimo da je umjesto inkrementacije varijable iCnt u gornjem primjeru potrebno napraviti neku drugu radnju nad njom. To bi
121
iziskivalo promjene u svim funkcijama koje koriste tu globalnu varijablu – a pošto su globalne varijable po definiciji dostupne svim funkcijama unutar iste datoteke, to znači pregled svih takvih funkcija. U složenom programiranju koje koristi na stotine funkcija, to je ozbiljan problem. Stoga se to, pogotovo u okviru objektno orijentiranog programiranja, rješava na drugi način, sugeriran u sljedećem odlomku. Globalne varijable i objektno orijentirano programiranje. Dosljednim provođenjem objektno orijentirane paradigme u jeziku C++ (koja se obrazlaže u kasnijim kolegijima programiranja) uvelike se smanjuje potreba za globalnim varijablama, pogotovo onima koje nisu deklarirane kao konstantne veličine. Promjena «zajedničkih varijabli» se ostvaruje s pomoću funkcija koje pribavljaju (tzv. pristupnici, engl. accessors), ili mijenjaju (modifikatori, engl. modifiers) varijable definirane unutar neke klase, ili . Iako uvođenje posebne funkcije predstavlja dodatnog «posrednika», time se upravo postiže gore spomenuta neovisnost i sprječava potreba da više funkcija poznaje zajedničke varijable. One «poznaju» samo ime modifikatora kojom će biti ostvarena neka radnja, kao npr. gore spomenuta dojava stanja, ili inkrementacija zajedničkog brojača. Ukoliko se detalji radnje ili struktura memorijskih varijabli treba promijeniti, to se naravno ne odražava u imenu pristupnika i modifikatora. Ovo je dobra ilustracija nekoliko ideja koje su dovele do uvođenja objektno orijentiranog programiranja i jezika C++. Rijedak primjer opravdanosti globalnih varijabli su objekti cin i cout iz klasa istream, odnosno ostream, za koje želimo da budu dostupni u svim funkcijama. Provjerite kako su deklarirani ovi objekti u datotekama istream.h i ostream.h.* Zadatak 8.14 U duhu načela objektnog programiranja, modificirajte gornji primjer s globalnim varijablama tako da sve one globalne varijable koje nisu konstante strpate u jednu funkciju odgovarajućeg imena (npr. globalInterface), te da uz pomoć dodatnih jednostavnih funkcija ostvarite radnje s kojim se dojavljuje stanje ili redefiniraju dotadašnje globalne varijable.
*
Ove datoteke možete pronaći u mapi (direktoriju) Include unutar mape C/C++ programskog alata kojeg koristite. Uobičajena staza na Win32 platformi je: C:\Program Files\Microsoft Visual Studio\VC98\Include. Otvorite datoteke u programskom alatu i proanalizirajte ih.
122
Poglavlje 9.
Znakovni poredci i C-nizovi. Primjer uporabe funkcija
Obrada tekstualnih datoteka vrlo je čest programski zadatak. Izvorne verzije naših programa prevodilac obrađuje kao tekstualne datoteke, raščlanjujući pojedine njihove dijelove na tvrdnje i izraze, da bi ih potom mogao zamijeniti odgovarajućim skupovima strojnih instrukcija. Također, kroz povijest računarstva svjedoci smo kako je sve veći udio podataka koje obrađujemo nenumeričke naravi. Sve to upućuje na važnost poznavanja pohrane i obrada tekstualnih podataka. Usput ćemo ilustrirati primjenu nekih od velikog broja funkcija za rad s njima iz standardnih C i C++ biblioteka funkcija. Znakovni poredak. Znakovni poredak, polje ili niz (engl. character array) predstavlja jednodimenzionalni poredak s elementima tipa char. Znakovni poredak s m elemenata objavljujemo na uobičajeni način: char cArr[m] ;
Sukladno općem pojmu poretka, znakovni je poredak određen svojom baznom adresom, odnosno adresom početnog elementa koju predstavlja njegovo ime, te brojem elemenata u poretku. Znakovni poredak može u sebi sadržavati bilo koje vrijednosti svojih elemenata, odnosno bajtova. Drugim riječima, osim onih ASCII znakova 0-te i 1.ve stranice koji se mogu ispisati, tu mogu biti i kontrolni znakovi, odnosno bilo koja 8-bitna cjelobrojna vrijednost. C-niz. Za razliku od uobičajenih poredaka određenih adresom početnog elementa i ukupnim brojem elemenata, među koje spadaju i poredci znakova, prilikom rada s tekstom promjenjive duljine prikladno je uvesti drugi način definicije. Tako ćemo standardni niz znakova definirati s pomoću: bazne adrese i posebne oznake za kraj niza. Definicija. Standardni C-niz znakova, ili kratko C-niz (engl. C-string) je niz ASCII znakova koji je okončan znakom null = '\0', kodne zamjene: ASCII(null) = 00h = 0000 0000b . Dakle radi se osmorci bitova u kojoj su svi na vrijednosti 0. Otuda i naziv: nulom okončan niz znakova (engl. null terminated character string). C-niz i znakovn poredci. Svaki C-niz je pohranjen u nekom znakovnom poretku. Iz gore izloženog pak je jasno da znakovni poredak ne mora predstavljati valjani C-niz. To je slučaj kad u poretku ne postoji niti jedan znak null. Želimo li tekst od n znakova ( n ≥ 0 ), pohraniti u znakovno polje veličine m, tada očito mora vrijediti:
m ≥ n +1 , tj. u poretku mora biti najmanje jedan element više za pohranu znaka null. Drugim riječima, svaki znakovni poredak s barem jednim znakom null predstavlja standardni Cniz računajući od početka polja do uključivo prvog takvog znaka. Dakle, u C-nizu može postojati samo jedan znak null — prvi koji se pojavljuje u poretku, i taj ujedno predstavlja njegov kraj. Znak null. U skladu sa svojim imenom*, znak null u tipu char nema nikakvu vrijednost, pa niti vrijednost znamenke 0. Nula je samo njegova kodna zamjena ili kodna riječ, odnosno sadržaj bajta s kojim je taj znak prikazan u ASCII kodu (vidjeti dodatak B). Prilikom ispisa znaka null na ekranu se pojavljuje prazno mjesto. Tako će npr. rezultat sljedeće linije koda: *
Engl. null = ništavan, nevažeći, nepostojeći.
123 cout << 'a' << 'b' << '\0' << 'c' << (char) 0 << 'd';
biti: ab c d
Međutim, izvršimo li pridruživanje ovih istih znakova elementima znakovnog poretka, te potom taj poredak pošaljemo na ispis preko objekta cout kao u sljedećem dijelu koda: const UINT uN = 10; char cA[uN] = {0, };
// svi znakovi = null = 00h
cA[0] = 'a'; cA[1] = 'b'; cA[2] = '\0'; cA[3] = 'c'; cA[4] = 0; cA[5] = 'd'; cout << cA;
ispisat će se samo znakovi prije prvog znaka null: ab
Dakle, pisanjem samo imena poretka (u našem slučaju cA), koje ujedno odgovara i imenu C-niza, operator kopiranja << ispisuje na ekranu cijeli C-niz. Na isti način, tj. kao C-niz, interpretirat će imena znakovnih poredaka i ostale funkcije za rad s C-nizovima (vidi niže). Ako pak u znakovnom poretku nema niti jednog znaka null, ispisat će se svi njegovi znakovi, te potom nastaviti ispis sljedećih bajtova memorije interpretiranih kao tip char, sve dok se ne naiđe na prvi bajt vrijednosti 00h (vidi Primjer 9.1). Ispišemo li pak isti poredak znak po znak, bit će ispisano svih njegovih 10 elemenata. Da bismo ih mogli izbrojiti ispisat ćemo najprije redak brojeva od 0 do 9. Pošto će se znakovi null ispisati kao prazna mjesta, da bismo na ekranu vidjeli koliko je znakova ukupno ispisano, po ispisu elemenata poretka stavit ćemo znak '/' : for(UINT i = 0; i < 2*uN; i++) cout << i%uN; cout << '\n' << endl; for(i = 0; i < uN; i++) cout << cA[i]; cout << '/' << '\n' << endl;
Ispis gornjeg odsječka će biti: 01234567890123456789 ab c d /
C-niz i tekstualni podaci. Nulom okončani niz znakova namijenjen je za pohranu tekstualnih podataka. Za rad s C-nizovima postoji niz standardnih funkcija, čemu je prilagođen i način inicijalizacije C nizova. Npr. sljedeća deklaracija i pridruživanje niza od n – 1 ASCII znakova unutar znakova dvostrukih navodnika: char cString[ ] = "n ASCII characters." ;
// Niz od n = 19 proizvoljnih ASCII znakova.
definira znakovni poredak od n + 1 elemenata u kojem je posljednji znak: cString[n] = ‘\0’ . U konkretnom slučaju, niz sadrži 19 znakova, pa će poredak imati 20 elemenata veličine 1B, te zauzeti ukupno 20B. Ovakva specifikacija standardnog znakovnog niza unutar programa je i najpraktičnija jer programer ne mora voditi brigu o okončanju niza znakom ‘\0’. Dodatna je pogodost da ukoliko se u gornjoj deklaraciji navede dimenzija poretka i ona je premalena za ispisani niz, tj. manja od broja znakova između navodnika uvećanog za 1, prevodilac će sâm dojaviti grešku.
124
Međutim, ako niz definiramo navođenjem ASCII znakova unutar vitičaste zagrade (vidi Primjer 9.1), možemo definirati niz znakova koji jest znakovni poredak, ali nije standardni C niz okončan znakom null. Na takav poredak ne možemo primjenjivati standardne C funkcije za obradu nizova. Kao što je već ilustrirano, izlazni objekt cout prihvaća C-nizove preko operatora kopiranja << i ispisuje sve znakove do znaka null na ekranu. Za unos ćemo koristiti funkciju getline() koja djeluje na ulazni objekt cin. Inače, za kopiranje jednog C niza u drugi, moramo izvršiti pridruživanje kao kod poretka, element po element. Funkcija strlen() daje duljinu C niza, kao broj znakova od nultog elementa poretka do znaka ‘\0’, ne ubrajajući znak null. Ove, i druge funkcije za rad sa standardnim C nizovima, objavljene su u zaglavnoj datoteci string.h koju je potrebno uključiti u program. Sintaksa i način primjene uočljivi su iz sljedećeg primjera. Primjer 9.1 Inicijalizacija C znakovnih nizova. Pažljivo promotrite sljedeće definicije nizova. Obratite pažnju na pitanja u komentarima i pokušajte odgovoriti na njih. Posebice obratite pažnju na retke s višestrukim upitnicima. Hoće li oni proći kompilaciju? Objasnite svoje odgovore. Potom testirajte programski odsječak, po potrebi otklonite greške, provjerite ispis i usporedite ga sa svojim odgovorima. Podsjetite se što daje operator sizeof, duljinu poretka ili duljinu C niza? Pogotovo obratite pažnju na to kako je ispisan znakovni poredak cArr. // Uobičajena inicijalizacija unutar dvostrukih navodnika: char cStr0[] = "C++ program" ; char cStr1[5] = "abcd"; char cStr2[5] = "abc";
// sizeof cStr0 = ? // C niz duljine 11 u poretku duljine 12
// cStr1[4] = ? // cStr2[3] = ?, cStr2[4] = ? // C niz duljine 3 u poretku duljine 5
char cStr3[5] = "12345";
// ???
// Inicijalizacija kao za poredak (po elementima): char cStr4[] = {'a', 'b', 'c', 'd', '\0'}; // sizeof cStr0 = ? char cStr5[5] = {'a', 'b', 'c', 'd', }; // cStr5[4] = ? char cStr6[5] = {'e', 'f', 'g', }; // cStr6[3] = ?, cStr6[4] = ? // Je li cArr legalan C niz? char cArr[5] = {'1', '2', '3', '4', '5'}; // Ispis: cout << "cStr0 << "cStr1 << "cStr2 << "cStr3 << endl;
= = = =
" " " "
<< << << <<
cout << << << << << << <<
"cStr1[3]= "cStr1[4]= "cStr2[3]= "cStr2[4]= "cStr3[3]= "cStr3[4]= endl;
" " " " " "
cout << << << << <<
"cStr4 "cStr5 "cStr6 "cArr endl;
<< << << <<
= = = =
" " " "
cStr0 cStr1 cStr2 cStr3
<< << << << << <<
<< << << <<
"\t "\t "\t "\t
cStr1[3] cStr1[4] cStr2[3] cStr2[4] cStr3[3] cStr3[4]
cStr4 cStr5 cStr6 cArr
<< << << <<
sizeof sizeof sizeof sizeof
<< << << << << <<
"\t "\t "\t "\t
"\t "\t "\t "\t "\t "\t
// ? ? ? cStr0 cStr1 cStr2 cStr3
= = = =
" " " "
= = = = = =
" " " " " "
<< << << << << <<
cStr4 cStr5 cStr6 cArr
= = = =
" " " "
ASCII ASCII ASCII ASCII ASCII ASCII
sizeof sizeof sizeof sizeof
<< << << <<
sizeof sizeof sizeof sizeof
(int) (int) (int) (int) (int) (int)
<< << << <<
cStr0 cStr1 cStr2 cStr3
<< << << <<
"\n" "\n" "\n" "\n"
cStr1[3] cStr1[4] cStr2[3] cStr2[4] cStr3[3] cStr3[4]
<< << << << << <<
"\n" "\n" "\n" "\n" "\n" "\n"
sizeof sizeof sizeof sizeof
cStr4 << "\n" cStr5 << "\n" cStr6 << "\n" cArr << "\n"
125 cout << << << << <<
"cStr5[3]= "cStr5[4]= "cStr6[3]= "cStr6[4]= endl;
" " " "
<< << << <<
cStr5[3] cStr5[4] cStr6[3] cStr6[3]
<< << << <<
"\t "\t "\t "\t
ASCII ASCII ASCII ASCII
= = = =
" " " "
<< << << <<
(int)cStr5[3] (int)cStr5[4] (int)cStr6[3] (int)cStr6[4]
<< << << <<
"\n" "\n" "\n" "\n"
Zadatak 9.2 Da utvrdite znanje o standardnim C nizovima, ustanovite koja je duljina svakog od 8 nizova (cStr0 do cStr6 , i cArr) iz primjera 9.1 uporabom funkcije strlen(). Razlikuje li se duljlina C-niza od broja elemenata znakovnog poretka? Što je sa znakovnim poretkom cArr ? Jeste li odgonetnuli njegov ispis? Razmislite odakle niz "efg" na njegovom kraju? Zatim s pomoću funkcije strlen() provjerite svoje odgovore. Primjer ispisa dan je u sljedećim linijama programskog koda: // Broj znakova // Ispis: cout << "cStr0 = << "cStr1 = << "cStr2 = << "cStr3 = << endl; cout << "cStr0 = << "cStr1 = << "cStr2 = << "cStr3 = << endl;
C niza " " " "
<< << << <<
cStr0 cStr1 cStr2 cStr3
<< << << <<
"\tstrlen(cStr0) "\tstrlen(cStr1) "\tstrlen(cStr2) "\tstrlen(cStr3)
= = = =
" " " "
<< << << <<
strlen(cStr0)<< strlen(cStr1)<< strlen(cStr2)<< strlen(cStr3)<<
"\n" "\n" "\n" "\n"
" " " "
<< << << <<
cStr4 cStr5 cStr6 cArr
<< << << <<
"\tstrlen(cStr4) "\tstrlen(cStr5) "\tstrlen(cStr6) "\tstrlen(cArr)
= = = =
" " " "
<< << << <<
strlen(cStr4)<< strlen(cStr5)<< strlen(cStr6)<< strlen(cArr) <<
"\n" "\n" "\n" "\n"
Funkcije za unos i ispis znakova: getchar i putchar. Funkcije getchar i putchar predstavljaju proceduralnu inačicu za unos i ispis znakova iz standardnog ulaznog ( stdin ) odnosno izlaznog ( stdout ) toka ili struje u jeziku C. Naravno, pošto je C kao «podskup» uključen u jezik C++, možemo ih koristiti i u C++. Tako funkcija getchar unosi po jedan ASCII znak iz standardnog ulaznog toka stdin, a putchar prenosi po jedan znak na standardni izlazni tok stdout. Na našoj platformi su ulazni i izlazni tokovi tipa FILE (za detalje vidjeti MSDN knjižnicu), a objavljeni su zajedno sa spomenutim funkcijama u zaglavnoj datoteci stdio.h, koju je potrebno uključiti u datoteku koju kompiliramo. Primjer 9.3 Potrebno je napisati program koji sa tastature učitava jednu liniju znakova u znakovni poredak kao standardni C-niz uz pomoć funkcija getchar za učitavanje ASCII znaka i putchar za ispis. Standardna linija sadrži maksimalno 80 znakova, što je jednako standardnom broju znakova jednog retka na konzolnom prikazniku, odnosno na matričnom printeru. Alternativno, korisnik može okončati liniju i kontrolnim znakom '\n' = new line. Dodatni zadaci: a) Podsjetite se --- kako mora biti definirana varijabla da bi se mogla pojaviti u tvrdnji koja određuje dimenziju statičkog poretka? Smije li se dimenzija poretka definirati aritmetičkim izrazom kao što je učinjeno dolje: char cLine[cN + 1]? Zašto je za poredak uzeta dimenzija cN + 1, a ne cN? b) Obratite pažnju na značenje konstante cN te na uvjet u prvoj while petlji. Zašto je nužan drugi uvjet u prvoj while petlji? c) Zamijenite while petlje s odgovarajućim for petljama. d) Testirajte program i uočite pogrešan ispis. Pronađite logičku grešku (engl. bug) koja je tome uzrok! Što treba promijeniti u uvjetu druge while petlje da bi ona korektno ispisivala standardni C-niz? e) Je li nužno da ova funkcija upisuje i ispisuje samo jedan redak teksta, tj. maksimalno 80 znakova? Promijenite konstantu cN na 100 znakova i upišite točno 100 znakova, tako da redom upisujete znamenke: 12345678901234567890… Prva nula u nizu označava 10-ti znak, druga nula 20-ti, itd… // ... prethodna uključenja ... #include
126 // Unos teksta preko getchar(), ispis preko putchar(char c): int main() { const unsigned int cN = 80; // Broj znakova u liniji char c, cL[cN + 1]; // Znakovni poredak za jednu liniju teksta cout << "Unos reda proizvoljnih znakova (za kraj 'return' ili 'enter'):\n" << "================================================================" << endl; unsigned int i = 0; while( ((c = getchar()) != '\n') && (i < cN) ) cL[i++] = c; cL[i] = '\0'; putchar('\n');
// Jedan red razmaka
cout << "Ispis reda teksta, kao C-niza iz znakovnog poretka cLine[81]:\n" << "================================================================" << endl; i= 0; while( (c = cL[i++]) != '\n' ) putchar(c);
}
putchar('\n'); putchar('\n');
// Ispis do kraja C-niza(!?!)
// Novi red // Jedan red razmaka
Oznaka kraja datoteke (EOF). Kao što je istaknuto u prethodnom poglavlju, jedno je od osnovnih načela programiranja postizanje čim veće općenitosti napisanog programskog koda, odnosno njegove primjenjivosti u čim većem broju slučajeva. Stoga nastojimo da napisane programske cjeline koje rješavaju općenite zadatke organiziramo u funkcije koje ćemo moći ponovno rabiti. Želimo li napisati gornju while petlju da bude općenitija, te da je prekida i oznaka kraja datoteke EOF (engl. End Of File) modificirat ćemo uvjet petlje na sljedeći način: while( ((c = getchar() ) != EOF) && ( c!= '\n') && (i < cN) ) cLine[i++] = c;
Na sličan način radi i funkcija getline koja će biti uporabljena u primjeru niže. Znak EOF ima vrijednost (kodnu riječ) ekvivalentnu broju −1 za predznačeni cjelobrojni tip iste duljine kao i zadani znakovni tip. Za ASCII kôd će to odgovarati kodnoj zamjeni FFh koja se na ekranu ispisuje kao prazno mjesto (vidi tablicu ASCII koda u dodatku B). Znak EOF nema ekvivalentnog znaka na tastaturi kojim bismo ga mogli unijeti izravno. Za operacijski sustav DOS i Windows tipka Ctrl se rabi za dobivanje alternativnih znakova, i EOF se bi trebao dobiti kao kombinacija Ctrl+Z (na sustavu Unix kao ^D). Međutim uz uobičajene postavke sustava Windows za hrvatski jezik i hrvatsku inačicu tastature izgleda da to nije moguće unijeti preko konzolne aplikacije. Dobra i zanimljiva vježba je provjeriti što se postiže uporabom tipke Ctrl u kombinaciji s drugim znakovima tastature. Npr. znak za novi red se osim pritiskom na tipku Return, postiže i s pomoću Ctrl+M, funkcija brisanja znakova unatrag s pomoću Ctrl+H, i sl. Zadatak 9.4 Kako ćete provjeriti da je znak EOF ekvivalentan kodnoj zamjeni s vrijednosti –1 ? Naputak. Provjerite npr. istinitost logičkog izraza EOF == -1, te EOF == 0xff (FFh = −1 u 8bitnoj ili aritmetici modulo 256) i ispišite odgovarajuće poruke. Ili umetnite vrijednost EOF u varijablu tipa char , te je ispišite kao znak, a potom i kao cijeli broj.
127
Graničnici. Graničnici ili delimiteri (engl. delimiter) su općenito znakovi koji služe za odvajanje dijelova znakovnog niza. Tako npr. dva dvostruka navodnika " u sljedećoj tvrdnji: char cText[] = "Ovo je C-niz. " ;
služe kao graničnici koji C / C++ prevodiocu odjeljuju C-niz od ostalog dijela tvrdnje. Već smo upoznali znak null kao oznaku kraja standardnog C-niza, te znak EOF kao delimiter kraja datoteke. Općenito, u širem smislu možemo reći da su graničnici svi oni znakovi koji mogu poslužiti odjeljivanju dijelova teksta. Tako npr. operator kopiranja >> na objekt cin kao graničnike podrazumijeva znakove koji u sebi uključuju razmake, tj. jednostruko prazno mjesto '\ ', i tabulator '\t' (višestruko prazno mjesto). Tu spadaju i znak za novi red '\n' i znak null '\0'. S druge strane, logično je da operator ispisa << preko objekta cin uvažava sve ove navedene znakove razmaka, kao i ostale kontrolne znakove za kontrolu ispisa na normalan, očekivani način, kao što nam je već dobro poznato iz dosadašnjeg izlaganja (vidi detalje u tablici B.3). Njegov jedini «delimiter» će biti upravo standardna oznaka kraja C-niza, tj. znak null. Nadalje, unutar tekstualnih datoteka, delimiteri mogu biti znakovi interpunkcije, npr. zarez, točkazarez, i sl. Primijetimo da znakove '\t' i '\n' možemo unijeti izravno iz tastature, dok znak null ne možemo. Znak '\t' se unosi pritiskom na tipku tabulator (TAB ili sl. oznaka), a znak '\n' s pomoću tipke «VRATI» (engl. RETURN) ili «UNESI» (engl. ENTER). Da utvrdite gradivo, proradite sljedeći primjer. Primjer 9.5 Prenesite niz znakova izravno iz objekta cin u znakovni niz objavljen kao u sljedećem programskom odsječku: const unsigned int cN = 25; char cArr[cN]; // ... ... cin >> cArr;
Npr. upišite rečenicu ”Riječi,riječibezrazmaka. Riječi sa razmacima.” Zatim ispišite cArr preko cout i odgovorite: a) Što je upisano u znakovni niz cArr? Kako ćete to provjeriti? b) Kako je okončan taj C-niz? Koja je vrijednost elementa cArr[cN-1]? Tko ili što je obavilo takvo okončanje, tj. umetanje odgovarajuće vrijednosti? c) Promijenite vrijednost zadnjeg elementa u EOF: cArr[cN-1] = -1 ; Je li sada cArr valjani Cniz? Ispišite ga i objasnite ispis. d) Izvedite zaključak: je li izravnim kopiranjem iz objekta cin moguće ostvariti unos proizvoljnog teksta u znakovni poredak? Funkcija get. Detalje o funkciji get, te njezine mnogobrojne inačice (bez argumenata, te s 3, 2 i jednim argumentom) proučite u MSDN knjižnici. get je tzv. članska funkcija (engl. member function) koja pripada klasi istream. Istoj klasi pripada i objekt cin (vidi pogl. 2) koji koristimo za standardni unos sa tastature. Pravilo je objektnog programiranja da na objekte neke klase mogu djelovati samo članske funkcije te iste klase. Djelovanje članske funkcije get na objekt cin se ostvaruje sljedećom sintaksom: cin.get()
,
tj. na isti način kao što se pristupa i članskim varijablama. Funkcija get() bez parametara je tipa int. Ona izlučuje po jedan znak iz ulazne struje cin i vraća ga kao rezultat.
128
Usporedimo li funkcije getchar i putchar s funkcijom get, vidimo da se u prvom slučaju radi o klasičnim funkcijama jezika C, dok se ovdje radi o djelovanju funkcija na odgovarajuće objekte. Jedna od osnovnih ideja objektnog programiranja je da se uz klasu vežu tzv. članske varijable i članske funkcije. Članske funkcije djeluju samo na objekte svoje (i srodnih) klase, prema gore navedenoj sintaksi. Stoga i različitost u sintaksi za C funkcije getchar i putchar naspram C++ članske funkcije get. Primjer 9.6 Primijenite funkciju get() bez argumenata za unos znakova u znakovni poredak, te ostvarite valjani C-niz odgovarajućim okončanjem. Proučite sljedeći programski odsječak i odgovorite na pitanja: a) Iz gore iznesenog zaključite koju zaglavnu datoteku treba uključiti da se razazna funkcija (funkcije) get? b) Objasnite detaljno izvršavanje petlje for. Što se događa ako korisnik unese manje ili točno 80 znakova i pritisne tipku VRATI? Je li znak '\n' unesen u znakovni poredak ili nije? Što se događa ako korisnik unese više od 80 znakova? Što zaključujete, gdje moraju biti pohranjeni znakovi koje vidimo na ekranu prilikom unosa prije nego što će biti učitani u znakovni poredak? c) Zamijenite petlju for odgovarajućom petljom while. d) Modificirajte programski odsječak tako da u svakom C-nizu koji je kraći od 80 znakova ostane znak novog retka '\n' koji je unio korisnik. Objasnite zašto to nije poželjno za niz koji ima točno 80 znakova. Gdje bi se ispisao redak koji bi uslijedio poslije takvog niza? // ... ... const unsigned int cN = 80; char cArr[cN + 1]; // Znakovni poredak za 1 red teksta. // Unos linije teksta: cout << "Unesite jedan redak teksta:\n" << "===========================\n"; char c; for(UINT i = 0; (c = cin.get()) != '\n' && i < cN; i++) cArr[i] = c; cArr[i] = '\0';
// Okončanje C-niza znakom null.
// Ispis linije teksta: cout << "\nIspis retka:\n" << "===========================\n" << cArr << '\n' << endl; // ... ...
Funkcija get s tri argumenta. Funkcija get s tri parametra objavljena je sljedećim prototipom: istream& get( char* pch, int nCount, char delim = '\n' );
Tip funkcije je navodnički tip klase istream, tj. ona vraća referencu na objekt cin klase istream. Prvi argument je kazaljka na početni element znakovnog poretka, drugi je maksimalni broj znakova u poretku, a treći predstavlja znak koji ima funkciju delimitera. «Automatskom inicijalizacijom» trećeg formalnog argumenta izrazom: char delim = '\n'
omogućava se pojednostavljeni poziv funkcije sa samo dva aktualna argumenta. U tom slučaju će treći argument biti upravo one vrijednosti kao što je definirano u prototipu. U našem slučaju to znači da će delimiter biti standardni znak za novi red. U slučaju da korisnik želi kao delimiter neki drugi znak, mora ga eksplicitno navesti kao treći argument u pozivu funkcije.
129
Funkcija get s tri parametra automatski izvlači znakove iz ulazne struje vezane uz objekt cin sve dotle dok ne naiđe na znak delim, dok ne unese nCount – 1 znakova, ili dok ne naiđe na znak EOF. Pri tom se graničnik niti ne izvlači iz ulazne struje, niti ne unaša u poredak. Nakon unosa niz se standardno okončava znakom null. Primjer 9.7 Proučite sljedeći programski odsječak i odgovorite na pitanja: a) Podsjetite se, je li moguće za dimenziju poretka navesti izraz kao što je cN + 1, gdje je cN konstanta? Je li to sintaktički korektno? b) Testirajte izvođenje programskog odsječka kako je ovdje napisan. Pokušajte unijeti dva retka teksta. Objasnite što se događa. Podsjetite se da funkcija get s tri parametra ostavlja znak novog retka u ulaznom toku. c) Maknite komentare ispred retka u kojem se poziva funkcija cin.get(), te ponovo testirajte program. Je li sada unos dva retka moguć? Objasnite. d) Promijenite cN na neku malu vrijednost, npr. cN = 10, te testirajte programski odsječak za unos niza od 10 i više znakova. Objasnite rad obiju funkcija get. Za jednostavnu kontrolu broja unesenih znakova upišite niz “12345678901234567890…”, u kojem je iz broja nula lako uočiti koliko desetica znakova smo upisali, dok posljednji broj različit od 0 govori o dodatnom broju znakova. // ... ... const unsigned int cN = 80; // Konstanta: broj znakova u retku. char cArr1[cN + 1]; // Znakovni poredak za 1. red teksta. char cArr2[cN + 1]; // Znakovni poredak za 2. red teksta. // Unos linije teksta: cout << "Unesite dva retka teksta:\n" << "===========================\n"; cin.get(cArr1, cN + 1, '\n'); // cin.get(); // Čemu služi ova linija koda? cin.get(cArr2, cN + 1, '\n'); // Ispis teksta: cout << "\nIspis dva retka:\n" << "===========================\n" << cArr1 << '\n' << cArr2 << '\n' << endl; // ... ...
Funkcija getline. Jednako kao i funkcija get, funkcija getline je članska funkcija klase istream, tj. može djelovati samo na objekte te klase (cin). Njezin tip i formalni parametri su jednaki kao i za funkciju get, tj. osnovni prototip joj je:: istream& getline( char* pCh, int nCount, char delim = '\n' );
Inicijalizacija trećeg formalnog argumenta na podrazumijevajuću vrijednost (engl. default argument value) ponovo dopušta da se funkcija poziva s dva aktualna argumenta, u kom slučaju se za delimiter podrazumijeva znak novog retka. Rad funkcija getline je sličan kao i kod funkcije get. Izvlače se znakovi iz ulazne struje sve do znaka delim, ili dok se ne unese nCount – 1 znakova, ili dok se ne naiđe na znak EOF. Graničnik se ponovo ne unaša u poredak znakova, ali se, za razliku od funkcije get, on miče iz ulazne struje. Nakon unosa niz se standardno okončava znakom null. Primjer 9.8 Potrebno je ostvariti gornji zadatak uporabom funkcije getline. Promotrite kako je to učinjeno te odgovorite na pitanja: a) Kako je moguće funkciju objavljenu s tri argumenta pozvati s dva? Radi li se ovdje o grešci? Koja
130
je vrijednost delimitera u funkciji getline u tom slučaju? b) Testirajte programski odsječak i odgovorite zašto sada nije potrebno umetnuti poziv cin.get() između poziva funkcije getline()? Podsjetite se koja je razlika u radu funkcije getline u odnosu na funkciju get s tri argumenta. c) Jednako kao i u prethodnom zadatku promijenite cN na neku malu vrijednost, npr. cN = 10, te testirajte programski odsječak za unos niza od 10 i više znakova. d) Ponovite podzadatak d iz prethodnog primjera i za ovaj slučaj. Radi li funkcija kao što očekujemo za unos cN ili više znakova? // ... ... const unsigned int cN = 80; // Broj znakova u retku. char cArr1[cN + 1]; // Znakovni poredak za 1. red teksta. char cArr2[cN + 1]; // Znakovni poredak za 2. red teksta. // Unos linije teksta: cout << "Unesite dva retka teksta:\n" << "===========================\n"; cin.getline(cArr1, cN + 1, '\n'); cin.getline(cArr2, cN + 1, '\n'); // Ispis teksta: cout << "\nIspis dva retka teksta:\n" << "===========================\n" << cArr1 << '\n' << cArr2 << '\n' << endl; // ... ...
Primjer 9.9 Potrebno je napisati program koji sa tastature učitava ime i prezime osobe u znakovni poredak uporabom članske funkcije getline klase istream. Potom je taj C-niz i njegovu duljinu potrebno ispisati na prikazniku. #include #include int main() { char cImeIPrez[30];
// Znakovni poredak dovoljne dimenzije
cout << "\nUnesite ime i prezime:
";
cin.getline(cImeIPrez, sizeof cImeIPrez); cout << "\nUneseno ime i prezime je: " << cImeIPrez << "\nDuljina C niza = " << strlen(cImeIPrez) << "znakova.\n" << endl; return 0; }
Primjer 9.10 Definirajte dvodimenzionalni poredak znakova i inicijalizirajte ga punim nazivima mjeseci (Siječanj, Veljača, … … , Prosinac). Zatim ispišite sadržaj tog poretka tako da imena mjeseci budu jedno ispod drugog. Ako je ispis hrvatskih dijakritičkih znakova nezgrapan, zamijenite ih latinskim slovima. #include int main() {
131 char cMjes[12][9] = {"Siječanj", "Veljača", "Ožujak", "Travanj", "Svibanj", "Lipanj", "Srpanj", "Kolovoz", "Rujan", "Listopad", "Studeni", "Prosinac" }; cout << endl; for(UINT i = 0; i < 12 ; i++) cout << i + 1 << ". mjesec:\t " << cMjes[i] << "\n"; cout << endl; }
return 0;
Primjer 9.11 U prethodnom zadatku drugu dimenziju poretka morali smo podesiti prema najduljem imenu. Elegantnije se to može riješiti uvođenjem poretka pokazivača na tip char. Prevodilac smješta sve C-nizove definirane unutar znakova navodnika "" na odgovarajuće mjesto u memoriji (npr. na izvedbeni stog ako je poredak deklariran lokalno), i zatim adrese početnih bajtova posprema u poredak kazaljki. a) Pokušajte dereferencirati kazaljku pCMjes[i] u ispisu. Objasnite što ste dobili, i zašto? b) Objasnite kako to da se navođenjem pCMjes[i] ispisuje C-niz, a ne vrijednost kazaljke? char * pCMjes[12] = {"Siječanj", "Veljača", "Ožujak", "Travanj", "Svibanj", "Lipanj", "Srpanj", "Kolovoz", "Rujan", "Listopad", "Studeni", "Prosinac" }; cout << endl; for(UINT i = 0; i < 12 ; i++) cout << i + 1 << ". mjesec:\t " << pCMjes[i] << "\n"; cout << endl;
Primjer 9.12 Potrebno je ostvariti program za unos određenog broja znakovnih nizova koji predstavljaju retke teksta. Korisnika se pita za broj redaka koji želi unijeti, uz uvjet da je taj broj manji od neke odabrane konstante (npr. 10). Retke teksta treba pohraniti u «poredak C-nizova», a zatim ih ispisati. Nakon što ste proučili program odgovorite na pitanja: a) Kako je ostvaren poredak C-nizova? Kako drugačije možemo okarakterizirati taj isti poredak? b) Testirajte program i primijetite u čemu je pogreška. Npr. za uneseni broj nizova 2, koliko se nizova upisuje? Pažljivo promotrite (brojeći retke) koliko ih se ispisuje? Ako ne možete odgonetnuti u čemu je logička greška, potražite rješenje u odgovorima na sljedeća pitanja. c) Koja funkcija je uporabljena za unos linije teksta? Podsjetite se kako ta funkcija radi? d) Što je neposredno prije unosa teksta bilo kopirano operatorom >> iz istog «ulaznog» objekta cin? Je li moguće da taj operator, slično kao i funkcija get, ostavlja u ulaznoj struji neki znak? Koji bi to znak mogao biti? e) Maknite komentare ispred poziva funkcije cin.get() i provjerite radi li sada program u redu. Na koncu obrazložite svoj odgovor u čemu je bila pogreška razmatranjem primjera 9.7. #include int main() { const unsigned int cN = 80; const unsigned int cNL = 10;
// Max. broj znakova u nizu. // Broj nizova.
// Poredak znakovnih nizova (2-dim poredak znakova): char cNiz[cNL][cN+1]; unsigned int iN;
// Broj nizova.
132 do { cout << "Unos n [n < " << cNL << "] znakovnih nizova, n = "; cin >> iN; // cin.get(); // Čemu bi mogla poslužiti ova tvrdnja? cout << "===================================================\n"; }while(iN > cNL); // Unos iN C-nizova: for (UINT i = 0; i < iN; i++) cin.getline(cNiz[i], cN + 1); cout << "===================================================\n" << endl; // Ispis iN C-nizova: cout << "Ispis n = " << iN << " znakovnih nizova:\n" << "===================================================\n"; for (i = 0; i < iN ; i++) cout << cNiz[i] << "\n"; cout << "===================================================\n" << endl; return 0;
}
133
Poglavlje 10. Strukturirano programiranje Osnove strukturiranog programiranja Do sada smo već upoznali pojam programske strukture (slijed, izbor, ponavljanje, Vježba 3), upoznali smo se s jednostavnim tipovima podataka (Vježba 2), te s poretkom kao osnovnom podatkovnom strukturom. Konačno, upoznali smo se i s funkcijom kao osnovnom programskom cjelinom jezika C/C++. Sve ove odrednice predstavljaju temelj strukturiranog programiranja (engl. structured programming). Dakle, možemo reći da je strukturirano programiranje karakterizirano uporabom jasno definiranih programskih struktura, odgovarajućih struktura podataka, te organizirano kroz dobro definirane, zasebne i jednostavnije cjeline (u slučaju jezika C funkcije, kod C++ klase). Strukturirano programiranje se u praksi često karakteriziralo kao programiranje bez goto tvrdnje. To je tvrdnja skoka na drugu tvrdnju, koja je označenu oznakom ili labelom (engl. label). U tzv. neproceduralnim jezicima (npr. izvorni FORTRAN, BASIC, i mnogi drugi), ona je bila nužna, a zbog česte "zloporabe" od strane loših programera postala je ozloglašena kao uzrok slabo čitljivog i nepreglednog programskog koda, koji se teško korigirao i usavršavao. Iako se goto tvrdnja može naći na popisu tvrdnji mnogih proceduralnih jezika (npr. Algol, Pascal, C), ona se smatra nepotrebnom i gotovo "zabranjenom". Ne postoji valjani primjer koji bi opravdavao njenu uporabu. Svaka tvrdnja goto unutar programskog koda je siguran znak da programer nije ovladao strukturiranim programiranjem. Primijetimo da dosada nismo imali potrebe za njom, a i sljedeći složeniji primjeri će to potvrditi.
Rješavanje problema strukturiranim programiranjem Kratki naputci i opća načela programiranja dani su u pogl. 5, a potrebno ih je usvajati i iz opsežnije literature namijenjene tome (vidi predavanja). Primijetimo da ćemo točke 2, 3 i 4, koje se ukratko mogu opisati načelom «podijeli pa vladaj», u jeziku C realizirati kroz funkcije (u C++ kroz klase). U tom smislu je u zadacima koji slijede potrebno rabiti funkcije za svaki zaseban, dobro definiran dio problema. Zadatak 10.1 Napišite strukturirani C/C++ program koji izračunava površinu trokuta iz duljine stranica a, b i c s pomoću Heronove formule:
A=
s ( s − a )( s − b)( s − c ) ,
s=
a+b+c 2 ,
gdje je s poluopseg (engl. semi-perimeter). Program najprije unosi duljine stranica a, b, c u «realne» varijable tipa (s pomičnom točkom) kojeg odaberite sami. Površinu izračunava funkcija pod nazivom areaOfTriangleHeorn. Naputak o funkciji kvadratnog korijena (engl. square root) sqrt već je dan u zadatku 5.2. Program omogućava da korisnik ponavlja izračun površine željeni broj puta. Zadatak 10.2 Potrebno je napisati program u C/C++ koji ispisuje prim (proste) brojeve u zadanom intervalu prirodnih brojeva, određenom cjelobrojnim varijablama iMin te iMax unesenim s tipkovnice. Podsjetimo, po definiciji je prim broj prirodni broj različit od 1 koji je djeljiv samo s 1 i sa samim
134
sobom. Program treba realizirati tako da funkcija tipa bool pod nazivom bIsPrimeNumber ispituje je li broj prost ili nije, te ako jest vraća vrijednost true, a u suprotnom false. Ispis se vrši unutar funkcije main(). Primjer unosa i ispisa: Prim brojevi u intervalu: iMin <= Nprim <= iMax ================================================= iMin = 1 iMax = 30
Zadatak 10.3 Modificirajte gornji zadatak tako da funkcija naziva primNumbersInInterval daje popis prim brojeva unutar navedenog intervala. Razmislite kako ćete ostvariti prijenos rezultata iz funkcije. Ispis ostvarite u funkciji main(). Zatim usporedite rješenje s prethodnim zadatkom i diskutirajte. Zadatak 10.4* Modificirajte prethodni zadatak tako da funkcija pod nazivom primNumbersInIntervalEratosthen nalazi prim brojeve unutar zadanog intervala koristeći Eratostenovo sito. Grčki matematičar i astronom Eratosten (Eratosthenes) našao je vrlo učinkovit algoritam za nalaženje prim brojeva. Algoritam umjesto uzastopnog dijeljenja kao testne operacije koristi činjenicu da svi brojevi koji nisu prosti predstavljaju višekratnik nekog ranijeg prostog broja. Algoritam: Algoritam Eratostenovo sito: E0. Za prim brojeve jedi
2 ≤ n ≤ n max
n prim ≤ n max
napisati sve brojeve n iz skupa prirodnih brojeva N za koje vri-
(po definiciji 1 nije prim broj), tj. niz: 2, 3, 4, 5, …
… , nmax .
E1. 2 je prvi prim broj. Poslije broja 2 prekrižimo sve brojeve koji su njegovi višekratnici. E2. Polazimo od najmanjeg broja koji je veći od prethodno ustanovljenog prim broja i koji nije prekrižen, kao sljedećeg prim broja. Prekrižimo sve brojeve koji su njegovi višekratnici. E3. Ponavljamo točku E2 algoritma sve dok je broj od kojeg polazimo manji ili jednak od nmax . Zadatak 10.5 Ostvarite C/C++ funkciju FibonacciNth koja vraća vrijednost i-tog člana Fibonaccijevog niza. Za i-ti član Fi Fibonaccijevog niza vrijedi:
F0 = F1 = 1 ; Fi = Fi − 2 + Fi −1 , i ≥ 2 . Zatim napišite glavnu funkciju koja će korisniku omogućavati da unese i , te dobije ispis Fi . Zadatak 10.6 Uočite što je potrebno za izračun i-tog člana Fibonaccijevog niza u gornjem zadatku? Je li racionalno za svaki novi i računati sve članove iznova? Ostvarite sada funkciju pod imenom FibonacciSeries koja pohranjuje sve članove Fibonaccijevog niza F0 , F1 , F2 , … … , Fn do uključivo n-tog , u poredak odgovarajuće veličine. Glavna funkcija ispisuje te članove. Zadatak 10.7 Napišite C/C++ program koji će za dva zadana prirodna broja n i m pronalaziti najveći zajednički djeljitelj s pomoću Euklidovog algoritma. Npr. najveći zajednički djeljitelj brojeva 8
135
i 12 je 4. Općenito, najveći zajednički djeljitelj brojeva n i m je najveći broj k za kojeg vrijedi: n = l1 × k , m = l2 × k ; n, m, k , l1 , l 2 ∈ N . Euklidov algoritam ostvarite kao zasebnu funkciju. Euklidov algoritam. Najveći zajednički djeljitelj prirodnih brojeva n i m. EUK0. n, m ∈ N . EUK1. r = n % m (r je ostatak cjelobrojnog dijeljenja n / m). Ako je r > 0 , izvršiti zamjenu: n = m i m = r . Ići na E1. EUK2. Ako je r = 0 , tada je m najveći zajednički djeljitelj. KRAJ. Zadatak 10.8 Modificirajte gornji algoritam i program tako da radi za sve cijele brojeve različite od nula. Zadatak 10.9* Napišite program koji korisniku omogućuje da arapski broj učitan s tipkovnice pretvara u rimski zapisani broj. Kod rimskog zapisa koristimo se sljedećim oznakama: I = 1, V = 5, X = 10, L = 50, C = 100, D = 500, M = 1000. Oznake se nižu od najveće potrebne za prikaz određenog broja, prema najmanjoj. Pravilo je da se nikoja znamenka (osim eventualno M) ne smije ponoviti više od tri puta. Tako se broj 3 prikazuje kao III, a broj 4 kao IV (5 – 1), broj 9 kao IX, broj 98 kao XCVIII, a broj 1989 kao MCMLXXXIX. Sugerirajte korisniku preporučeni opseg brojeva.* Zadatak 10.10* Napišite program koji će učitani rimski broj prevesti u arapski broj.
*
Uočite posvemašnju nepreglednost rimskih brojeva u usporedbi s arapskim pozicionim brojevnim sustavom. te njihovu potpunu neučinkovitosti pri računu. Također, iz njihovog standardnog opisa nije jasno kako da se prikazuju brojevi veći od 4000. Očito je da se zbog nepostojanja oznake za 5000 kod tisućica mora odustati od načela da se ne pojavljuju više od tri ista znaka.
136
Poglavlje 11. Pretraživanje i sređivanje Pretraživanje (engl. searching) i sređivanje ili sortiranje (engl. sorting) predstavljaju osnovne radnje koje se obavljaju nad strukturama podataka. U temeljnoj strukturi podataka koju smo upoznali, poretku, često postoji potreba nalaženja elementa neke vrijednosti, odnosno provjera je li on unutar te strukture, te ako jest u elementu kojeg indeksa je on pohranjen. Također, često želimo poredak srediti po nekom načelu, npr. brojeve u uzlaznom (engl. ascending) ili silaznom (engl. descending) nizu, znakovne nizove po leksikografskom načelu (engl. lexicographic order), odnosno po abecedi, i sl. Veličina koju pretražujemo, odnosno po kojoj sređujemo se općenito naziva ključ (engl. key) pretraživanja ili sređivanja.* To može biti podataka bilo kojeg tipa, npr. broj, znak ili niz znakova (Cniz), struktura ili objekt određene klase.
Linearno pretraživanje Ukoliko poredak koje pretražujemo nije sortirano po ključu pretraživanja, tada primjenjujemo tzv. linearno ili slijedno (sekvencijalno) pretraživanje (engl. linear, sequential search). Očito je da u tom slučaju moramo redom proći kroz poredak i usporediti sve postojeće vrijednosti ključeva sa traženom vrijednosti. Linearno pretraživanje je realizirano u sljedećem primjeru. Primjer 11.1 Potrebno je realizirati funkciju pod nazivom linIntSearch koja pretražuje sadrži li 1dim poredak tipa int, te veličine n, cjelobrojni ključ iKey, počevši od indeksa vrijednosti iStart. Ukoliko je ključ pronađen u poretku, funkcija vraća vrijednost indeksa prvog elementa poretka u kojem je ključ. Ukoliko takvog elementa nema, funkcija vraća vrijednost –1. Dodatna pitanja: Kakav je po tipu prvi formalni argument funkcije? Kako ćemo prenijeti poredak iz pozivne funkcije u ovu funkciju? Odgovorite što se događa ako je indeks iStart >= n ? Radi li program ispravno, tj. onako kako bismo očekivali, i za taj slučaj? Ako je potrebno doznati sve indekse elemenata poretka u kojima se nalazi ključ (a ne samo prvog), kako ćete to ostvariti? Kako u tome pomaže povratna vrijednost funkcije? Što će se desiti ako u pozivnom programu vrijednost funkcije linIntSearch() zabunom pridružite varijabli tipa unsigned int (npr. u svrhu provjere vraćene vrijednosti funkcije). Obrazložite svoj odgovor. // Linear Search int linIntSearch (int* pI, unsigned int n, unsigned int iStart, int iKey) {
*
Ključ je temeljni pojam u području baza podataka, gdje on mora biti jedinstven za svaki «cjeloviti podatak» baze. Npr. u slučaju baze podataka o studentima, dobar ključ bi bio matični broj u indeksu, pošto više studenata može imati isto ime i prezime. U područjima računarstava van baza podataka, termin ključ se koristi u labavijem smislu i ne mora biti jedinstven. Tako ćemo ga i mi koristiti.
137 for (unsigned int i = iStart; i < n ; i++) if (pI[i] == iKey) return i; }
return -1;
Primjer 11.2 Potrebno je napisati funkciju tipa void pod imenom outputKeyElsFrmArray (skraćeno za: ispiši elemente jednake ključu, iz poretka), koja uz korištenje gornje funkcije za linearno traženje, treba ispisati sve elemente jednake ključu, zajedno s njihovim indeksima. Dodatno, korisniku treba omogućiti da odabere želi li ispis svih ili najviše prvih nK elemenata jednakih ključu. Pitanja u svezi programskog koda: a) Odgovorite kako će korisnik odabirati opciju za ispis svih nađenih elemenata istovjetnih ključu? b) Kako se koristi funkcija za linearno traženje? c) Primijetite da bismo ovu funkciju lako mogli ostvariti bez uporabe gornje funkcije, u prvom redu zbog jednostavnosti usporedbe brojeva. Napravite to i usporedite dobiveni programski kôd. void outputKeyElsFrmArray(int* piA, UINT uN, int iKey, UINT uSrchN) { // int* piA ; // Pokazivač na početak poretka. // UINT uN ; // Dimenzija poretka. // int iKey ; // Ključ pretraživanja. // UINT uSrchN ; // Ispiši uSrchN prvih elemenata jednakih ključu // (ili manje ako ih nema toliko), a uz // uSrchN = 0 nađi sve elemente jednake ključu if(uSrchN == 0) { uSrchN = uN;
// Ispisati sve el. jednake ključu – dakle max. // njih uN; cout << "Ispis svih elemenata poretka jednakih kljucu K = " << iKey << "\n" << "=====================================================\n";
} else cout << "Ispis prvih " << uSrchN << " el. poretka jedna. kljucu K = " << iKey << "\n" << "=====================================================\n"; int j = 0; UINT uCnt = 0;
// Var. za povratnu vrijednost funkcije. // Brojač nađenih elemenata.
// Opetovani pozivi funkcije linearnog traženja: do { j = linSearch(piA, uN, j, iKey); if (j < 0) break; // Nema više el. jednakih ključu -// -- izlaz iz while petlje. // 0 <= j, nađen el. jednak ključu: cout << "El[" << j << "]= " << piA[j] << "\n"; j++; uCnt++; }while( uCnt < uSrchN ); cout << "=====================================================\n" << "Ukupno nadjeno " << uCnt << " el.\n" << endl; }
138
Primjer 11.3 Ovaj primjer objedinjuje uporabu gornjih dviju funkcija u jedinstvenom glavnom programu.* U glavnom programu, poretke je potrebno kreirati dinamički, te korisniku omogućiti da unese njihovu dimenziju. Glavni program koristi još dodatne dvije funkcije za unos i ispis vrijednosti cjelobrojnih poredaka, te funkciju za ispis izbornika i unos željene radnje. Ujedno je ovo primjer kako u glavni program uključiti funkcije napisane u zasebnim datotekama: a) Najprije otvorite uobičajenu radnu okolinu u Visual C++ za novi projekt Win32 konzolnog primjenskog programa (Win32 Console Application) prikladnog imena, npr. ArraySearch. Zatim u tu radnu okolinu smjestite gornje dvije funkcije u sklopu datoteke pod imenom linSearch.cpp. Također, unutar iste radne okoline kreirajte datoteku linSearch.h i u nju smjestite prototipove ovih funkcija. Uz prototipove funkcija potrebno je u potpunosti opisati zadaću i formalne argumente funkcije, te sve njene posebnosti, kako je prikazano u sljedećem primjeru: // File: linSearch.h // Linearno traženje u poretku s početkom na int* pI, dimenzije int n, // počevši od indeksa iStart. // Funkcija vraća indeks prvog el. jednakog ključu int iKey, // ako takvog nema, vraća vrijednost -1: int linSearch(int* pI, UINT n, UINT iStart, int iKey); // Funkcija ispisuje uSrchN prvih el. jednakih ključu, odnosno sve takve // el. ako je uSrchN = 0.eako // Koristi gornju funkciju linSearch(); void outputKeyElsFrmArray(int* piA, UINT uN, int iKey, UINT uSrchN);
b) Slično kao pod a, postupite za dvije funkcije unosa i ispisa elemenata poretka (nazivi datoteka dani su u komentaru u najgornjem redu). // File: arrayInOut.c #include #include void arrayInput(int* pI, unsigned int n) { cout << "Unos " << n << " cjelobrojnih elemenata u poredak:\n" << "=====================================================\n"; for (UINT i = 0; i < n; i++) { cout << "iA[" << i << "]= "; cin >> *pI++; }
}
cout << "=====================================================\n" << endl;
void arrayOutput(int* pI, unsigned int n) { cout << "Ispis " << n << " cjelobrojnih elemenata poretka:\n" << "=====================================================\n"; for (UINT i = 0; i < n; i++) { cout << "iA[" << i << "]= " << *pI++ << "\n"; if ( (i+1)%10 == 0 ) *
Zbog opsežnosti, ovaj je primjer najbolje izvesti van termina vježbi.
139 cout << "\n"; } cout << "=====================================================\n" << endl; } // File: arrayInOut.h // Unos podataka u cjelobrojni poredak dimenzije n: void arrayInput(int* pI, unsigned int n); // Ispis podataka iz cjelobrojnog poretka dimenzije n: void arrayOutput(int* pI, unsigned int n);
c) U radnu okolinu pospremite i dio s glavnom funkcijom i uz njega pospremljenu funkciju za izbornik u sklopu datoteke ArraySearch.cpp. Na početku ove datoteke nalaze se direktive #include kojima smo uključili zaglavne datoteke linSearch.h, i arrayInOut.h. Primijetimo da imena zaglavnih datoteka koje se nalaze u direktoriju (mapi) istom kao i glavna funkcija, tj. unutar naše radne okoline, pišemo unutar navodnika. Imena zaglavnih datoteka koje se nalaze u direktoriju prevodioca pak pišemo unutar trokutastih zagrada. // File: ArraySearch.cpp // Uključenje zaglavnih datoteka smještenih u direktorijima prevodioca: #include #include #include // Uključenje zaglavnih datoteka smještenih u našoj radnoj okolini, // tj. u direktoriju gdje se nalazi i glavna funkcija: #include "arrayInOut.h" #include "linSearch.h" // Funkcija izbornika: unsigned int uMenu() { int iS; do { cout << "=====================================================\n" << "Linearno pretrazivanje poretka. IZBORNIK:\n" << "=====================================================\n" << "0) Izlaz iz programa.\n" << "1) Kreacija i inicijalizacija poretka;\n" << "2) Unos elemenata poretka;\n" << "3) Ispis elemenata poretka;\n" << "4) Lin. pretrazivanje poretka;\n" << "=====================================================\n" << "Odaberite radnju 0 do 4 : "; cin
>> iS;
cout << endl; }while(iS < 0 || iS > 4); return (unsigned int) iS; } // Glavna funkcija: int main()
140 {
UINT uS = 1; UINT uN; UINT uSrchN;
// Izbornik // Dimenzija poretka // Broj elemenata za ispis
int iIn; int iKey;
// Cjelobrojna var. za unos // Cjelobrojni kljuè
int *piA = NULL; char c;
// Pokazivaè na int
while (uS != 0) { uS = uMenu(); if (uS != 1 && piA == 0 && uS != 0) { cout << "Poredak nije kreiran!!! Odaberi (1) u izborniku!\n\n"; uS = 1; }; switch ( uS ) { case 0 : if (piA != 0) delete []piA; // Obavezno brisanje!!! break; case 1 : { if (piA != 0) { cout << "Poredak dimenzije n = " << uN << " vec postoji!" << "\n" << "Obrisati i kreirati novo? (d): "; cin >> c; cout << endl; if (c == 'd' || c == 'D' || c == 'y' || c == 'Y') delete []piA; else break; }; cout << "1) Kreacija dinamickog poretka i inicijalizacija el. na 0,\n" << " dimenzija poretka, n = "; cin >> uN; cout << endl; piA = new int[uN]; for (int* pI = piA; pI < piA + uN; pI++) *pI = 0;
} break;
case 2 : cout << "2) Unos elemenata poretka;\n" << endl; arrayInput(piA, uN); cout << "Pritisnite bilo koju tipku za nastavak: " << flush; getchar(); cout << endl;
141 break; case 3: cout << "3) Ispis elemenata poretka;\n\n"; arrayOutput(piA, uN); cout << "Pritisnite bilo koju tipku za nastavak: " << flush; getchar(); cout << endl; break; case 4: cout << "4) Lin. pretrazivanje poretka po kljucu, K = "; cin >> iKey; do { cout << "Ispis prvih n elemenata jednakih kljucu,\n" << "(unesite 0 za ispis SVIH elemenata), n = "; cin >> iIn; }while (iIn < 0); uSrchN = (UINT) iIn; cout << endl; outputKeyElsFrmArray(piA, uN, iKey, uSrchN); cout << "Pritisnite bilo koju tipku za nastavak: " << flush; getchar(); cout << endl; break; };
}
return 0; // main() }
d) Nakon što su datoteke pod imenom linSearch.h, linSearch.cpp, arrayInOut.h, arrayInOut.cpp i ArraySearch.cpp kreirane i pohranjene u radnu okolinu, pozivamo jednu po jednu «izvedbenu», tj. .cpp datoteku i prevodimo je tvrdnjom Compile imeDatoteke.cpp iz izbornika Build (Ctrl+F7). Na pitanje želimo li uključiti datoteku u naš projekt odgovaramo potvrdno. Po završetku kompilacija, sve datoteke su propisno uključene u projekt. Pripadajuće funkcije možemo vidjeti u desnom potprozoru zvanom Radni prostor (engl. Workspace), izaberemo li oznaku ClassView. Izaberemo li oznaku FileView vidimo strukturu svih datoteka. Ukoliko se zaglavne datoteke ne nalaze u mapi zvanoj ”Header Files”, možemo ih ručno prebaciti iz mape ”External Dependencies”. e) Konačno, možemo izvršiti i završnu izgradnju (engl. Build) izvršne verzije programa, i pokrenuti ga. Po pokretanju programa testirajte ga za sve mogućnosti koje nudi izbornik. Posebno testirajte dio programa za pretraživanje, te obratite pažnju na robustnost programa. Analiza binarnog pretraživanja. Pretpostavimo da je ključ jedinstven, tj. da se on smije pojaviti samo jednom u poretku od n elemenata. U najpovoljnijem slučaju ključ će biti upravo početni element, tj. pronaći ćemo ga nakon nmin = 1 usporedbi, a u najgorem slučaju napravit ćemo nmax = n usporedbi. Prosječan broj elemenata koje moramo ispitati da nađemo traženi ključ je:
n= gdje aproksimacija vrijedi za veliki n.
1+ n n ≈ , 2 2
142
Binarno pretraživanje U računarstvu presudnu ulogu igra učinkovitost algoritama. Iako je linearno pretraživanje primjer jednostavnog i relativno učinkovitog algoritma, uvijek postavljamo pitanje postoji li brži. Ukoliko je poredak sortiran po nekom ključu, tada je puno bolji algoritam binarnog traženja (engl. binary search). Poredak mora biti sortiran bilo u i) uzlaznom (engl. ascending), ili u ii) silaznom (engl. descending) poretku ili nizu (engl. order, series). Ako je pojava istih elemenata dozvoljena, preciznije je reći za slučaj i) da je niz nesilazan, tj. nakon neke vrijednosti ključa može se pojaviti ista ili veća vrijednost, a ne smije se pojaviti manja. Analogno, za slučaj ii) kažemo da je niz neuzlazan. Najuobičajeniji uređaj poretka je uzlazan (nesilazan), koji je ekvivalentan uređaju u skupu prirodnih, odnosno cijelih brojeva. Leksikografski uređaj («po abecedi») također je tog tipa. Ideja binarnog pretraživanja da se ključ usporedi sa srednjim elementom poretka. Indeks srednjeg elementa se izračuna kao aritmetička sredina rubnih vrijednosti indeksa. U slučaju uzlaznog (nesilaznog) niza gleda se je li traženi ključ veći od vrijednosti srednjeg elementa. Ako jest, tada se postupak «polovljenja» ponavlja u gornjoj polovici poretka, a ako nije u donjoj. Algoritam se nastavlja sve dok se gornji i donji indeks ne izjednače. Za silazno (neuzlazno) sređeni poredak mijenja se samo relacija usporedbe, tj. gleda se je li ključ manji od vrijednosti srednjeg elementa, i nastavlja analogno kao i za uzlazni poredak. Analiza binarnog pretraživanja i usporedba s linearnim. Lako je uočiti da gornji proces uzastopnog polovljenja ima toliko koraka koliko ima mogućih polovica u skupu od n elemenata. Npr. skup od samo jednog elementa ne možemo dijeliti pa tu nema polovljenja. U skupu od 2 elementa imamo 1 korak, u skupu od 4 elementa 2 koraka, u skupu od 8 elemenata 3 koraka itd. U skupu od 6 elemenata morali bismo izvršiti polovljenje na dva podskupa od 3 elementa, a zatim u tim podskupovima izvršiti još najmanje 2 polovljenja, dakle isto kao i za 8 elemenata. Zaključujemo da s k koraka, tj. polovljenja možemo doći do jedinstvenog indeksa za najviše 2 k elemenata. Drugim riječima, za broj elemenata n i broj koraka k algoritma vrijedi:
n ≤ 2k , k ≥ log 2 n =
1 ln k . ln 2
Tu je s log 2 označen logaritam po bazi 2, tzv. dualni logaritam.* Dakle za n elemenata poretka binarno traženje zahtijeva log2 n prolaza kroz petlju. Usporedba linearnog i binarnog pretraživanja. Iz gornjeg izlaganja uviđamo da se analizira učinkovitost algoritma iskazuje potrebnim brojem koraka k (npr. usporedbi elemenata, i sl.) kao funkcije od broja ulaznih podataka n¸ dakle kao k = f (n). Pri tom je bitan samo «red veličine» od n sadržan u funkciji f dok faktori nisu važni. Tako umjesto o točnoj funkciji f govorimo o aproksimativnoj funkciji O («veliko o»), i ocjeni brzine algoritma. U našem slučaju je linearno traženje linearno u n, a binarno je reda veličine log2 n, što je bitno manje. Npr. uz n ≈ 1000, linearno traženje zahtijeva u prosjeku oko n / 2 = 500 usporedbi ključa s elementima poretka, dok će binarno traženje zahtijevati svega 10, jer je log2 1000 ≈ 10. Dakle algoritam će trebati 50 puta manje koraka. Zbog karaktera logaritamske funkcije u odnosu na linearnu, što je veći n razlike su drastičnije. Uvjerite se da je uz n = 106 binarno traženje približno 25 000 puta *
Dualni logaritam lako možemo izračunati preko prirodnog logaritma kao što je naznačeno. Također, dualni logaritmi se lako približno računaju kao inverzna operacija potencije po bazi 2. Npr. log2 (100) je broj između 6 i 7, jer 26 = 64 < 100, i 100 < 27 = 128.
143
brže, uz n = 109 približno 16 700 000 puta brže itd. Iz izloženog je jasno je da je za velike poretke koji su sortirani krajnje neučinkovito rabiti linearno pretraživanje. Primjer 11.4 Potrebno je napisati funkciju za binarno pretraživanje brojevnog poretka, obrativši pažnju na formalnu jasnoću i kratkoću programskog koda. U slučaju da je nađen element jednak ključu (bilo koji element, ako ima više takvih) funkcija vraća indeks tog elementa, a u slučaju kad takav element ne postoji, vraća vrijednost –1. Odgovorite na pitanja: a) Napišite neki nesilazni poredak na papiru, odaberite neki ključ i prateći programski kôd odredite rezultat. Osim što je to dobar način upoznavanja s radom algoritma, to je vrlo često i jedini način pronalaženja logičkih pogrešaka. Ponovite to za nekoliko različitih ključeva. b) Objasnite liniju koda: iL = iM + 1; Zašto je nužno pridodati 1. Objasnite što se događa ako umjesto te liniju stavimo: iL = iM; ? c) Odaberite nesilazni poredak koji ima više jednakih elemenata. Indeks kojeg od jednakih elementa u niz će vratiti ova funkcija? Da se uvjerite u rezultat, odaberite poredak u kojem su svi elementi jednaki. Izvedite zaključak. // Binary Search int binSearch(int* pI, unsigned int n, int iKey) { unsigned int iL = 0; // iL = donji indeks, unsigned int iH = n – 1; // iH = gornji indeks, unsigned int iM = 0 // iM = srednji indeks. while(iL < iH) { iM = (iL + iH)/2; if( iKey > pI[iM] ) iL = iM + 1; else iH = iM; }; if (pI[iL] == iKey) return iL; else return -1; }
Zadatak 11.5 Modificirajte gornju funkciju tako da preko argumenta iCnt daje broj nađenih istovjetnih elemenata. Kako mora biti «prenesen» taj argument, tj. kojeg tipa mora biti, da bismo njegovu vrijednost mogli imati u pozivnoj funkciji? Promotrite donji odsječak koda kao ideju, i dodajte sve ostalo što je potrebno. Objasnite ima li while petlja tijelo, te što se događa u njoj? Usput, da se podsjetite o načinu radu operatora inkrementiranja ++, odgovorite je li moguće zamijeniti njegovu prefiksnu formu sa postfiksnom? // Uključeno pobrojavanje istih el. if (pI[iL] == iKey) { while( pI[++iH] == pI[iL] ); iCnt = iH - iL; return iL; } else return -1;
Zadatak 11.6 Napišite glavni program koji će testirati gornju funkciju.
144
Zadatak 11.7 U programu iz primjera 8.3 dodajte u izborniku mogućnost provjere sortiranosti poretka koju ćete ostvariti kao zasebnu funkciju. Zatim dodajte i binarno pretraživanje poretka. Prije binarnog pretraživanja, obvezatna je provjera sortiranosti poretka. Ako poredak nije sortiran, treba odustati od binarnog traženja uz ispis odgovarajuće poruke.
Sređivanje (sortiranje) numeričkih poredaka Česta je potreba da se elementi poredaka sortiraju prema vrijednostima svojih elemenata. Npr. želimo da ispis poretka s imenima osoba bude po abecednom redu. Također, mogućnost korištenja efikasnog binarnog pretraživanja dodatno motivira potrebu za sortiranjem. Brojevne poretke ćemo jednostavno sortirati prema veličini, u već spomenutom uzlaznom (nesilaznom — ako postoji više jednakih elemenata), odnosno silaznom (neuzlaznom — ako postoji više istih elemenata) uređaju, kao što je već diskutirano u odsječku 8.2. Problem sortiranja poredaka jedan je od temeljnih algoritamskih problema računarstva i njegova analiza zahtjeva konzultiranje dodatne i opsežnije literature. Slično kao i kod pretraživanja, ključni je pojam brzina algoritma, tj. broj potrebnih koraka (u ovoj slučaju zamjena elemenata) za sređivanje poretka od n elemenata. Dodatni čimbenik je i memorijski prostor koji ćemo rabiti pri sortiranju. Ako dopustimo da se rabi veći dodatni prostor moguće je napraviti brže algoritme. Ovdje ćemo razmatrati samo one algoritme koji vrše sređivanje unutar zadanog poretka, tj. na mjestu (engl. in lieu, in place). Jednostavne algoritme kao što su sređivanje usporedbom svaki-sa-svakim, te mjehuričastog sređivanja (engl. bubble sort), lako je shvatiti i napisati, ali je njihova ovisnost reda n2 (točnije ≈ n2 / 2). Zbog kvadratne ovisnosti svi se ovi algoritam smatraju sporim i neprihvatljivim za velike n. To vrijedi bez obzira na brzinu računala, jer postoje bitno bolji algoritmi. Pogotovo treba izbjegavati najsporije, a to su upravo gore spomenuto sortiranje usporedbom svakisa-svakim i mjehuričasto uspoređivanje. Njihova je uporaba opravdana u edukacijske i demonstracijske svrhe, uz ograničenje n do najviše 103. Postoji niz drugih algoritama s istom kvadratnom ovisnosti n2, ali bitno boljim faktorom. Jedan od najboljih je sređivanje umetanjem (engl. insertion sort) prikazan u primjeru 8.11. Ovaj algoritam je u prosjeku dva puta brži od mjehuričastog, i uputno ga je koristiti za n do nekoliko tisuća. Od niza drugih algoritama koji su brži, ali i kompleksniji, spomenimo današnji de-facto standard za sortiranje, algoritam quicksort (od engl. brzo-sređivanje). Njegova je ovisnost reda n log n. Brži je u odnosu na mjehuričasto sređivanje za faktor n / log n , odnosno toliko puta koliko je puta binarno traženje brže od linearnog (vidi 8.2). Zbog važnosti, podsjetimo se brojaka: npr. uz n = 106 quicksort će biti u prosjeku 25 × 103 puta brži, a uz n = 109 čak ≈ 170 × 106 puta brži od mjehuričastog algoritma. Primjer 11.8 Sređivanje usporedbom «svaki-sa-svakim». Realizirajte funkciju pod imenom slowSort koja uzastopnim uspoređivanjem po načelu svaki-sa-svakim, vrši usporedbu elemenata cjelobrojnog poretka i njihovo sređivanje u uzlaznom nizu. Odgovorite na pitanja: a) Objasnite zašto unutarnja petlja ne mora ići kroz sve vrijednosti indeksa poretka? b) Podsjetite se iz elementarne kombinatorike koliko različitih parova (tj. podskupova od 2 elementa) možemo napraviti iz skupa od n elemenata? Kao pomoć, konzultirajte Zadatak 8.5. Je li taj broj upravo jednak broju usporedbi koje moramo napraviti između međusobno različitih elemenata skupa? c)* Nakon što ste odgovorili na prethodno pitanje razmotrite koliko puta će se izvršiti usporedba u unutarnjoj petlji mjehuričastog algoritma, počevši od i = 0, pa zatim za i = 1, itd. Objasnite sljedeći izračun broja izvršenja unutarnje petlje, odnosno broja usporedbi Nu :
145
N u = (n − 1) + (n − 2) + ... ... + 1 = (n − 1) + (n − 2) + ... ... + (n − (n − 1)) = (n − 1)n − (1 + 2 + ... ... + (n − 1)) n −1
= (n − 1)n − ∑ i i =1
(n − 1)n 2 n(n − 1) ⎛ n ⎞ = = ⎜⎜ ⎟⎟ 2 ⎝2⎠ = (n − 1)n −
.
d) Pretpostavite da je poredak već sortiran. Hoće li ta činjenica smanjiti broj potrebnih usporedbi? Hoće li se smanjiti broj zamjena? // Slow Sort – compares all elements with each other void {
}
slowSort(int* pI, unsigned int n) int iTmp; for(unsigned int i = 0; i < n - 1; i++) for(unsigned int j = i + 1; j < n; j++) { if ( pI[j] < pI[i] ) { iTmp = pI[j]; // Zamjena pI[j] = pI[i]; pI[i] = iTmp; } }
Primjer 11.9 Sređivanje mjehuričastim algoritmom. Proanalizirajte mjehuričasti algoritam (bubblesort). Ovdje se uspoređuju susjedni elementi. Veći broj (kod uzlaznog sređivanja) se ovdje premješta prema kraju poretka, slično kao što mjehurić zraka u tekućini isplivava na površinu. Odgovorite na pitanja: a) Ima li ovaj bazični mjehuričasti algoritam manje prolaza od gornjeg algoritma svaki-sa-svakim? b) Koliko prolaza ima za već sređeni poredak? c) Postoji li način da se ovaj algoritam poboljša? // Bubble Sort Basic -- slow as the slowSort !!! void bubbleSortBasic(int iA[], unsigned int n) { int iTmp;
}
for (unsigned int i = n - 1; i > 0; i--) for (unsigned int j = 0; j < i; j++) { if (iA[j] > iA[j+1]) { iTmp = iA[j]; //Swap iA[j] = iA[j+1]; iA[j+1] = iTmp; } }
146
Napomena o uporabi indeksa petlje tipa unsigned int (UINT). Uporaba nepredznačenih cjelobrojnih tipova za brojače for petlji i indekse poredaka je teorijski-formalno opravdana činjenicom da indeksi C/C++ poredaka ne mogu poprimiti negativne vrijednosti. Također, dobiva se dvostruko veći raspon brojeva. Međutim, uz nepredznačene brojače petlje potrebno je obratiti posebnu pažnju kad se takav brojač dekrementira od neke više vrijednosti prema 0, kao u primjeru vanjske petlje gornjeg algoritma. Primijetimo da je uvjet te petlje postavljen kao i >= 0, ona bila beskonačna! Naime, nakon što brojač deklariran kao unsigned int dosegne vrijednost 0, njegovom dekrementacijom ne dobivamo negativan broj, već zbog tzv. obmotavanja (engl. wrap-around) nepredznačenih brojeva, dobivamo maksimalni pozitivni broj (vidjeti mnogobrojne primjere u pogl. 2). Stoga je uvjet petlje uvijek zadovoljen i ona se izvršava beskonačan broj puta. Rezimirajmo to sljedećim primjerom i upozorenjem: for( unsigned int i = n – 1; i >= 0; i-- ) { // ... ... ... // ... ... ... }
// OPREZ!!! Beskonačna petlja!!!
Ukoliko je potrebno da se brojač petlje dekrementira do uključivo 0, te se želi izaći iz petlje kad on postane negativan, tada je nužno da on bude predznačenog tipa.* Gornju petlju bismo morali napisati kao: for( int i = n – 1; i >= 0; i-- ) { }
// ... // ...
... ...
// Petlja se izvršava n puta. // Zadnji prolaz kroz petlju uz i = 0, // izlaz iz petlje za i = –1 .
... ...
Primjer 11.10 Poboljšani mjehuričasti algoritam. Osnovni mjehuričasti algoritam jednake je brzine kao i sređivanje usporedbom svaki-sa-svakim. Međutim, dok se algoritam svaki-sa-svakim ne može unaprijediti, mjehuričasti algoritam nudi mogućnosti poboljšanja. Može se primijetiti da unutarnja petlja odgovara provjeri sortiranosti poretka (vidi i zad. 8.7). Ako u toj petlji nije došlo do nijedne zamjene znači da je poredak sortiran, pa to nudi mogućnost jednostavnog poboljšanja algoritma. Odgovorite na pitanja: a) Objasnite detaljno kako se kontrolira sortiranost poretka, te kako se to koristi u vanjskog petlji? b) Koliko usporedbi će biti za već sređeni poredak? c) Što se događa ako je poredak inverzno sortiran? Bi li se algoritam mogao unaprijediti da radi uzastopne naprijed i natrag usporedbe susjednih parova (engl. forward-backward bubblesort). // Improved Bubble Sort void bubbleSort(int iA[], unsigned int n) { int iTmp; bool bNotSorted = true; for (unsigned int i = n - 1; bNotSorted && i > 0; i--) { bNotSorted = false; for (unsigned int j = 0; j < i; j++) {
*
Algoritmi se najčešće daju napisati tako da se uvjet za brojač petlje vidljivo iz primjera za mjehuričasto sređivanje.
i >= 0
dade zamijeniti s
i > 0,
kao što je i
147
}
}
if (iA[j] > iA[j+1]) { bNotSorted = true; iTmp = iA[j]; //Swap iA[j] = iA[j+1]; iA[j+1] = iTmp; }
}
Primjer 11.11 Sređivanje umetanjem. Kao što je već napomenuto, od jednostavnih algoritama s n2 ovisnosti najučinkovitije je sortiranje umetanjem. Ovaj algoritam se često koristi u praksi, npr. prilikom sređivanja karata koje igrač drži u ruci. Uzme se karta, uspoređuje sa ostalima, i onda umetne na ispravno mjesto s obzirom na npr. sebi lijeve karte. Slično, kod sređivanja ključeva u poretku, polazimo od elementa za jedan većeg od početnog indeksa ( i = 1 ), te ga uspoređujemo redom s elementima manjih indeksa (vidi donji algoritam). Sve dok je taj odabrani element manji, posmičemo elemente manjih indekasa «u desno», tj. na mjesta većih indekasa. Kad se nađe prvi manji element, posmak se više ne vrši, te se na preostalo prazno mjesto umetne dotični element. Zatim se algoritam nastavlja za sljedeći element ( i = 2 ). Kod algoritma umetanjem prednji dio poretka do vrijednosti indeksa i je uvijek sortiran. Dodatni zadaci: a) Da biste dobro razumjeli kako radi ovaj algoritam napišite nekoliko nesređenih poredaka na papiru i sortirajte ih sljedeći donji programski kôd. Potrebno je u svakom koraku algoritma zapisati vrijednosti svih indeksa i varijabli. b) Proanalizirajte ponašanje ovog algoritma u slučaju već sređenog i inverzno sređenog poretka. // Insertion Sort (the best among the slow n2 algorithms). void insertionSort(int iA[], unsigned int n) { int iCmp; // Element to compare for (unsigned int i = 1; i < n; i++) { unsigned int j = i // Inner index iCmp = iA[j]; while ( j > 0 && iCmp < iA[j-1] ) { iA[j] = iA[j-1]; j--; } iA[j] = iCmp; } }
Zadatak 11.12 Unaprijedite funkcije za sređivanje svaki-sa-svakim, poboljšani mjehuričasto i sređivanje umetanjem (primjeri 8.8, 8.10 i 8.11) tako da korisnik može odabrati želi li sređivanje prema uzlaznom ili silaznom nizu. Rukovodite se generalnim načelom da je bolje napisati nešto više linija koda nego usporiti svaki korak algoritma. Ostvarite to na dva načina: i) s pomoću varijable tipa bool bDescendingOrder, koja je za normalni uzlazni poredak lažna, a za silazni istinita. ii) Razmislite kako dojaviti funkciji smjer sortiranja bez uvođenja novih argumenata! Ideja: pošto veličina poretka n ne može biti negativna, uvedite dogovor da se uzlazno sortiranje obavlja kao gore, a silazno slanjem negativne vrijednosti –n za veličinu poretka. Oprez! Što morate učiniti sa tipom argumenta za n u tom slučaju? Treba li nova funkcija imati drugačije ime od ove prethodne? Što treba učiniti prilikom pisa-
148
nja prototipa ovakve funkcije da bi korisniku jasno dali do znanja kako da ostvari njenu punu funkcionalnost? Zadaci 8.13 Neka je zadano 2-dimenzionalni poredak dimenzija n×m (podsjetimo se, to odgovara matrici ili tablici s n redaka i m stupaca). Ključ je pohranjen u početnom stupcu, tj. u nizu elemenata iA[i][0] s drugim indeksom 0. Potrebno je prilagoditi funkcije sortiranja tako da se zajedno sa zamjenom ključeva obavlja i odgovarajuća zamjena elemenata u svim ostalim stupcima. Rješenje: Dano je rješenje za poboljšani mjehuričasti algoritam. Analogno slijedi rješenje za sređivanje usporedbom svaki-sa-svakim. Također, po istom obrascu dovršite prilagodbu funkcije za sređivanje umetanjem. // Improved Bubble Sort – sorts by the key element in iA[i][0] // Swaps all elements in the columns iA[i][k], k = 1, 2, ... , m – 1. void bubbleSort(int* iA, int n, int m) { int i, j, iTmp, k; bool bNotSorted = true;
}
for (i = n - 1; bNotSorted && i >= 0; i--) { bNotSorted = false; for (j = 0; j < i; j++) { if (iA[j][0] > iA[j+1][0]) { bNotSorted = true; for(k = 0; k < m; k++) // Swap all “columns” { iTmp = iA[j][k]; iA[j][k] = iA[k+1][0]; iA[k+1][0] = iTmp; } } } }
Zadatak 11.14 Uočite u gornjim funkcijama sortiranja dijelove programskog koda koji obavljaju zamjenu pojedinih elemenata. Ostvarite funkciju tipa void pod imenom swap koja će to obavljati. Za koje algoritme se može koristiti ista funkcija swap? Koliko funkcija zamjene treba ostvariti za algoritam sređivanja umetanjem? Zatim pažljivo proučite sljedeći komentar: Komentar u uporabi funkcija u radno intenzivnim odsječcima programskog koda. Zamjena nezavisnih programskih cjelina je u skladu s načelima strukturiranog programiranja i povećane ponovne uporabljivosti programskog koda. Međutim, poziv funkcije je zahtjevna operacija glede korištenih računarskih izvorišta (resursa), pod kojima podrazumijevamo korištenu memoriju i utrošeno vrijeme, odnosno nužan broj strojnih instrukcija koje se moraju izvršiti. Stoga se prilikom umetanja funkcija u dijelove programskog koda koji se često pojavljuju pažljivo razmatra nije li bolje odustati od poziva funkcije te napisati dio potrebnog koda u cijelosti. Pošto je broj koraka potrebnih za izvršenje funkcija sređivanja kritičan čimbenik, opravdano je upisati cijeli potreban kôd. Definiranje «makroa». Način da se skrati pisanje često ponavljanog dijela koda je definiranje tzv. makroa (engl. macro). Ta ideja se često koristi u zbirnim jezicima, gdje se govorilo o makro-tvrdnji (engl. macro-statement) U C/C++ makro se ostvaruje s pomoću direktive #define koju možemo pisati na vrhu .cpp datoteke (npr. ispod #include direktiva, ili ako definirani identifikator želimo učiniti
149
doglednim i u drugim datotekama,* onda u odgovarajućoj zaglavnoj datoteci. Npr. želimo li uvesti kratku i preglednu oznaku za dio koda u kojem vršimo zamjenu dviju varijabli, možemo definirati makro pod nazivom swap2 na sljedeći način: #define swap2(a, b, t) (t) = (a); (a) = (b); (b) = (t);
Kompajler će na mjestu zapisa imena makroa upisati ekvivalentnu tvrdnju (tzv. C/C++ token) (cijeli programski kôd iza znaka = u definiciji makroa). Dodatne zagrade u definiciji makroa osiguravaju ispravnost tvrdnje nakon zamjene. Npr. ako se u programskom kodu napiše: // ... ... swap(x1, x2, iTmp);
prevodilac će na tom mjestu izvršiti jednostavnu zamjenu teksta u skladu s definicijom makroa: // ... ... iTmp = x1; x1 = x2; x2 = iTmp;
Očito je da svi rabljeni identifikatori moraju biti propisno deklarirani da bi supsituirani kod bio valjan. Naglasimo da se makro «ne poziva» kao što se pozivaju funkcije! Na mjesto makroa ugrađuje se kôd u vrijeme kompliranja. Koliko puta je makro napisan u programu, toliko puta će njegov supstituirajući kôd biti upisan na odgovarajućim mjestima.† Zadatak 11.15* Konstruirajte testni program za ispitivanje «sporih» algoritama sređivanja, koji imaju ovisnosti brzine o broju n elemenata izraženu kao c × n2. Definirajte poredak od 10 elemenata, a kasnije ga povećajte. U dijelu rješenja dolje, date su tri inicijalizacije poretka, od kojih dvije moraju biti «iskomentirane». U prvom poretku su elementi nasumično raspoređeni, u drugom su već sređeni, a u trećem su sređeni u inverznom poretku. U glavnom programu pozivamo samo jednu od funkcija slowSort, bubbleSortBasic, bubbleSort i insertionSort, dok su preostale iskomentirane: int main() { const unsigned int cN = 10; // Array definition: int iArr1[cN] = { 23, 0, -100, -123, 11, 1234, 333, 98, 65, 65}; // { -123, -100, 0, 11, 23, 65, 65, 98, 333, 1234}; // { 1234, 333, 98, 65, 65, 23, 11, 0, -100, -123}; // Sorting functions: // // //
slowSort(iArr1, cN); bubbleSortBasic(iArr1, cN); bubbleSort(iArr1, cN); insertionSort(iArr1, cN);
*
Identifikator definiran van tijela funkcija je općenito dogledan unutar .cpp datoteke gdje je definiran. Želimo li ga učiniti doglednim i u drugim datotekama, definiramo ga u zaglavnoj datoteci koju zatim uključimo direktivom #include u svim onim datotekama u kojim ga trebamo (vidi također pogl. 1, i primjer. 8.3).
†
U C++ postoji mogućnost da se članske funkcije klasa objave kao inline funkcije. Takvom objavom se kompajler upućuje da na mjestu navođenja funkcije, umjesto da je poziva, ugradi njen kôd slično kao što to radi makro definiran direktivom #include, ali naravno uz mogućnost pisanja po volji složenog koda. Time je programerima omogućeno da sami odluče gdje je racionalnije ugrađivanje programskog koda u odnosu na vremenski zahtjevniji poziv funkcije. Prevodilac ipak može odbiti taj zahtjev za rekurzivne funkcije i one koje se pozivaju preko pokazivača umjesto preko svog imena (za detalje vidjeti MSDN biblioteku naputaka). U Visual C++ prevodiocu ta mogućnost ne postoji za funkcije definirane u «starom» stilu jezika C, tj. za funkcije van klasa.
150 for (UINT i = 0; i < cN; i++) cout << iArr1[i] << " "; cout << "\n" << endl; return 0; }
U svim funkcijama dodajte linije koda za brojanje «prolaza» (tj. broja usporedbi), te broja izvršenih zamjena. Nešto složeniji primjer brojanja usporedbi i zamjena za funkciju insertionSort riješen je na sljedeći način: // Insertion Sort – modified to count the comparisons and swaps. void insertionSort(int iA[], unsigned int n) { int iSel; // Temp. var. for selected element UINT uCnt1 = 0, uCnt2 = 0, uCnt0 = 0; for (unsigned int i = 1; i < n; i++) { unsigned int j = i; iSel = iA[j]; while ( j > 0 && iSel < iA[j-1] ) { iA[j] = iA[j-1]; j--; uCnt0++; } iA[j] = iSel; uCnt2 += uCnt0; if(uCnt0 == 0) uCnt1++; else
uCnt1 += uCnt0;
uCnt0 = 0; } cout << "insertionSort, broj prolaza = " << uCnt1 << ", broj zamjena = " << uCnt2 << "\n" << endl; }
Nakon što ste modificirali i ostale funkcije za ispis broja prolaza i zamjena pristupite testiranju. Počnite od funkcije slowSort redom za slučajni raspored elemenata, za sređeni niz i za inverzno sređeni niz, a zatim nastavite za sve ostale funkcije. Bilježite broj usporedbi i broj zamjena. Dodatno odgovorite na sljedeća pitanja: a) Koliki je broj usporedbi za algoritam sređivanja usporedbom svaki-sa-svakim uz n = 10. Slaže li se taj broj s formulom izvedenom u Primjer 10.8? Ovisi li taj broj o sređenosti niza? b) Usporedite podatke za bazični mjehuričasti algoritam s onima pod (a). c) Što primjećujete kod poboljšanog mjehuričastog algoritma? Kako se on ponaša za slučajni niz, a kako za već sređeni? d) Pažljivo proučite ponašanje algoritma za sređivanje umetanjem. Je li bilo koji prethodni algoritam, za bilo koji slučaj sređenosti poredaka, bolji od njega?
151
Zadatak 11.16 (Opsežniji zadatak za domaći rad.) Dodajte sada programu iz primjera 8.3 novu stavku u izborniku koja će sortirati poredak s pomoću sređivanja umetanjem. Potrebno je ostvariti funkcionalnost da se odmah po unosu novih vrijednosti u poredak korisnika pita želi li izvršiti sređivanje polja. Da učinite program praktičnijim za uporabu, možete dodati unos (i ispis) elemenata za samo zadani raspon indeksa, što je očita pogodnost kod velikih poredaka. Zadatak 11.17 (Opsežniji zadatak za domaći rad.) Preuredite program iz zadatka 8.3 uz sva poboljšanja (vidi zad. 8.15) tako da radi s realnim podacima tipa float. Koristite uređivačke pogodnosti grafičke radne okoline za kreiranje novih funkcija koristeći radnje nađi (engl. find) i zamijeni (engl. replace) prilikom promjene tipova i preimenovanja varijabli. Konačno, dodajte statistički modul programu, koji će izračunavati srednju vrijednost i standardnu devijaciju. Primjer 11.18 Brzo sortiranje – quicksort. Za sortiranje poredaka s 103 – 104 i više elemenata nužno je koristiti naprednije algoritme. Od njih je najpoznatiji već spomenuti quicksort. Njegovo razmatranje prelazi okvire ovog izlaganja. Osvrnut ćemo se na njegovo korištenje u C/C++ preko njegove implementacije u funkciji qsort. Njezin je prototip sljedeći: void qsort( void *base, size_t num, size_t width, int (__cdecl *compare )(const void *elem1 const void *elem2 ) );
Promotrimo redom formalne argumente: void* base. Prvi argument je kazaljka na «baznu adresu», tj. početni element poretka, jednako kao i u gore izloženim algoritmima. Međutim, pošto je ova funkcija namijenjena sređivanju poredaka s elementima proizvoljnog tipa, umjesto očekivanog tipiziranja kazaljke kao elType*, gdje je elType proizvoljan tip elementa poretka, funkcija mora prihvatiti proizvoljan tip. To se postiže uvođenjem pokazivača tipa void. Podsjetimo se da uobičajene pokazivače interpretiramo kao tipizirane adrese, u smislu da prevodilac strogo vodi računa o tome na koji tip podataka se ta adresa odnosi. Ako pak pokazivač deklariramo kao: void *pVoid;
tada će on moći prihvatiti adresu bilo kojeg tipa i zamišljamo ga kao «bezličan» pokazivač. size_t num.
Drugi argument num predstavlja broj elemenata poretka. Tip size_t je u bitnom nepredznačeni tip u kojem svoju vrijednost vraća operator (i funkcija) sizeof (detalje pogledajte u MSDN knjižnici). Podsjetimo (vidi pogl. 4xx i Primjer 10.14) da num općenito ne možemo dobiti s pomoću operatora sizeof(arrName), gdje je arrName ime poretka. S pomoću sizeof se dobiva veličina poretka izražena u bajtovima. Veličina poretka nam je, naravno, poznata iz njegove objave.
size_t width.
Treći argument je «širina», odnosno veličina elementa poretka izražena u bajtovima. Dobivamo je elegantno primjenom funkcije sizeof(elType), gdje je elType tip elementa poretka. int (__cdecl *compare )(const void *elem1 const void *elem2 ). Četvrti i zadnji argument predstavlja pokazivač na funkciju usporedbe. Korisnik funkcije qsort mora sam specificirati funkciju usporedbe, ovisno o tipu podataka s kojim radi. Ovo je ujedno ilustracija kako se i funkcije mogu navoditi kao argumenti (drugih) funkcija. Već je istaknuto da prevodilac pri kompilaciji analizira sve identifikatore (vidi pogl. 1), i za sve one koji nisu ključne riječi gradi tzv. tablicu simbola (engl. symbol table). U toj tablici se imenu funkcije pridjeljuje adresa mjesta gdje je funkcija definirana, slično kao što se imenu poretka pridjeljuje bazna adresa. _cdecl predstavlja oznaku (tip) C/C++ funkcije tzv. standardnog poziva (detalje vidi u MSDN knjižnici). Shodne tome _cdecl* je kazaljka na takve funkcije, koja se jednostavna podudara s njenim imenom (vidi primjer niže). Funkcija usporedbe compare uspoređuje dva argumenta arg1, arg2, i vraća cjelobrojne vrijednosti –1, 0, 1 ako je arg1 < arg2, arg1 == arg2, arg1 > arg2, respektivno. Pošto se mora osigurati potpuna općenitost,
152
odnosno mogućnost usporedbe za sve tipove, uključujući i one korisnički definirane (npr. C strukture, objekti C++ klasa), do argumenata se ponovo pristupa preko bezličnih kazaljki (tipa void* ). Objavom const void* osigurava se da se te kazaljke neće moći promijeniti unutar funkcije qsort. Poziv funkcije qsort za sortiranje poretka tipa int i double, te realizacija potrebnih funkcija usporedbe ilustrirana je sljedećim programom: // Compares 2 integers, (i1, i2): // If i1 < i2 returns -1, if i1 == i2 returns 0, if i1 > i2 returns 1. int intCmp(const void* pI1, const void* pI2); // Compares 2 double floating variables, (d1, d2): // If d1 < d2 returns -1, if d1 == d2 returns 0, if d1 > d2 returns 1. int doubleCmp(const void* p1, const void* p2); int main() { const unsigned int cN = 10; // // // // // // //
Integer arrays: int iArr[cN] = { 23, 0, -100, -123, 11, 1234, 333, 98, 65, 65}; // random { -123, -100, 0, 11, 23, 65, 65, 98, 333, 1234}; // ordered { 1234, 333, 98, 65, 65, 23, 11, 0, -100, -123}; // inversed Double arrays: double dArr[cN] = { 2.3, 0., -10.0, -12.3, 1.1, 123.4, 33.3, 9.8, 6.5, 6.5}; // random { -12.3, -10.0, 0., 1.1, 2.3, 6.5, 6.5, 9.8, 33.3, 123.4}; // ordered { 123.4, 33.3, 9.8, 6.5, 6.5, 2.3, 1.1, 0., -10.0, -12.3}; // inversed 1. quicksort for int array: qsort( (void *) iArr, cN, sizeof(int), intCmp);
//
2. quicksort for double array: qsort( (void *) dArr, cN, sizeof(double), doubleCmp);
//
Output: for (UINT i = 0; i < cN; i++) cout << iArr[i] << " "; cout << "\n" << endl; for (i = 0; i < cN; i++) cout << dArr[i] << " cout << "\n" << endl;
}
";
return 0;
int intCmp(const void* p1, const void* p2) { int *pI1 = (int*) p1; int *pI2 = (int*) p2; int i1 = *pI1; int i2 = *pI2; // //
Shorter: int i1 = * (int*) p1, i2 = * (int*) p2; if( i1 < i2) return -1; else if( i1 == i2) return 0; else
153 }
return 1;
int doubleCmp(const void* p1, const void* p2) { double d1 = * (double*) p1, d2 = * (double*) p2;
}
if( d1 < d2) return -1; else if( d1 == d2) return 0; else return 1;
Promotrimo prvi poziv funkcije za sređivanje cjelobrojnog poretka iArr: qsort( (void*) iArr, cN, sizeof(int), intCmp);
Kao prvi argument stavljeno je ime poretka iArr (dakle bazna adresa), kojem je nabačen bezličan pokazivački tip void* da se u uskladi s tipom formalnog parametra. Za sljedeća dva argumenta stavljeni su broj elemenata u poretku i veličina elementa, a posljednji argument je ime funkcije koja će uspoređivati dva broja tipa int. Cjelobrojna funkcija pod nazivom intCmp je uz detaljan komentar objavljena svojim prototipom ispred glavne funkcije: int intCmp(const void* p1, const void* p2);
Objava i formalni parametri su točno u skladu sa zahtjevom navedenim u funkciju sqrt (vidi gore). Funkcija je definirana ispod glavne funkcije. Najprije je bezličnim kazaljkama tipa void* nabačen pokazivački tip int* sukladan tipu podataka koji ćemo uspoređivati. Zatim su te kazaljke dereferencirane, i dobivene varijable tipa int uspoređene. Primijetite kako je to u iskomentiranom dijelu koda kraće i elegantnije napravljeno. Na potpuno analogan način realiziran je poziv funkcije qsort za sređivanje poretka tipa double, kao i realizacija odgovarajuće poredbene funkcije doubleCmp. Testirajte program i odgovorite na sljedeća pitanja: a) Za vježbu, maknite pridjeljivanje tipa void* kod stvarnog argumenta pri pozivu qsort i vidite što se događa pri kompilaciji? Što zaključujete da je prevodilac napravio? Je li ipak formalno korektnije pridijeliti ispravan tip stvarnom argumentu? b) Pokušajte zamijeniti objave bezličnih kazaljki u funkciji intCmp s kazaljkama nekog drugog tipa, npr. s int*. Hoće li proći kompilacija? Objasnite. c) Testirajte program za slučajno raspoređene elemente, za sređeni i inverzno sređenji poredak, mičući i stavljajući komentare ispred odgovarajućih dijelova koda.
Sređivanje tekstualnih poredaka. Ukoliko se radi o tekstualnom poretku, leksikografski (abecedni) uređaj se također svodi na brojevnu usporedbu kodnih zamjena ASCII koda za pojedina slova. Tako za 26 velikih slova latinske abecede (koja se podudara s engleskom), vrijedi da su kodne zamjene ASCII koda: ASCII('A') = 41h, … … , ASCII('Z') = 5Ah , a za 26 malih slova: ASCII('a') = 61h, … … , ASCII('z') = 7Ah (vidi pogl. 2 i tablicu ASCII koda). Prilikom usporedbe, razumljivo da se sva slova tretiraju bilo kao velika, bilo kao mala, pa se tek onda vrši usporedba. U skladu sa «C/C++ stilom», uobičajeno je tretirati sva slova kao mala. Za abecede koje su istovjetne latinskoj (engleskoj) time je problem u potpunosti riješen. Činjenica da znak praznine ( ASCII(' ') = 20h ) ima manju vrijednost kodne zamjene od bilo kojeg slova
154
udovoljava dogovoru da će kraća riječ biti leksikografski ispred duže kojoj je ova kraća predmetak (prefiks). Npr. riječ «program» je abecedno prije «programiranje». Abecede koje imaju dijakritičke znakove (sastavljene od slova latinske abecede i posebnih dodatka, apostrofa, crtica, kvačica, itd…) u koje spada i hrvatska abeceda, usporedba se komplicira jer treba voditi računa o položaju tih nestandardnih znakova. Ta se problematika rješava uvođenjem posebnih kodnih stranica za specifične abecede pojedinih jezika. Usporedba C-nizova.
155
Poglavlje 12. Dodatna poglavlja funkcijskog programiranja Kao što je istaknuto u uvodnom poglavlju, zadaća ovih vježbi je usvajanje osnovnih znanja iz funkcijskog programiranja u jeziku C. Temeljito poznavanje jezika C i njegovih velikih mogućnosti prelazi okvire ovih vježbi, i čitalac se za to upućuje na dodatnu literaturu. No, da bismo zaokružili cjelinu koja se može nazvati «osnove proceduralnog (funkcijskog) programiranja», u ovom poglavlju razmatramo jednu važnu programsku strukturu — rekurziju, te složeni tip podataka zapis, ostvaren kao C struct.
Rekurzija U računarskom smislu rekurzija predstavlja algoritam koji poziva samog sebe. Taj poziv se ponavlja sve dok se ne udovolji nekom uvjetu, tj. dok se ne dosegne tzv. dno rekurzije. Postoji analogija s čovjekom koji se promatra između dva paralelna zrcala, ili sa kamerom koja snima televizijski prijemnik koji emitira ono što kamera snima. U oba slučaja se u slici nalazi manja slika koja sadrži veću, u manjoj slici još manja, itd. Kao što je za ostvarenje ovog efekta nužno imati dva zrcala, ili kameru i prijemnik, tako u rekurzivnoj funkciji mora postojati barem jedan poziv kojom funkcija poziva samu sebe. Rekurzivni algoritmi su u svezi s klasom matematičkih funkcija koje se nazivaju rekurzivne, i kod kojih se rješenje za f(n) može opisati kao f(n) = g( f(n – 1) ). Primjer takve funkcije je npr. i poznata funkcija faktorijela:
n! = n × ( n − 1)! , 0! = 1 . gdje je dno rekurzije definirano izrazom 0! = 1 (vidi također zad. 3.17). Međutim, za faktorijele je bilo vrlo lako izraditi i iterativno rješenje. Pošto je poziv funkcije računarski puno zahtjevniji od ostvarenja iteracije, iterativno rješenje se smatra boljim, i glede utrošene memorije i glede vremena potrebnog za dovršenje algoritma. Za vježbu, uputno je napraviti program koji faktorijele izračunava i s pomoću rekurzivne funkcije. Primjer 12.1 Rekurzivna funkcija za izračun faktorijela. // Rekurzivni izračun faktorijela: double factorialR(unsigned int n) { if(n > 0) return n*factorialR(n – 1) ; else return 1. ; }
Zadatak 12.2 Izradite program koji učitava cijeli nenegativni broj n ≥ 0, i zatim uz pomoć funkcije factorialR izračunava n! U glavnom programu je potrebno osigurati cjelobrojni ispis za sve rezultate unutar opsega nepredznačenog cjelobrojnog tipa, te ispis u obliku tipa double pune preciznosti za veće rezultate.
156
Zadatak 12.3 Proanalizirajte funkciju factorialR. Odredite koliko će se ukupno biti rekurzivnih poziva za izračun n! Podsjetite se da se prilikom svakog poziva na stog prenese argument funkcije, pa se zatim izgradi pozivni stožni okvir (vidi dodatak C). Potom se preda kontrola izvođenja pozvanoj funkciji. Odgovorivši na pitanje koliko će biti rekurzivnih poziva, odgovorili ste koliko će biti pozivnih stožnih okvira, a svaki od njih zauzima na stogu određeni broj bajtovaxXXX. Po izlasku iz funkcije rezultat se vraća npr. preko registra, i kontrola izvođenja se vraća na pozvanu funkciju. Sve to zajedno iziskuje više memorijskog prostora i više strojnih instrukcija nego izvođenje nerekurzivne inačice funkcije. a) Usporedite rekurzivnu funkciju factorialR s rješenjem iz zad. 3.17, te diskutirajte. b) Koju ćete funkciju rabiti u praksi? Primjer 12.4 Napišite rekurzivnu C/C++ funkciju za Euklidov algoritam koji za dva zadana prirodna broja n i m pronalazi najveći zajednički djeljitelj (vidi zad. 7.7 i dodatni zad. 7.8 ). Dodatna pitanja: a) Proanalizirajte algoritam i raspišite ga za slučaj brojeva n = 24 i m = 9. Kao primjer uzmite raspis rekurzivnog algoritma iz primjera 12.6. b) Usporedite rekurzivni algoritam s njegovom iterativnom zamjenom (zad. 7.8). // Recursive Euclid Algorithm int euclidR(int n, int m) { int iR = n % m; if(iR != 0) euclidR(m, iR); else { if(m < 0) m = -m; return m; } }
Zadatak 12.5 Kompilirajte gornju funkciju (prečac ctrl+F7) i diskutirajte što dojavljuje prevodilac. Je li upozorenje relevantno? Zatim pozovite rekurzivnu funkciju za Euklidov algoritam iz glavnog programa (npr. onog iz zadatka 7.7) i testirajte ju. Složenije rekurzije. U gornjim primjerima dali smo nekoliko rekurzivnih funkcija koje imaju jednostavno nerekurzivno (iterativno) rješenje. To je gotovo uvijek slučaj kad funkcija sadrži samo jedan rekurzivan poziv. Međutim, postoji niz problema koje je teško riješiti uobičajenim iterativnim pristupom, a postoji vrlo elegantno i jednostavno rekurzivno rješenje. Iako nerekurzivna inačica algoritma uvijek postoji, često je ona vrlo kompleksna i teška za razumijevanje. Stoga se takvim problemima pristupa tako da ga najprije rješavamo s pomoću rekurzije.* Uobičajeni primjer za kompleksan problem s jednostavnim rekurzivnim rješenjem su Hanojski tornjevi (engl. Towers of Hanoi). Primjer 12.6 Hanojski tornjevi. Na raspolaganju su tri štapa označena kao s1, s2, s3, na koja stavljamo probušene diskove različitih promjera i gradimo «tornjeve». Neka je zadatak da sve diskove prebacimo sa štapa s1 na štap s2, koristeći pritom štap s3 kao pomoćni, te poštujući sljedeća pravila:
*
Ukoliko su zahtjevi za utroškom memorije i vremena kritični, nakon što je rekurzivni algoritam poznat, on se može s pomoću standardnih pravila prevesti u nerekurzivni.
157
Diskovi se moraju stavljati samo na raspoložive štapove s1, s2 ili s3; Diskovi se smiju prebacivati samo jedan po jedan; Diskovi na štapu moraju uvijek biti poredani tako da je disk iznad veći od onog ispod. Rekurzivno rješenje problema hanojskih tornjeva. Neka je na štapu s1 ukupno n diskova, dok su ostali prazni. Nastojimo povezati rješenje za n diskova s rješenjem za n – 1 diskova, te uočiti dno rekurzije. Prebacivanje opisujemo nizom s1, s2, s3, tj. sa štapa s1 na s2 uz pomoć štapa s3. Pretpostavimo da je n – 1 diskova već prebačeno sa štapa s1 na pomoćni štap s3, uz pomoć s2, dakle uz redoslijed: s1, s3, s2. Sada je prema pravilima dozvoljeno najdonji i najveći disk prebaciti na dno štapa s2, gdje može i ostati. Još je preostalo prebaciti n – 1 diskova sa štapa s3 na s2 koristeći s1 kao pomoćni, dakle u redoslijedu s3, s2, s1. Rekurzivna funkcija koja ispisuje redoslijed prebacivanja diskova sa jednog štapa na drugi tada je: // Towers of Hanoi void hanoi(unsigned int n, char s1, char s2, char s3) { if(n > 0) { hanoi(n – 1, s1, s3, s2); // recursion call 1 cout << '\t' << s1 << " --> " << s2 // transfer << '\n'; hanoi(n – 1, s3, s2, s1); // recursion call 2 } }
Izvršavanje rekurzivnog algoritma. Primijetimo da ispisivanja neće biti sve dok rekurzija ne «udari» na dno, tj. dok n ne postane 0. Tada će algoritam izvršiti prvi povratak iz rekurzivnog poziva broj 1 i izvršiti prvi prijenos: sa s1 na s2 ukoliko je broj diskova neparan, odnosno sa s1 na s3 ukoliko je broj diskova paran. Prijenos je označen ispisom na ekranu: i --> j
što označava radnju: premjesti disk sa štapa i na štap j, i, j = 1, 2, 3. Potom će se izvršiti rekurzivni poziv broj 2. Pošto je tada n = 0 neće se dogoditi ništa doli povratka iz tog poziva. Zatim slijedi povratak iz prethodnog poziva 1 kod kojeg je bio n = 1, u funkciju kod kojeg je n = 2, i novi ispis. Uvijek nakon ispisa u našoj rekurzivnoj funkciji slijedi drugi rekurzivni poziv. Tak nakon povratka iz tog drugog poziva, dolazi do povratka iz funkcije u kojoj je izvršen ispis, itd. Dok je ispravna postavka rekurzije često jednostavna i elegantna, praćenje njenog izvršenja može biti vrlo kompleksno, pogotovo ako postoji više od jednog rekurzivnog poziva unutar funkcije kao u našem slučaju. Za detaljnu provjeru izvršenja rekurzije je potrebno u nekom konkretnom slučaju ispisati sve pozive i vrijednosti argumenata «na papiru». Zatim možemo metodom matematičke indukcije zaključiti da će za veći broj rekurzija također raditi točno (kao što smo mi već pokazali gore). Izvršenje funkcije hanoi(3, s1, s2, s3) za 3 diska dano je prikazom niže. Pozivi funkcije je označen s dva broja, tj. kao call k.l , gdje je prvi broj k razina poziva, odnosno «gniježđenja» funkcije (potprograma), a drugi broj l oznaka rekurzivnog poziva.* U našem slučaju l = 1 za rekurzivni po-
*
Kod većine naprednih programskih jezika dozvoljen je proizvoljan broj razina «gniježđenja potprograma unutar potprograma». Drugim riječima, unutar nekog potprograma na razini gniježđenja r ne postoji teorijsko ograničenje za to smije li se ponovo pozvati potprogram, i doći na razinu gniježđenja r +1. Ograničenje proizlazi
158
ziv prije ispisa, te l = 2 za rekurzivni poziv poslije ispisa. Kod svakog poziva označena je i vrijednost broja diskova n, te redoslijed prebacivanja u vidu uređene trojke simbola. Oznakom ret k.l označen je povratak iz funkcije koju smo pozvali s call k.l. Pažljivo proučite izvođenje ove rekurzivne funkcije. Zatim pokušajte sami rekonstruirati izvođenje funkcije za jedan, dva, te ponovo tri početna diska. // unutar pozivne funkcije (npr main()) call 1.1, n = 3, (s1, s2, s3); call 2.1, n = 2, (s1, s3, s2); call 3.1, n = 1, (s1, s2, s3); call 4.1, n = 0, (s1, s3, s2); ret 4.1; // unutar 3.1, ispis 3.1: "s1 --> s2"; call 4.2, n = 0, (s3, s2, s1); ret 4.2; ret 3.1; // unutar 2.1, ispis 2.1: "s1 --> s3"; call 3.2, n = 1, (s2, s3, s1); call 4.1, n = 0, (s2, s1, s3); ret 4.1; // unutar 3.2, ispis 3.2: "s2 --> s3"; call 4.2, n = 0, (s2, s1, s3); ret 4.2; ret 3.2; ret 2.1; // unutar 1.1, ispis 1.1: "s1 --> s2"; call 2.2, n = 2, (s3, s2, s1); call 3.1, n = 1, (s3, s1, s2); call 4.1, n = 0, (s3, s2, s1); ret 4.1; // unutar 3.1, ispis 3.1: "s3 --> s1"; call 4.2, n = 0, (s1, s3, s2) ret 4.2 ret 3.1 // unutar 2.2, ispis 2.2: "s3 na s2"; call 3.2,
n = 1, (s1, s2, s3);
jedino zbog sklopovskih razloga (veličina memorije), odnosno zbog parametara operacijskog sustava (dozvoljena veličina stoga). Međutim, jezici koji poziv potprograma nemaju riješen preko stoga (npr. FORTRAN) ne dozvoljavaju uporabu rekurzivnih poziva. Jezici kao što su Algol, Pascal i C/C++ organiziraju poziv potprograma u potpunosti preko stoga. To je računarski korektnije, elegantnije, i omogućava rekurzivne pozive. Prijenos argumenata između potprograma se tu obavlja preko stoga, jednako kao i pohrana informacija o pozivnoj funkciji (vidi dodatak C).
159 call 4.1, n = 0, (s1, s3, s2); ret 4.1; // unutar 3.2, ispis 3.2: "s1 na s2"; ret 3.2; ret 2.2; ret 1.1 // unutar pozivne funkcije (npr. main())
Iz oblika funkcije hanoi te iz gornjeg ispisa rekurzivnih poziva jasno je da poziv oblika hanoi(0, si, sj, sk), gdje je i, j, k = 1, 2, 3, ne radi ništa osim jednog suvišnog poziva i povratka iz funkcije. Stoga algoritam možemo učiniti racionalnijom i smanjiti «režijske troškove» zamjenom relativno zahtjevnog poziva funkcije selekcijom ostvarenom tvrdnjom if: // Towers of Hanoi void hanoi(unsigned int n, char s1, char s2, char s3) { if(n > 0) { if(n > 0) hanoi(n – 1, s1, s3, s2); // recursion call 1 cout << "\t" << s1 << " --> " << s2 // transfer << '\n'; if(n > 0) hanoi(n – 1, s3, s2, s1); // recursion call 2 } }
Radi potpunosti, dajemo glavni program koji će korisnika pitati za početni broj diskova na štapu 1, izvršiti ispis potrebnih prijenosa, te omogućiti ponovno izvršavanje programa proizvoljan broj puta: int main() { char c1 int iN; cout << << << cin >>
= '1', c2 = '2', c3 = '3'; "TORNJEVI HANOJA [za izlaz unesite: n < 0]\n" "==========================================\n" "Unesite broj diskova, n = "; iN;
while(iN >= 0) { cout << "==========================================\n" << "Prenesi disk\n" << "\t sa stapa --> na stap:" << endl; if(iN > 0) hanoi((unsigned int) iN, c1, c2, c3); else cout << "\tNema diskova za prenasanje!\n" << endl; cout << "==========================================\n\n" << endl; cout << "Tornjevi Hanoja [za izlaz unesite: n < 0]\n" << "==========================================\n" << "Unesite broj diskova, n = ";
160 } }
cin
>> iN;
return 0;
Zadatak 12.7 Testirajte gornji program te odgovorite: a) Koliko premještanja diskova ima redom, za početni broj diskova n = 1, 2, 3, 4, 5, … Što zaključujete, koliki je broj premještanja za općeniti broj diskova n? Nakon što ste izveli zaključak, odnosno «pogodili» rješenje, dokažite ga i matematički s pomoću matematičke indukcije. Napišite izraz za broj premještanja za n – 1 diskova, a zatim dokažite koliko premještanja još treba ako se pridoda 1 disk, tj. za ukupno n. b) Testirajte program za n = 10, 20, 21, 22, 23, 24, 25, … Što primjećujete? Je li to razumljivo s obzirom na odgovor pod a? c) Kolika je «dubina rekurzije» za funkciju hanoi(n, s1, s2, s3), tj. kolika je najdublja razina ugniježđenja u ovisnosti o broju diskova? Za koji n bi moglo doći do tzv. prepunjenja stoga (engl. stack overflow). Testirajte funkciju za n = 1000, 10000, 20000, pa zatim za 10500, 10030, … , naravno, ne čekajući da se ispis izvrši. d) Ukoliko ste točno odgovorili na pitanje pod a, tada znate da je za n diskova potrebno izvršiti točno f (n) = 2 − 1 premještanja. Ako bismo u svakom retku knjige napisali shemu za 2 premještanja, te ako prosječna knjiga ima na stranici 50 redaka, koliko stranica bi imala knjiga s ispisanim premještanjima na Hanojskim tornjevima uz početni broj diskova n = 20. e) Pronađite u literaturi i na Internetu podatke o ritualu Hanojskih tornjeva, te zaključite koji je bio cilj kreatora ove zadaće. n
Zapis, C-struct U računarstvu je česta potreba da se neke osobitosti opisuju nizom raznorodnih tipova podataka. Kad smo u našim dosadašnjim primjerima htjeli nekoj osobi pridijeliti neko obilježje, tada smo to radili uvodeći dva poretka. Prvo je bilo dvodimenzionalno i sadržavalo je imena osoba, a drugo je moglo biti nekog drugog tipa i sadržavati potrebno obilježje osobe, npr. ocjenu studenta, ili godinu rođenja, itd. Pritom su osoba i njeno obilježje bili vezani istim indeksom poretka, a pohranjeni u različitim podatkovnim strukturama, na različitim mjestima u memoriji, itd. Očito je da se radi o nezgrapnom i nezadovoljavajućem rješenju. Da bismo prirodne objekte adekvatno opisati u računarstvu se rabi temeljne podatkovna struktura zapis (engl. record), koju podržavaju mnogi viši programski jezici. U C jeziku za ostvarenje zapisa služi složeni podatkovni tip (struktura) pod nazivom struct.* Njegova općenita definicija je sljedeća: struct structName { memberType1 mVar1; // Member 1 memberType2 mVar2; // Member 2 ... ... memberTypen mVarN; // Member N } sVar = { valuemVar1, valuemVar2, ... ... , valuemVarN }
*
Naziv očito potiče od engl. riječi “structure” za strukturu podataka, tj. od puno općenitijeg pojma. Zato u tekstu često koristimo odrednicu C-struktura, ili C-struct, da označimo da se ne radi o općenitoj strutkuri podataka, već o konkretnoj implementaciji strukture koja se uobičajeno naziva zapis.
161
Iza ključne riječi struct kao odrednice složenog podatkovnog tipa, nalazi se ime strukture, a zatim se unutar vitičaste zagrade navode tzv. članske varijable ili kratko članovi (engl. members) proizvoljnog tipa. U gornjoj definiciji strukture odmah je deklarirana varijabla sVar tipa structName, sa članskim varijablama inicijaliziranim na vrijednosti navedenim u vitičastoj zagradi. Kao primjer, sljedeća C-struktura pod nazivom Student definira osnovne podatke o studentu nekog fakulteta: struct Student { char cArrMatN[10] ; char cArrImeIPrez[30] ; unsigned short int iGodRodj ;
// Jedinstvena matična oznaka studenta // Ime i prezime studenta // Godina rođenja studenta
} stud1 = { “007-R2004”, “Ivo Ivic”, 1990}
Ujedno je stvorena i nova varijabla tipa Student pod imenom stud1, kojoj su odmah inicijalizirane vrijednosti pojedinih članskih varijabli. Nove varijable ovog tipa možemo kreirati uz istovremenu inicijalizaciju (usporedite s inicijalizacijom poretka u pogl. 6) po sljedećem obrascu: struct Student stud2 = {“008-R2004”, “Iva Ivić”, 1984}; Student stud3 = {“010-R2004”, “Pero Perić”, 1984};
// C stil // C++ stil
Pojedinom članu zapisa pristupamo navodeći ime konkretne varijable, te iza točke ime člana Cstrukture: sVar.mVarI
// I = 1, 2, ... ... , N
Ako imamo definiranu kazaljku na C-strukturu te je želimo dereferencirati, vrijedi sljedeća sintaksa: struct structName* pStructName = new structName ; (*pStructName).mVarI = valuemVarI;
gdje se (*pStructName).mVarI pojednostavljeno piše kao pStructName->mVarI. Dakle, pridjeljivanje vrijednosti članskoj varijabli mVar1 uobičajeno bismo zapisali na sljedeći način: pStructName->mVarI = valuemVarI ;
// I = 1, 2, ... ... , N
Ili, u našem konkretnom primjeru: struct Student pStud = new Student ; pStud->iGodRodj = 1985 ;
// u C++ možemo ispustiti ključnu riječ struct
Korisnički kreirani tip podataka najčešće želimo koristiti u više funkcija nekog programa. U tom slučaju ga deklariramo kao globalnog. Isto vrijedi i za C-strukture.
162
Poglavlje 13. Dodatni zadaci Cilj ove vježbe je samostalna izrada programa, koristeći dosada izneseno gradivo. Obavezno je koristiti čim više mogućnosti koje pruža jezik C/C++.
Naputci za programiranje Navedimo neke načelne korake koje je dobro slijediti prilikom programiranja: 2. Dobro razumijevanje zadatka, njegova razrada glede potrebnih podatkovnih tipova, te precizna i čim elegantnija (sređenija) matematička formulacija potrebnih izračuna. 3. Podjela problema na manje cjeline prema načelu podijeli pa vladaj (lat. divide et impera). Podjela se može ostvariti: i) od vrha prema dnu (engl. top-down), gdje se cjelina razbija na jednostavnije dijelove dok se ne dođe do relativno malih i jednostavnih zadataka; ii) od dna prema gore (engl. bottom-up), gdje se rješenja manjih cjelina kombiniraju za ostvarenje kompleksnog zadatka; ili iii) mješovitim pristupom koji kombinira prethodna dva. Kod jednostavnijih problema najčešće uočavamo unos podataka od strane korisnika (ako je potreban), obradu podataka , te ispis ili drugi način prenošenja rezultata (vidi npr. funkcije, vježba 6). 4. Kodiranje pojedinih dijelova programa razlučenih u točki 2. Započinje se objavom i inicijalizacijom potrebnih varijabli, i struktura podataka. te iznalaženjem pogodnih algoritama i programskih struktura za rješavanje pojedinih dijelova. 5. Pisanje pojedinih dijelova programa u radnoj okolini Visual C++, uz provjeru sintakse povremenom kompilacijom. 6. Provjera rezultata i ispisa, uz eventualno korištenje otkrivača pogrešaka (engl. debugger). Pri tom je potrebno koristiti nezavisna računalna pomagala, npr. kalkulatore. 7. Testiranje robustnosti programa, unašanje problematičnih i «rubnih» vrijednosti, razmatranje slabih točaka programa. 8. Ako je u bilo kojem koraku uočen nepremostiv problem ili pogreška, potrebno je vratiti se na ranije točke. 9. Tijekom cijelog programiranja treba koristiti sve pogodnosti koje pruža programski jezik, programska okolina, te nastojati pisati čim elegantniji, kraći, brži i pregledniji programski kod.
Zadaci Zadatak 13.1 Potrebno je objaviti jednodimenzionalni poredak elemenata xi tipa float, veličine 100. Od korisnika se traži unos broja n (prikazati nepredznačenom 16-bitnom cjelobrojnom varijablom uN), za koji mora vrijediti n ≤ 100. Zatim se vrši unos n = uN podataka tipa float u poredak. Program treba izračunati aritmetičku sredinu x i varijancu σ
1 n x = ∑ xi , n i =1
2
unesenog niza podataka prema formulama:
∑ =
n
σ
2
i =1
( x − xi ) 2
n −1
;
163
(varijancu označenu kvadratom grčkog slova «sigma» označite u programu na prikladan način). Kvadrat izračunavajte preko množenja. Program ispisuje broj n podataka, njihovu srednju vrijednost i varijancu. Zadatak 13.2* Dodajte u gornjem zadatku i izračun standardne devijacije σ . Kvadratni korijen izračunajte preko C funkcije sqrt() ( double sqrt(double dX) ), deklarirane u zaglavnoj datoteci math.h. Pošto funkcija sqrt prima argumente tipa double, i vraća isti tip, obratite pažnju na potrebno pridjeljivanje tipova, tj. nastojte izbjeći upozorenje prevodioca. Naputak: da se izračuna korijen varijable deklarirane se double dX i pospremi u tu istu varijablu, dovoljno je pozvati funkciju sqrt na sljedeći način: dX = sqrt(dX). Zadatak 13.3 Potrebno je napraviti program koji omogućava izračun i pohranu srednje ocjene od m predmeta za n učenika. Ocjene su pohranjene u "retke" 2-dim poretka tipa unsigned short int (neka maksimalne dimenzije poretka budu određene konstantama cN ≤ 100 i cM ≤ 20). Studenti su, radi jednostavnosti, identificirani prvim indeksom poretka. Brojevi n i m su inicijalno postavljeni na maksimalne vrijednosti, tj. n = cN, i m = cM – 1, jer u zadnjem elementu svakog retka (tj. u zadnjoj "koloni tablice"), želimo ostaviti mjesto za srednju ocjenu, ispravno zaokruženu na cijeli broj. Program sadrži i 1-dim poredak tipa float (naravno, dimenzije jednake broju redaka 2-dim poretka) sa srednjim ocjenama ispravno zaokruženim na dvije decimale. Program ima izbornik koji nudi sljedeće radnje: Unos broja studenata n ( n ≤ cN ) i broja predmeta m ( m ≤ cM – 1 ). Unos ocjena iz svih pojedinih predmeta redom, za svakog učenika redom. Pri upisu, korisnik mora vidjeti za koji indeks studenta, te za koji indeks predmeta vrši unos. Ispis ocjena za sve studente redom, i to: srednje ocjene na dvije decimale, i na cijeli broj ispravno zaokružene konačne ocjene. Ispis je u obliku tablice sa zaglavljem i tri kolone: prva kolona je indeks studenta, a druge dvije su navedene konačne ocjene. Izlazak iz programa. Izbornik se ostvaruje selekcijom tipa switch. Unosom broja od 1 do 4 korisnik bira željenu radnju. Napomena: razmislite kako ostvariti ispravno zaokruživanje brojeva! Ako nemate ideje, razmotrite sljedeću formulu za varijablu x deklariranu tipom float: x k-dec = (float) ( (int) ( 10k × x + 0.5 )) / 10k . Najprije razmotrite rezultate uz k = 0, i pokušajte objasniti formulu. Koja je uloga prilagodbe na tip int u unutarnjoj zagradi? Vrijedi li formula općenito, tj. i za k < 0 ? Zadatak 13.4* Učinite gornji program potpunijim uvođenjem 1-dim znakovnog poretka za imena studenata. Dodajte upis imena pod točku 2 gornjeg izbornika, te ispis imena u prvoj koloni ispisa (iza indeksa). Učinite gornji program robusnijim, uvođenjem ulaznog filtra za ocjene. Zadatak 13.5 Napišite program koji nenegativni cijeli broj ispisuje na ekran u formi pozicionog brojevnog sustav s bazom b = 16 (heksadekadski sustav). Podsjetite se najprije algoritma za pretvorbu brojeva u heksadekadski sustav na papiru. U programu koristite posebnu varijablu za bazu sustava, tj. nastojte osigurati općenitost programa za sve sustave s bazom b = 2, 3, ... ... , 16. Ako ste ispravno
164
postavili algoritam, on će biti valjan za proizvoljnu bazu. Nakon što ste napravili algoritam, dodajte unos broja sa tastature, unos željenog baze i ispis broja u pozicionom sustavu date baze. Zadatak 13.6 Potrebno je napraviti program koji na ekranu ispisuje tablicu znakova proširenog ASCII koda (vidi dodatak B). Pošto je svaki znak ASCII kodiran s 8-bita, najelegantnije je tu kodnu zamjenu prikazati s dvije heksadekadske znamenke. Više značajna (prva) heksadekadska znamenka kodnih zamjena bit će zapisana u početnoj koloni tablice, a manje značajna u zaglavlju tablice. Program najprije ispisuje zaglavlje tablice, a zatim sve redove tablice, prema dolje navedenom primjeru (prikazano je samo nekoliko znakova radi ilustracije). Da se ispišu znamenke koje označavaju redak u heksadekadskoj formi, na ostream objekt cout pošaljite hex manipulator na sljedeći način: cout << hex ;
Sav ispis cjelobrojnih tipova poslije ove tvrdnje će biti u formi heksadekadskog preslika memorije. Na isti način manipulator oct služi za postavljanje oktalne baze za ispis. Želimo li vratiti bazu sustava na dekadsku (koja je podrazumijevajuća), na cout pošaljemo manipulator dec. Nakon što ste napravili bitni dio programa, popravite ispis prema sljedećim naputcima. U prvom redu ne smiju se ispisati znakovi s kodnim zamjenama od 0Ah do 0Fh , jer neki među njima (kao npr. ASCII('\n') = 0Ah za prijelaz u novi red) služe za kontrolu ispisa i kvare izgled tablice. Radi preglednosti, nakon prva dva reda (koji uključuju kontrolne znakove s kodnim zamjenama od 00h do 1Fh ), te nakon osmog reda (nakon reda s vodećom znamenkom 7), tj. nakon 0-te stranice ASCII koda, potrebno je napraviti red razmaka. 0x | 0 1 2 3 4 5 6 7 8 9 a b c d e f ===================================================== 0 1
2 3
!
"
0
1
2
p
q
r
… … 7
8 … … f
165 =====================================================
Zadatak 13.7 Kreirajte strukturirani C++ program koji vrši sljedeću obradu: 1) Unos podataka u dva dinamička jednodimenzionalna poretka veličine n. Jedan poredak je znakovni: char cSpol[n]; i sadrži spol studenata, a drugi poredak je realni: float fProsOcj[n]; i sadrži prosječne ocjene studenata. 2) Određivanje srednje prosječne ocjene studenata. 3) Određivanje minimalne i maksimalne prosječne ocjene u skupu prosječnih ocjena 4) Ispis minimalne i maksimalne prosječne ocjene, te odgovarajućeg spola, u sljedećem obliku: ==================================================== Prosječna ocjena, minimalna: 2.35 Spol M Prosječna ocjena, maksimalna: 4.25 Spol Z ==================================================== Prosječna ocjena, usrednjena: 3.20
Sve točke obrade od 1 do 4 potrebno je strukturirati kao funkcije s parametrima. Zadatak 13.8 Potrebno je izraditi strukturirani C++ program za obradu ocjena na gimnastičkom natjecanju. U glavnom programu potrebno je dinamički alocirati dva poretka: • za ime i prezime natjecatelja — dvodimenzionalni poedak tipa char dimenzija [n][25]; • za prosječnu ocjena natjecatelja — jednodimenzionalni poredak tipa float, dimenzije [n]. Potrebno je ostvariti sljedeće: 1. Unos podataka u zadane poretke: 1.1 unijeti ime i prezime natjecatelja u zadano znakovni poredak; 1.2 unijeti pet sudačkih ocjena kao podatke realnog tipa, te njihovu prosječnu ocjenu upisati u zadani poredak; 2. Sortirati natjecatelje po izračunatom prosjeku; 3. Ispisati rang listu natjecatelja, prema sljedećem obrascu: RANG LISTA NATJECATELJA 1. Ime 2. Ime 3. Ime ... ... n. Ime
i prezime: prosječna–ocjena i prezime: prosječna-ocjena i prezime: prosječna-ocjena ... ... ... ... i prezime: prosječna-ocjena
Unos podataka treba ostvariti s pomoću zasebne funkcije vUnos tipa void, preko odgovarajućih argumentata.
166
Dodatak A. Potencije baze 2, računarski prefiksi. Računarski prefiksi (faktori). Poznavanje definicije računarskih prefiksa te njihovih točnih (za 1K) i približnih vrijednosti (za 1M, 1G, i 1T) predstavlja temelj računarskog znanja. Tu spada i poznavanje potencija baze 2n od 20 do 210. Računarski prefiksi su neimenovani faktori (brojevi) jednaki cjelobrojnim nenegativnim potencijama od 210 = 1K. Oni su redom: 1K = 210 , 1M = (210 )2 = 220 , 1G = (210 )3 = 230 , 1T = (210 )4 = 240 . Njihova imena dana su u tablici A.1. Primijetimo da je 1K = 1024 ≈ 103 , što odgovara SI* prefiksu 1k istog imena ali s kraticom pisanom malim slovom. Očito je da će cjelobrojne potencije od 1K biti približno jednake cjelobrojnim potencijama od 103 , tj. SI prefiksima (vidi tablicu A.1). Za računarske prefikse 1M, 1G i ostale, kratice i nazivi su isti kao i za SI prefikse. Pošto je njihovo značenje samo približno jednako, iz konteksta se mora uočiti o kojem se prefiksu radi. U načelu se uz računarske veličine kao što su broj memorijskih lokacija (kapacitet), veličina naslovnog prostora rabe računarske kratice, a uz fizikalne veličine kao što su dimenzija, vrijeme, frekvencija, itd., SI kratice. U području računarskih mreža, za opis njihovog kapaciteta i brzine prijenosa najčešće se koriste SI prefiksi. U tom smislu kratica kbps (engl. kilo bit per second) ima uobičajeno značenje 1kbps = = k bit / s = 103 bit/s . Tek u iznimnim, odnosno krivo rabljenim slučajevima, njeno značenje je 1024 bit/s. Iz gornjeg izlaganja jasno je da bi u tom slučaju ispravno označena kratica trebala biti Kbps. Slično vrijedi i za ostale kratice u ovom području. Dobri autori će naglasiti točnu vrijednost kratica koje rabe. Ujedno, uočimo da su kratice kao kbps neusklađene sa SI sustavom, i stoga ih je najbolje izbjegavati, te koristiti već napisane kratice: k bit / s = 103 bit/s = 103 bit s-1 . U njima je za oznaku bita uporabljena korektna kratica bit , a dimenzija vremena (T-1) je izražena matematički eksplicitno pisanjem s-1 . Heksadekadski prikaz binarnog sadržaja. Zbog dualnog karaktera digitalnih sklopova, najbolje je za prikaz brojeva odabrati binarni brojevni sustav (baza b = 2) i binarnu aritmetiku. Za prikaz binarnog sadržaja, npr. sadržaja memorije, registara, vrijednosti binarnih adresa, ljudima je praktičnije i preglednije rabiti sustave koji dobro korespondiraju s binarnim. To su sustavi čija je baza cjelobrojna potencija baze 2, a u uporabi su oktalni sustav (b = 8), te heksadekadski sustav (b = 16) kao današnji računarski standard.† U tom smislu, prikažemo li stvarni binarni sadržaj u heksadekadskoj formi, govorimo o njegovom heksadekadskom ekvivalentu. Kao primjer, binarni sadržaj 32-bitnog registra: R = 0000 0001 0010 0011 1010 1011 1100 1101b , za ljudsku uporabu preglednije je prikazati u formi heksadekadskog ekvivalenta: R = 0123 ABCD h . On se dobiva izravno pretvorbom četvorki bitova u odgovarajuću heksadekadsku znamenku, pa umjesto 32 bita imamo točno 8 heksadekadskih znamenaka. Pritom je običaj pisanja vodećih nula da se
*
SI = engl. System International, internacionalni sustav jedinica temeljen na metričkom sustavu (m, kg. s) i dekadskom brojevnom sustavu, koji propisuje i nazive SI kratica. Npr. da = 10, h = 102 , k = 103 , M = 106 , G = 109 , itd.. †
Ponovite pravila za pretvorbu iz binarnog sustava u oktalni i heksadekadski sustav, te obrnuto, i usporedite ih s pravilima za konverziju između binarnog i dekadskog sustava. Objasnite na temelju toga zašto kažemo da su binarni i oktalni, te binarni i heksadekadski sustav korespondentni, a binarni i dekadski nisu. Obrazložite što je uzrok tome.
167
odrazi broj i stanje svih bitova. Primijetimo istovremeno da za prikaz istog sadržaja u dekadskoj formi: R = 19 114 957d , moramo izvršiti punu konverziju, da znamenke jednog i drugog sustava međusobno ne korespondiraju, da dekadskih znamenaka može biti do 10, itd. Prikaz cjelobrojnih potencija baze 2 u tablici A.1 također potvrđuje jednostavnost i eleganciju njihovog zapisa u heksadekadskom sustavu.
168
Tablica A.1. Potencije baze 2 i računarski prefiksi. Dane su cjelobrojne nenegativne potencije 2n za n = 0, 1, 2, … , 50, izražene u i) dekadskoj formi, točno i približno, te u ii) računarskoj formi: binarno, heksadekadski i s pomoću računarskih prefiksa. Uočite da su računarski prefiksi neimenovani faktori (brojevi) definirani preko cjelobrojnih nenegativnih potencija od 210 : 1K = 210 , 1M = (210 )2 = 220 , 1G = (210 )3 = 230 , 1T = (210 )4 = 240 , itd.
n
2n
0
Dekadski, b = 10 Točno Približno 1 ≈1
Binarno b=2
Heksadekadski b = 16
1
2
10
2
2
4
100
4
3
8
1000
8
4
16
1 0000
10
5
32
10 0000
20
1
1
6
64
100 0000
40
7
128
1000 0000
80
8
256
1 0000 0000
100
9
512
10
1 024
11
≈ 103
2 048
Računarski prefiks
10 0000 0000
200
100 0000 0000 = 2
10
400
1K = Kilo
2
11
800
2K
12
1000
4K
2000
8K
12
4 096
2
13
8 192
213
14
16 384
2
14
4000
16K
15
32 768
1215
8000
32K
65 536
10
16
1 0000
64K
10
20
16 20
1 048 576
10 0000
1M = Mega
21
2 097 152
1021
20 0000
2M
22
4 194 304
10
22
40 0000
4M
23
8 388 608
1023
80 0000
8M
16 777 216
10
24
100 0000
16M
10
30
24
≈ 10
6
30
1 073 741 824
4000 0000
1G = Giga
31
2 147 483 648
1031
8000 0000
2G
4 294 967 296
10
32
1 0000 0000
4G
10
40
100 0000 0000
1T = Tera
1050
4 0000 0000 0000
1P = Peta
32 40
1 099 511 627 776
50
1 125 899 906 842 624
≈ 10
9
≈ 10
12
≈ 1015
169
Dodatak B. ASCII kod. Izvorni ASCII kôd (engl. American Standard Code for Information Interchange) je 7-bitni znakovni kôd. Sa 7 bitova možemo kodirati 27 = 128 znakova, tj. svakom znaku pripada jedna kodna zamjena ili riječ (engl. code word) od 7 bitova: 000 0000, 000 0001, 000 0010, 000 0011, 000 0100, … … … , 111 1111. U 128 kodnih riječi smješteno je svih 26 velikih i 26 malih slova latinske (engleske) abecede A = = {A, B, C, D, … … , X, Y, Z}, potrebni znakovi interpunkcije, znamenke dekadskog brojevnog sustava, znakovi interpunkcije, veliki broj drugih tekstualnih i matematičkih znakova, te posebnih znakova. Zbog smještaja kodnih zamjena osnovnog ASCII koda u 1B memorije, uveden je prošireni ASCII kôd koji u sebi uključuje izvorni, a dodaje i još 128 novih kodnih zamjena. Tako kod proširenog ASCII koda govorimo o tzv. 0-toj stranici kod koje kodne zamjene započinju vodećim bitom 0, i koja predstavlja izvorni ASCII kôd. Pored nje postoji i 1-va stranica, s kodnim zamjenama koje započinju vodećim bitom 1, i koja predstavlja proširenje ASCII koda: 0-ta stranica: 0000 0000, 0000 0001, 0000 0010, 0000 0011, … … … , 0111 1110, 0111 1111; 1-va stranica: 1000 0000, 1000 0001, 1000 0010, 1000 0011, … … … , 1111 1110, 1111 1111. U računarstvu se ekvivalenti binarnog sadržaja memorije, iznosa adresa, pa i kodnih riječi, standardno prikazuju u heksadekadskom obliku, zbog jednostavne korespondencije između ova dva sustava.* Očito je da će nam za prikaz 8-bitnih kodnih zamjena biti dovoljne dvije heksadekadske znamenke: b7 b6 b5 b4 b3 b2 b1 b0 = h1 h0 , gdje znamenka h1 pokriva više značajnih 4 bita, odnosno značajniju (gornju) polovicu bajta (engl. nibble), dok h0 pokriva manje značajnih 4 bita, odnosno donju polovicu bajta. Odavde slijedi da je prikaz 0-te i 1-ve stranice ASCII koda u heksadekadskom obliku sljedeći: 0-ta stranica: 00, 01, 02, 03, … … … , 7E, 7F; 1-va stranica: 80, 81, 82, 83, … … … , FE, FF. Već smo u pogl. 2 istaknuli neke karakteristične kodne riječi ASCII koda. Npr. tekstualni znakovi započinju sa znakom praznine ASCII(' ') = 20h , dekadske znamenke započinju od 30h , i završavaju na 39h : ASCII('0') = 30h , ASCII('1') = 31h , … , ASCII('9') = 39h . Velika latinska slova započinju na ASCII('A') = 41h , a mala na ASCII('0') = 61h . Dakle, za 26 slova latinske abecede kodne zamjene imaju konstantan razmak od 20h = 32d . Općenito je ASCII kôd uređen s obzirom na heksadekadski prikaz kodnih riječi (proučite tablicu B.1). S druge strane, prikaz kodnih zamjena u dekadskoj formi je nezgrapan, potrebne su čak do tri dekadske znamenke (i za 0-tu stranicu), a princip uređenja znakova unutar koda je nejasan.
*
Prisjetite se pravila o pretvorbi između binarnog u heksadekadski sustava i obrnuto. Ukratko, svaka «četvorka bitova» 0000, 0001, 0010, 0011, … … , 1110, 1111, nastala njihovim grupiranjem polazeći od zamišljene ili postojeće racionalne točke u lijevo i desno, predstavlja se jednom heksadekadskom znamenkom. Analogno, jedna heksadekadska znamenka 0, 1, 2, 3, … … , E, F predstavlja četiri bita. Uobičajeno se u računarstvu pišu i vodeće nule da se točno odrazi broj raspoloživih bitova.
170
Tablica B.1. Izvorni ASCII kôd (ASCII ISO-7). Kodna zamjena ASCII('c') za znak 'c' dana je s dvije heksadekadske znamenke: ASCII('c') = h1 h0 , h1 = 0, 1, 2, … , 7. U prvoj koloni tablice je značajnija heksadekadska znamenka h1 , a u gornjem retku (zaglavlju) tablice manje značajna znamenka h0 kodne zamjene. Tako npr. znak razmaka ' ' (engl. space, blank), označen sa SP ima kodnu zamjenu: ASCII(SP) = ASCII(' ') = 20h . Znak DEL ima u većini operacijskih sustava (MS-DOS, MS Windows) kontrolnu ulogu jednaku znaku BS (Back Space), a možemo ga generirati na tastaturi pritiskom na CTRL+BKSP.
h0 h1
0
1
2
3
4
5
6
8
9
A
B
C
D
E
F
0
NUL SOH STX ETX EOT ENQ ACK BEL BS
HT
LF
VT
FF
CR
SO
SI
1
DLE DC1 DC2 DC3 DC4 NAK SYN ETB CAN EM SUB ESC FS
GS
RS
US
2
SP
!
"
#
$
%
&
'
(
)
*
+
,
-
.
/
3
0
1
2
3
4
5
6
7
8
9
:
;
<
=
>
?
4
@
A
B
C
D
E
F
G
H
I
J
K
L
M
N
O
5
P
Q
R
S
T
U
V
W
X
Y
Z
[
\
]
^
_
6
`
a
b
c
d
e
f
g
h
i
j
k
l
m
N
O
7
p
q
r
s
t
u
v
w
x
y
z
{
|
}
~
DEL
Tablica B.2 Znakovi proširenog ASCII koda. (Sa zahvalnošću preuzeto iz: www.XxXXx ) a) Ispis znakova na ekranu terminala, ili emulatora konzolnih aplikacija.
b) Ispis iz MS Windows© grafičkog okruženja.
7
171
Tablica B.3 Kontrolni ASCII znakovi. Značenje kontrolnih znakova ASCII koda. Kodne zamjene od ASCII(NUL) = 00h do ASCII(US) = 1Fh . ASCII hex
name
00
NUL
01
Control function
ASCII
Control function
Hex
name
Null = '\0'
10
DLE
Data Link Escape
SOH
Start Of Heading
11
DC1
Device Control 1
02
STX
Start Of Text
12
DC2
Device Control 2
03
ETX
End Of Text
13
DC3
Device Control 3
04
EOT
End Of Transmission (Not the same as ETB)
14
DC4
Device Control 4
05
ENQ
Enquiry
15
NAK
Negative Acknowledge
06
ACK
Acknowledge
16
SYN
Synchronous Idle
07
BEL
17
ETB
End Of Transmission Block (Not the same as EOT)
08
BS
18
CAN
Cancel
19
EM
End Of Medium
moves the cursor (or print head) to a new line. On Unix and in C moves to athe beginning of the new line.
1A
SUB
Substitute
Vertical Tabulator
1B
ESC
Escape
1C
FS
File Separator
1D
GS
Group Separator
1E
RS
Record Separator
1F
US
Unit Separator
Bell – caused teletype machines to ring a bell. Causes a beep in common termiminals and terminal-emulation programs.
Backspace – moves the cursor (printer head) backwards (left) one space.
Horizontal Tab = '\t' – moves the cur-
09
HT
0A
LF
0B
VT
0C
FF
0D
CR
Carriage Return – moves the cursor all
0E
SO
Shift Out – switches output device to
0F
SI
Shift In – switches output device back to
sor (print head) right to the next tab stop (usually 8 or 10 spaces).
NL = New Line, Line Feed = '\n' –
Form Feed –advances paper to the top of the next page – on printer only.
the way to the left. No advance to the next line. alternate character set.
the default character set.
172
Hrvatski znakovi u konzolnim aplikacijama. Posebni hrvatski znakovi (tzv. dijakritički) smještaju se, ovisno o rješenju, na mjesta različitih kodnih zamjena ASCII koda. Do pojave grafičkih operacijskih sustava hrvatski znakovi smještali su u tablicu izvornog ASCII koda, čime su bili izgubljeni neki izvorni znakovi. Npr. na mjesto «naopačke kose crte» (engl. back-slash) ASCII('\') = 5C h , poznate oznake za stazu datoteke, bilo je stavljeno slovo Đ, na mjesto ']' je bilo stavljeno Ć, itd (vidi tablicu B.4a. Prednost ovog rješenja je bila ta da su sva velika hrvatska slova bila u rasponu kodnih riječi od 40h do 5Eh , a mala od 60h do 7Eh , dakle zadržan je standardni razmak između velikih i malih slova od 20h . Mana je bila ta da je izgubljeno mnoštvo znakova, npr. uglate i vitičaste zagrade, znak '@', itd. U konzolnim aplikacijama grafičkih operativnih sustava (kao npr. MS Windows), hrvatski dijakritički znakovi preseljeni su iz 0-te u 1-vu stranicu ASCII koda. Prednost je što su na taj način oslobođene kodne zamjene za važne znakove iz prve stranice, i što se ti znakovi sada mogu normalno rabiti. Mana je ta što velika i mala hrvatska slova više nisu u jedinstvenom nizu, i što za dijakritička velika i mala slova ne vrijedi da je razmak kodnih zamjena standardan (vidi tablicu B.4b). Stoga pretvorbu velikih dijakritičkih slova u mala ili obrnuto, treba ostvariti zasebno od pretvorbe standardnih slova latinske abecede. Tablica B.4a Hrvatski dijakritički znakovi u konzolnim aplikacijama MS-DOS operacijskog sustava (modificirani ASCII ISO-7). U zaglavlju tablice dana je heksadekadska vrijednost ASCIIh (znak) kodne riječi, a u tijelu tablice njena interpretacija kao hrvatskog znaka, odnosno izvornog znaka nulte stranice ASCII koda (00h do 7Fh). Usporediti s tablicom B.1. ASCIIh (znak)
5E
5D
5C
5B
40
Hrvatski znak
Č
Ć
Đ
Š
Ž
Izvorni znak
^
]
\
[
@
ASCIIh (znak)
7E
7D
7C
7B
60
Hrvatski znak
č
ć
đ
š
ž
Izvorni znak
~
}
|
{
`
Tablica B.4b Hrvatski dijakritički znakovi u konzolnim aplikacijama grafičkih operacijskih sustava (MS Windows). U zaglavlju tablice dana je heksadekadska vrijednost ASCIIh (znak) kodne riječi, a u tijelu tablice njena interpretacija kao hrvatskog znaka, odnosno izvornog znaka prve stranice ASCII koda (80h do FFh). Usporediti s tablicom B.2a. ASCIIh (znak)
AC
8F
D1
E6
A6
Hrvatski znak
Č
Ć
Đ
Š
Ž
Izvorni znak
¼
Å
╤
μ
a
ASCIIh (znak)
9F
86
D0
E7
A7
Hrvatski znak
Č
ć
đ
š
Ž
Izvorni znak
ƒ
å
╨
τ
o
173
Dodatak C. Poravnanje podataka u memoriji Poravnanje podataka u memoriji. Pojam memorijskog poravnanja podataka (engl. memory data alignement) odnosi se na smještaj podataka tipa T i duljine t memorijskih zrna (bajtova) na memorijsku adresu A(t), gdje je t cijeli, nenegativan, broj. Po definiciji, podatak je poravnat ako je smješten na adresu koja je djeljiva s duljinom podatka t . Matematički to možemo formulirati na sljedeći način:
A(t )align (mod t ) = 0 ,
t ∈ N 0+ .
Ovdje smo s A(t)align označili poravnatu adresu nekog tipa duljine t, a s (mod t ) operaciju modulo t , tj. nalaženje ostatka cjelobrojnog djeljenja brojem t. Ako neka adresa A nije poravnata za tip duljine t, tada će za nju gornja operacija modulo dati neku vrijednost r različitu od 0 i manju od t :
A (mod t ) = r ,
0 < r ≤t .
Oduzmemo li od početne adrese A(t) broj r, novodobivena adresa je poravnata, pa možemo pisati sljedeću formulu:
A(t )align = A − A (mod t ) , t ∈ N 0+ . Ova je formula općenita i vrijedi uvijek, čak i kad je početna vrijednost A(t) poravnata, jer je u tom slučaju rezultat operacije modulo 0. Koncept poravanja podatka vrlo je lako ilustirati na primjerima. Poznato je da su duljine tipova podataka na današnjim računalnim platformama uglavnom sljedeće: t = 1 (tip char), t = 2 (short int), t = 4 (int = long int, i float), t = 8 (long long int = long long, i double). Smjestimo li npr. tip duljine t = 4 na memorijsku adresu A1 = 1234 5678, on je očito poravnat, jer je navedena adresa djeljiva s 4. U heksadeakdskom sustavu su svi brojevi koji okončavaju znamenkama 0, 4, 8 i C uvijek djeljivi s 4, jer su te znamenke djeljive s 4, a i baza sustava je djeljiva s 4. Isti taj tip smješten na adresu A2 = 1234 567B nije poravnat jer navedena adresa nakon dijeljenja s 4 daje ostatak r = 3. Prva niža poravnata adresa je za 3 manja, tj. uprvo je jednaka adresi A1 . Iz navedenog je jasno da su podaci tipa char uvijek poravnati, jer je svaka adresa djeljiva s 1 bez ostatka. Podaci duljine t = 2 (short int) su poravnati ako su smješteni na parne adrese, podaci duljine t = 4 , kao što je već rečeno, ako su im adrese djeljive s 4, a podaci duljine t = 8 ako su im adrese djeljive s 8, odnosno ako adresa okončava brojem 0 ili 8 (vidi tablicu 7.1). Primijetimo da su duljine tipova t/B upravo potencije baze 2 (20 = 1, 21 = 2, … 24 = 16). Otuda slijedi ova jednostavna korespondencija između djeljivosti adresa prikazanih u heksadekadskom sustavu s bazom Bh = 16 = 24, i duljina tipova.
174
Poravnate adrese
Duljina tipa t / bit
Duljina tipa t/B
Standardni tipovi na većini platformi
Djeljive s:
8
1
Char
1
sve: 0, 1, 2, ... ... , F
16
2
short int
2
0, 2, 4, 6, 8, A, C, E
32
4
int = long int float
4
0,
64
8
long long int = long long double
8
0,
128
16
int_128 (MS Visual Studio)
16
0
Zadnje znamenke:
4,
8,
C
8
Tablica 7.1 Poravnate adrese za standardne tipove jezika C / C++. Tipovi podataka su razvsrtani po duljini. Podatak, odnosno adresa na koju je smješten, je poravnat, ako je adresa djeljiva s duljinom tipa izraženom brojem bajtova. U zadnjoj koloni dane su zadnje znamenke heksadekadskih adresa za koje su dotični tipovi poravnati.
Poravnatost podataka je vrlo bitna zbog načina rada procesora i memorijskog sustava. Podaci se dohvaćaju iz glavne, odnosno priručne memorije u sabirničkim ciklusima. Da bi se optimalno iskoristila propusnost podatkovnih sabirnica, standardno se u sabirničkim ciklusima čitanja i pisanja prenosi upravo toliko bitova kolika je širina adresne sabirnice. Možemo zamišljati da je na 32-bitnim procesorima to upravo 32 uzastopna bita. Pri tom, zbog organizacije cijelog memorijskog sustava, to nisu bilo koja 32 bita, već upravo 4 uzastopna bajta od kojih je prvi na poravnatoj adresi. Dakle, situacija jednaka onoj koju bismo ostvarili kad bismo poravnali podatak duljine t = 4B. S druge strane, pretpostavimo da je neki od podataka duljine t = 4B neporavnat, za njegovo će dohvaćanje biti potrebno organizirati dva sabirnička ciklusa, što je, sa stanovišta vremena izvođenja, dvostruko u odnosu na poravnati podatak. Isto će vrijediti i za sve dulje tipove. Poravnatost podataka u zbirnom jeziku morao je osigurati sam programer. U višim programskim jezicima poravnatost podataka osiguravaju prevodioci. [Povezati s primjerom 7.2] Pretpostavimo da je prva objavljena varijabla tipa char. Adresa dna stoga je uobičajeno neka okrugla adresa, najčešće poravnata do na duljinu t = = 16, dakle okončana znamenkom 0. Kompilator će za pohranu varijable tipa char umanjiti adresu dna stoga za t = 1, i na nju pohranit znakovni podatak. U skladu s prethodnim primjerom, to bi bilo na adresi A(char) = 0012 FF6F (vidi sl. 7.3). Ukoliko se kao sljedeća objavi neka varijabla duljine t = 4B, npr. tipa int, tada je adresa za 4 manja od prethodne A(int) = 0012 FF6B, neporavnata. Kompilator odredi da je rezultat operacije:
A (mod 4) = 0012 FF6B = 3 ,
pa od početne adrese oduzme 3 da dobije poravnatu adresu:
A(4)align = 0012FF6B − A (modt ) = 0012FF6B − 0000 0003 = 0012 FF68 .
Spomenimo usput da je ova operacija u binarnom sustavu trivijalna: jednostavno se posljednja dva bita adrese izjednače s nulom ( Fh = 1111b , Ch = 1100b ). Dakle, nova varijabla ide na poravnatu adresu A(int) = 0012 FF68, i popunjava 4 bajta redom, od A(int) = 0012 FF68 do A(int+3) = 0012 FF6B . Zbog potrebe poravnanja, 3 bajta s adresama A(char−1) = 0012 FF6E do A(char−3) = 0012 FF6C ostala su nepopunjena.
175
Da ilustriramo poravnanje podataka, proširit ćemo primjer 7.2 (pogl. 7) uvođenjem tri nove varijable, jedne tipa char, i dvije tipa short int, kao što je to učinjeno u sljedećem primjeru: Primjer D.2 Deklaracija i inicijalizacija kazaljki i poretka. Prikaz smještaja varijabli u memoriji. // 1. Deklaracija i definicija varijabli i kazaljki: const unsigned int cuN = 100; // Nepredznačena konstanta za definiranje // broja elemenata 1-dim poretka char c1 = '@’; // sizeof(char) = 1B short s1 = -32768, s2 = 32767; // sizeof(short int) = 2B; int iX1 = +1, iX2 = -2; // Deklaracija i inicijalizac. 2 var. tipa int int *pI = &iX1; // Deklaracija i inicijal. kazaljke pI = A(iX1) int iY[cuN] = {0, }; // Deklaracija poretka s cuN elemenata i // inicijalizacija elemenata na 0. // Ispis 1. Ispis adresa varijabli i njihovih vrijednosti. // Zaglavlje tablice: cout << " Mem. adresa A(var) Sadržaj adrese MT(A) \n" << "================================================\n" << "1. dio:\n"; cout << << << << << << << <<
hex "A(cuN) = " << &cuN << "\t cuN = " << cuN "A(iX1) = " << &iX1 << "\t iX1 = " << iX1 "A(iX2) = " << &iX2 << "\t iX2 = " << iX2 "A(pI ) = " << &pI << "\t pI = " << pI "A(iY[0])= " << &iY[0] << "\tiY[0] = " << "A(iY[1])= " << &iY[1] << "\tiY[1] = " << "A(iY[" << cuN - 1 << "])=" << &iY[cuN-1] << "\tiY[" << cuN - 1 << "]= " << << endl;
<< '\n' << '\n' << '\n' << '\n' iY[0] << '\n' iY[1] << '\n' iY[cuN-1] << '\n'
[Dovršiti i komentirati primjer, te razmještaj varijabli u Dev-C++, Code::Block i VS 2006.] [Dodati razmatranje o Debug i Release verzijama programa.]
176
Dodatak D. Pohrana C/C++ lokalnih varijabli na Wintel platformi. Ukratko ćemo obrazložiti način pohrane lokalnih varijabli za jezik C/C++, za što je potrebno poznavati osnove arhitekture računala. Detalji ostvarenja stožnog okvira i pohrane lokalnih varijabli bitno ovise o platformi i prevodiocu, a sljedeći opis vrijedi za Wintel (Windows/Intel) 32-bitnu računarsku platformu i Microsoft C++ kompajler, što se uobičajeno naziva imenom Win32. Neka je funkcija fB pozvana od strane funkcije fA. Funkciju main našeg C/C++ programa možemo promatrati kao (pot)program fB koji je pozvan od kontrolnog programa fA operacijskog sustava. Nakon što je unutar funkcije fA izvršen poziv funkcije fB, ona izgrađuje svoj aktivacijski slog na sljedeći način. Najprije se vrijednost registra EBP (Pentium procesor), u kojem je do tog trenutka bila «bazna adresa» lokalnih varijabli «pozivne» funkcije fA, stavi na stog. Nakon što se vratimo iz fB u fA, skidanjem te adrese sa stoga moći ćemo restaurirati baznu adresu od koje počinju lokalne varijable funkcije fA. Stavljanje na stog koji raste prema manjim adresama znači da se vrijednost kazaljke stoga, koja se nalazi u registru ESP, najprije smanji za 4, tj. za duljinu adresnog registra (32bitne adrese) izraženo u bajtovima. Potom se na tako umanjenu adresu koja pokazuje na novi element stoga, stavi željeni sadržaj. Nakon što je prethodni sadržaj registra EBP sačuvan na stogu, u EBP stavljamo trenutnu vrijednost kazaljke stoga ESP. Potom se na stogu rezervira mjesto dovoljne veličine za pohranu lokalnih varijabli. To se jednostavno izvede tako da se kazaljka stoga umanji za određenu vrijednost (stog raste prema manjim adresama). Ta je vrijednost jednaka prostoru potrebnom za svaku deklariranu varijablu, a rezervira se još i dodatni prostor duljine l = 40h bajtova za funkciju main, i l = 44h bajtova za ostale funkcije. Taj prostor može poslužiti za pohranu sadržaja 16 8-bitnih, ili 8 64-bitna registra. Prilikom prevođenja prevodilac gradi tablicu simbola (identifikatora) različitih od ključnih riječi, u koju ulaze svi lokalno objavljeni identifikatori. Na temelju tipa varijable i tipa poretka određuje se dodatni prostor. Na Win 32-bitnoj platformi rezervira se po 4B za svaku lokalnu varijablu čiji je tip duljine 4B ili manje, te dvostruko toliko za tip double (long double je prezentiran jednako kao i double, dakle s 8B, iako se u procesoru obrada vrši s 80 bita). Rezervacija 4B za tipove short int i char ima opravdanje u činjenici da je broj lokalnih varijabli relativno malen, pa se ne bi radilo o velikoj uštedi, pogotovo s obzirom na velike radne memorije današnjih računala. Razlog tome je pojednostavljenje rada kompajlera, koji će za adresne pomake za sve varijable računati kao višekratnike od 4. Za poretke to, naravno, ne vrijedi. Uz deklaraciju poretka tipa type dimenzije n: type Array[n] ;
gdje duljina tipa type iznosi t bajtova, se uvijek rezervira prostor od t × n bajtova. Nakon što se tako odredi broj bajtova v potreban za sve lokalno objavljene varijable (uključujući i sve strukture, poretke, itd., kao što je već rečeno), tome se doda gore spomenuta konstanta l. Vrijednost v + l se oduzme od kazaljke stoga ESP, čime je stog automatski narastao za potrebnu vrijednost. Drugim riječima, memorijski sadržaj se ne briše, jer to nije potrebno, već će jednostavno na slobodna mjesta u memoriji biti upisane lokalne varijable. Varijable se upisuju u rezervirani prostor na stogu počevši od adrese EBP – 4 × d (podsjetimo se, na stogu na adresi koja je sada u EBP je vrijednost prijašnjeg sadržaja registra EBP), gdje je d = 2 ako je prva varijabla tipa double, i d = 1 za sve ostale tipove.. Npr. ako su deklarirane tri varijable, var1, var2, var3, od kojih je samo treća tip double, one će biti pohranjene na sljedećim adresama redom: EBP – 4h , EBP – 8h , i EBP – 10h . Očito da je prva varijabla na najvećoj, a posljednja na najmanjoj adresi (vidi zad. 2.12). Pretpostavimo da je nakon varijabli var1, var2, var3, deklarirano gore navedeni poredak type Array[n]. Tada je njegova adresa, tj. adresa nultog člana:
177 Array = &Array[0] =
EBP – 10h – t × n ,
a adresa posljednjeg člana: &Array[n - 1] =
EBP – 10h – t × n + t × ( n – 1 ) = EBP – 10h – t .
Dakle, iako je prostor za lokalne varijable rezerviran na korisničkom stogu, one se ne referiraju standardnim stožnim operacijama. Jasno je da to nikako ne bi bilo u redu, jer npr. stožna operacija uzimanja sa stoga "skida" element sa stoga, a lokalne varijable moramo moći referirati dokle god smo unutar funkcije gdje je ona deklarirana. Do lokalnih varijabli se dolazi kako je opisano, dodavanjem fiksnih (negativnih) posmaka (engl. displacement, offset) adresi pohranjenoj u registru EBP, na gore opisani način. Prijenos argumenata i rezultata funkcije. Argumenti funkcije se također prirodno prenose preko stoga. U pozivnoj funkciji fA prevodilac prije strojne instrukcije poziva (call), stavlja na stog stvarne argumente poziva koji se moraju po redoslijedu, tipu i broju slagati s fiktivnim argumentima navedenim u zaglavnoj datoteci date funkcije. Ništa drugo pozivna funkcija fA ne mora znati o pozvanoj funkciji fB, što je u skladu s važnim načelom programiranja, tzv. principom skrivanja (nenužne) informacije. Nakon što su na stog pospremljeni argumenti poziva, prevodilac u kod ugrađuje strojnu instrukciju call Adress_fB koja najprije na stog posprema adresu povratka u fA (adresu instrukcije neposredno iza instrukcije call), a zatim vrši grananje (skok) na adresu Adress_fB. Sada kontrolu izvođenja preuzima funkcija fB koja će izgraditi svoj aktivacijski slog na stogu kao što je već opisano, tj. pohranom sadržaja registra EBP na stog, itd. redom. Prilikom završetka funkcije fB povratna vrijednost funkcije koja nije tipa void može se vratiti preko rezerviranog mjesta na stogu (koje je pozivna funkcija fA također mogla rezervirati zajedno s argumentima poziva), ili preko dogovorenog registra (EAX). Potom se sa stoga «miču» sve lokalne varijable jednostavnim stavljanjem vrijednosti EBP u ESP (dakle radi se o brzoj registarskoj operaciji koja ništa ne mora raditi s memorijskim operandima). Tako je čitav aktivacijski slog fB «nestao», došli smo do mjesta na stogu gdje se nalazila stara vrijednost registra EBP. Tu vrijednost sada skinemo sa stoga i premjestimo u EBP, čime je sve pripremljeno za povratak u funkciju fA. Na tom mjestu prevodilac ugrađuje instrukciju povratka (ret), koja sa stoga skida adresu povratka i stavlja je u programsko brojilo (na Intelovim procesorima to je registar IP, engl. Instruction Pointer, tj. kazaljka instrukcije). Time se kontrola izvođenja vraća na pozivnu funkciju fA, koja se nastavlja izvoditi na instrukciji neposredno iza instrukcije poziva (call).
178
Literatura 1. B. W. Kernighan, D. M. Ritchie, The C Programming Language, 2nd ed., Upper Saddle River, NJ, Prentice Hall, 1988. 2. B. Stroustrup: The C++ Programming Language, 3rd ed., Reading, Massachusetts, 1997. 3. C. S. Horstmann, Mastering Object-Oriented Design in C++, John Wiley & Sons, New York, 1995. 4. A. B. Tucker at al. Fundamentals of Computing I & II, C++ Edition, McGraw-Hill, New York, 1995. 5. Motik, Šribar: Demistificrani C++, 2. izdanje Element Zagreb, 2001. 6. L. Budin, Informatika 1, udžbenik za 1. razred gimnazije, Element d.o.o., Zagreb, 2001. 7. Wikipedia članak: www.wikipedia.org. – članak imena navedenog u tekstu. 8. Microsoft Developers Network (MSDN) Library, http://msdn.microsoft.com/en-us/library/ .