p o g l a v l j e 1 : osnovne napomeneprogramiranje.yolasite.com/resources/c_uvod.pdf · tu spadaju...

153
P o g l a v l j e 1 : OSNOVNE NAPOMENE Počnimo sa brzim uvodom u C. Cilj nam je da pokažemo suštinske elemente jezika kroz praktične programe, ali bez zadržavanja na detaljima, formalnim pravilima i izuzecima. U ovom poglavlju ne pokušavamo da budemo sveobuhvatni, niti sasvim precizni (podrazumeva se da su svi primeri ispravni). Želimo da vas, što je pre moguće, dovedemo do nivoa znanja na kome možete sami pisati korisne programe. Da bismo to uradili, moramo se koncentrisati na osnovne stvari: promenljive i konstante, aritmetiku, kontrolu toka, funkcije i najosnovnije poznavanje ulaza i izlaza nekog programa. Namerno smo izostavili iz ovog poglavlja one karakteristike C-a koje su od vitalnog značaja za pisanje dužih programa. Tu spadaju pokazivači, strukture, najveći deo bogatog seta C operatora, nekoliko iskaza za kontrolu toka i standardna biblioteka. Ovaj pristup, naravno, ima i svoje nedostatke. Najuočljiviji je da se ne može naći prikaz neke karakteristike jezika u celini na samo jednom mestu. A kako primeri ne iskorišćavaju u potpunosti svu snagu C-a, to oni nisu tako sažeti i elegantni kako bi mogli da budu. Mi smo pokušali da umanjimo ovaj efekat, ali imajte ga na umu. Sledeći nedostatak je taj što se u kasnijim poglavljima neizbežno ponavljaju delovi iz ovog poglavlja. Nadamo se da će ponavljanje više pomoći nego odmoći. U svakom slučaju, iskusniji programeri bi trebalo da izvuku iz ovog poglavlja ono što je njima potrebno. Početnici bi trebalo da nakon pročitanog poglavlja napišu sami svoje male programe, već prema zadacima koji su dati za vežbu na kraju svake celine. Obe grupe mogu koristiti ovo poglavlje kao podlogu na kojoj će se zasnivati detaljniji opisi koji počinju u Poglavlju 2. 1.1 POČETAK Jedini način da se nauči neki programski jezik je da se pišu programi u njemu. Prvi program koji treba napisati je isti za sve jezike: odštampaj hello, world Ovo je početna prepreka: da bi se preskočila potrebno je da negde napišete tekst programa, da ga uspešno kompajlirate (prevedete), učitate u memoriju, startujete i ustanovite gde se pojavio izlaz hello, world. U poređenju sa ovim tehničkim detaljima, sve drugo je relativno lako. Naravno, prvo treba ovladati njima. U C-u, program za štampanje hello, world izgleda ovako: #include<stdio.h>

Upload: others

Post on 06-Sep-2019

0 views

Category:

Documents


0 download

TRANSCRIPT

P o g l a v l j e 1 : OSNOVNE NAPOMENE Počnimo sa brzim uvodom u C. Cilj nam je da pokažemo suštinske elemente jezika kroz praktične programe, ali bez zadržavanja na detaljima, formalnim pravilima i izuzecima. U ovom poglavlju ne pokušavamo da budemo sveobuhvatni, niti sasvim precizni (podrazumeva se da su svi primeri ispravni). Želimo da vas, što je pre moguće, dovedemo do nivoa znanja na kome možete sami pisati korisne programe. Da bismo to uradili, moramo se koncentrisati na osnovne stvari: promenljive i konstante, aritmetiku, kontrolu toka, funkcije i najosnovnije poznavanje ulaza i izlaza nekog programa. Namerno smo izostavili iz ovog poglavlja one karakteristike C-a koje su od vitalnog značaja za pisanje dužih programa. Tu spadaju pokazivači, strukture, najveći deo bogatog seta C operatora, nekoliko iskaza za kontrolu toka i standardna biblioteka. Ovaj pristup, naravno, ima i svoje nedostatke. Najuočljiviji je da se ne može naći prikaz neke karakteristike jezika u celini na samo jednom mestu. A kako primeri ne iskorišćavaju u potpunosti svu snagu C-a, to oni nisu tako sažeti i elegantni kako bi mogli da budu. Mi smo pokušali da umanjimo ovaj efekat, ali imajte ga na umu. Sledeći nedostatak je taj što se u kasnijim poglavljima neizbežno ponavljaju delovi iz ovog poglavlja. Nadamo se da će ponavljanje više pomoći nego odmoći. U svakom slučaju, iskusniji programeri bi trebalo da izvuku iz ovog poglavlja ono što je njima potrebno. Početnici bi trebalo da nakon pročitanog poglavlja napišu sami svoje male programe, već prema zadacima koji su dati za vežbu na kraju svake celine. Obe grupe mogu koristiti ovo poglavlje kao podlogu na kojoj će se zasnivati detaljniji opisi koji počinju u Poglavlju 2. 1.1 POČETAK Jedini način da se nauči neki programski jezik je da se pišu programi u njemu. Prvi program koji treba napisati je isti za sve jezike: odštampaj hello, world Ovo je početna prepreka: da bi se preskočila potrebno je da negde napišete tekst programa, da ga uspešno kompajlirate (prevedete), učitate u memoriju, startujete i ustanovite gde se pojavio izlaz hello, world. U poređenju sa ovim tehničkim detaljima, sve drugo je relativno lako. Naravno, prvo treba ovladati njima. U C-u, program za štampanje hello, world izgleda ovako: #include<stdio.h>

main() { printf(đhello, world\nđ); } Kako će se ovaj program startovati, zavisi od operativnog sistema koji koristite. Kao specifičan primer, na UNIX operativnom sistemu možete otkucati tekst programa i datoteci u kojoj se nalazi dati nakon imena ekstenziju đ .c đ. Neka je program nazvan hello.c; kompajlirali bi ga komandom cc hello.c. Ako niste nigde pogrešili u kucanju, kompajliranje će proteći bez upozorenja i formiraće se datoteka a.out. Ako startujete program a.out kucajući njegovo ime, on će odštampati hello, world Na drugim sistemima, postupak će biti drugačiji. Slede neka objašnjenja o samom programu. C program, bilo koje veličine, sastoji se od jedne ili više funkcija i od promenljivih. Funkcija se sastoji od iskaza koji određuju koje računske operacije treba sprovesti, a promenljive sadrže vrednosti sa kojima se vrše izračunavanja. C funkcije su slične funkcijama i potprogramima Fortrana ili procedurama i funkcijama Paskala.U našem primeru, funkcija je main. U opštem slučaju, imate slobodu da funkciji date bilo kakvo ime, ali je main specijalno ime - program se izvršava od početka funkcije main. To znači da svaki program mora imati main negde. Funkcija main je obično pozvati druge funkcije koje obavljaju posao, od kojih će neke biti u istom programu sa main , a druge će im se priključiti iz biblioteke prethodno napisanih funkcija. Prva linija programa, #include <stdio.h> govori kompjuteru gde da potraži podatke o ulazno/izlaznim funkcijama kakva je, recimo, printf. Podaci su smešteni u biblioteci koja je definisana standardom: u ovom slučaju je to STanDard Input Output biblioteka. Detaljnije će o njoj biti rečeno u Dodatku B. Jedan od načina razmene podataka između funkcija je pomoću argumenata. Između zagrada () koje slede iza imena funkcije navodi se lista argumenata sa kojima će funkcija operisati. Ovde main nema argumente, što se zaključuje iz main(). Vitičaste zagrade { } obuhvataju iskaz ili grupu iskaza koji čine funkciju. Funkcija main sadrži samo jedan iskaz, printf(„hello, world\n“); Funkcija se poziva navođenjem njenog imena i liste argumenata koje koristi. Lista argumenata je navedena u zagradama (). Ne postoji naredba CALL kakvu ima Fortran, na primer. Zagrade () moraju biti prisutne čak i ako funkcija nema argumente. Linija printf(„hello, world\n“);

predstavlja poziv funkcije printf koja ima argument „hello, world\n“. printf je funkcija iz biblioteke i ona štampa argument na ekranu (ukoliko nije drugačije određeno). U ovom slučaju štampa niz karaktera koji čini njen argument. Grupa znakova između dvostrukih navodnika, kakva je hello, world\n zove se niz znakova ili string. U početku će jedina upotreba stringa biti u vidu argumenata funkcije printf i nekih drugih funkcija. Deo niza \n je u C-u oznaka za novi red, i označava da štampanje karaktera posle njega treba početi od početka novog reda. Važeći naziv za konstrukcije tipa \n je eskejp (escape) sekvenca. Ako izostavite \n (vredan eksperiment), otkrićete da se nakon odštampanog hello, world sledeća pozicija za štampanje nije premestila na početak sledećeg reda. Jedini način da se to izvede je pomoću \n, kako je već opisano. Ako pokušate nešto kao printf(„hello, world „); C kompajler će neljubazno prijaviti da nedostaju navodnici. Funkcija printf nikad ne obezbeđuje prelazak na početak novog reda automatski, tako da uzastopni pozivi te funkcije formiraju izlaznu liniju postupno, a ne proizvode više linija jednu ispod druge. Naš prvi program je mogao biti napisan i ovako: #include<stdio.h> main() { printf(„hello“); printf(„,world“); printf(„\n“); } i proizveo bi istu izlaznu liniju. Primetite da \n predstavlja samo jedan znak. Konstrukcija kakva je \n predstavlja način da se prikažu i primene znakovi koji su nevidljivi ili se ne mogu otkucati. Između ostalih, C obezbeđuje \t za tabulator, \b za backspace, \“ za dvostruki navodnik, i \\ za obrnutu kosu crtu (backslash). Kompletna lista je data u odeljku 2.3. þ Vežba 1-1 Startujte program „hello, world“ na vašem kompjuteru. Eksperimentišite sa izostavljanjem delova programa da biste videli koje ćete poruke o greškama dobiti.

þ Vežba 1-2 Otkrijte šta se dešava kada niz koji printf treba da štampa sadrži \x , gde je x neki znak koji nije među pomenutima. 1.2 PROMENLJIVE I ARITMETIČKI IZRAZI Sledeći program štampa tabelu temperatura u Farenhajtovim stepenima i njihove ekvivalente u Celzijusovim stepenima, koristeći formulu øC = (5/9)(øF - 32): 0 -17 20 -6.7 40 4.4 60 15.6 ... ... 260 126.7 280 137.8 300 148.9 Program se sastoji od same funkcije main. On je duži od onog koji štampa „hello, world“, ali ne i komplikovaniji. Uvodi se mnogo novih elemenata, uključujući komentare, deklaracije, promenljive, aritmetičke izraze, petlje i formatizovani izlaz. #include<stdio.h> /* štampaj Fahrenheit - Celsius tabelu za fahr = 0, 20, ... , 300 */ main() { float fahr, celsius; int lower, upper, step; lower = 0; /* donja granica temp. tabele */ upper = 300; /* gornja granica */ step = 20; /* veličina koraka */ fahr = lower; while (fahr <= upper) { celsius = (5.0/9.0) * (fahr - 32.0); printf(„%4.0f %6.1f \n“, fahr, celsius); fahr = fahr + step; } } Prve dve linije predstavljaju komentar, koji u ovom slučaju ukratko objašnjava šta program radi. Bilo koje znake između /* i */ kompajler ignoriše; ova činjenica se može slobodno iskoristiti da bi se napisao program koji je lakši za razumevanje. Komentar se može pojaviti na bilo kom mestu na kom može i blanko, tabulator, ili znak za novi red.

U C-u, sve promenljive moraju biti deklarisane pre upotrebe, najčešće na početku funkcije, pre bilo koje izvršne komande. Ako zaboravite deklaraciju, kompajler će to prijaviti kao grešku. Deklaracija se sastoji od tipa i liste promenljivih koje imaju taj tip, kao u float fahr, celsius; int lower, upper, step; Tip int znači da su promenljive na listi celi brojevi; float stoji za pokretni zarez, tj. za realne brojeve koji mogu imati deo iza decimalne tačke. Tačnost int i float zavisi od mašine koju koristite: na primer, 16- bitni int može predstaviti brojeve u opsegu od -32768 do +32767. Tipična dužina float broja je 32 bita (4 bajta), sa sedam značajnih cifara i opsegom od 10^-38 do 10^+38. Pored int i float, C obezbeđuje još nekoliko osnovnih tipova podataka : char znak - jedan bajt short mali ceo broj long veliki ceo broj double realan broj dvostruke tačnosti Veličine ovih tipova takođe zavise od računara. Postoje još i polja, strukture i unije osnovnih tipova, pokazivači na njih i funkcije koje ih vraćaju, a koje ćemo pravovremeno sresti. Izračunavanje programa konverzije temperature počinje iskazima dodeljivanja : lower = 0; upper = 300; step = 20; fahr = lower; koji postavljaju promenljive na njihove početne vrednosti. Svaki iskaz se završava sa ; (tačka-zarez). Svaka linija u tabeli se izračunava na isti način, pa smo zato upotrebili petlju koja se ponavlja jedanput za svaku liniju; to je svrha while iskaza: while (fahr <= upper) { ... } Ispituje se istinitost uslova u zagradama. Ako je istinit (fahr je manje ili jednako upper), telo petlje (svi iskazi unutar zagrada { i }) se izvršava. Zatim se uslov ponovo ispituje, i ako je istinit telo se izvršava još jedanput. Kad uslov postane neistinit (fahr postane veće od upper), petlja se završava, telo se preskače i izvršavanje se nastavlja iskazom koji sledi iza petlje. U našem programu nema iskaza koji slede posle petlje, pa se program

tu završava. Telo while petlje može biti jedan ili više iskaza obuhvaćenih zagradama { i }, ili samo jedan iskaz koji u tom slučaju ne mora biti naveden u zagradama, kao u while (i < j) i = 2 * i; U oba slučaja iskazi koji čine telo while petlje su pomereni za jednu tab poziciju udesno, tako da odmah jasno možete videti šta je telo petlje. Ovo uvlačenje teksta udesno naglašava logičku strukturu programa. Iako C ne vodi računa o tome na kom mestu je iskaz ispisan, pravilno pozicioniranje teksta i upotreba blanko znakova su izuzetno značajni za čitljivost programa. Preporučujemo vam pisanje samo jednog iskaza u jednoj liniji, i ostavljanje blanko znakova oko operatora. Pozicija zagrada je manje značajna; izabrali smo jednu od nekoliko popularnih varijanti. Izaberite stil koji vam odgovara, a zatim ga se dosledno pridržavajte. Najveći deo posla se obavi u telu petlje. Temperatura u øC se izračunava i dodeljuje promenljivoj celsius kroz iskaz celsius = (5.0 / 9.0) * (fahr - 32.0) Razlog za korišćenje 5.0 / 9.0 umesto jednostavnijeg 5 / 9 leži u prirodi C- a. Kao i kod mnogih drugih jezika, i ovde je rezultat deljenja dva cela broja ceo broj i ostatak koji se odbacuje. Tako bi 5 / 9 bilo nula, što bi dalo sve temperature jednake nuli. Decimalna tačka kod konstanti naznačava da je reč o realnom broju, tako da je 5.0 / 9.0 jednako 0.555... , što smo i želeli. Takođe smo napisali i 32.0 umesto 32 iako je jasno da je fahr tipa float. U ovom slučaju 32 će biti automatski konvertovano u float tip (u 32.0) pre nego što počne oduzimanje. Više kao pitanje stila, savetujemo vam da pišete konstante (koje su realni brojevi) sa decimalnom tačkom i onda kada sadrže celobrojne vrednosti; to naglašava njihovu prirodu realnog broja kod onih koji čitaju tekst. Detaljna pravila konverzije celih brojeva u realne je data u Poglavlju 2. Za sada, primetite da dodeljivanje fahr = lower; i test while (fahr <= upper) rade kao što se i moglo očekivati - int promenljiva se konvertuje u float pre nego što se operacija započne. Ovaj primer, takođe, malo bolje pokazuje način na koji printf radi.

Funkcija printf je izlazna funkcija opšte namene, do detalja opisana u standardnoj biblioteci. Njen prvi argument je niz znakova u kome svaki znak % pokazuje da umesto njega treba da se odštampa njemu odgovarajući argument koji sledi posle tog niza, i to u formatu koji je takođe uz njega naveden. Na primer, u iskazu printf(„%4.0f %6.1f \n“, fahr, celsius); Prvi argument je niz %4.0f %6.1f \n, drugi argument je fahr, a treći je celsius. Prvi deo niza, %4.0f, odnosi se na drugi argument tj. na fahr, a drugi deo niza, %6.1f, na njemu odgovarajući, treći argument tj. na celsius. Ovde je specifikacijom %4.0f određeno da se fahr štampa kao realan broj od četiri cifre, bez cifara iza decimalne tačke. Specifikacijom %6.1f se od printf zahteva da promenljivu celsius odštampa u formatu realnog broja sa šest cifara za celobrojni deo i jednom cifrom posle decimalne tačke. Neki delovi specifikacije se mogu izostaviti: %6f znači da argument treba da bude predstavljen sa šest cifara; %.2f zahteva dve cifre iza decimalne tačke, ali ne ograničava dužinu celobrojnog dela; najzad, %f zahteva da se broj odštampa u formatu realnog broja (sa decimalnom tačkom). Funkcija printf takođe prepoznaje %d za decimalne cele brojeve, %o za oktalne, %x za heksadecimalne brojeve, %c za znak, %s za niz znakova, i %% za % (procenat). Svaka % konstrukcija u prvom argumentu funkcije printf je u paru sa njoj odgovarajućim drugim, trećim, itd. argumentom; ti argumenti moraju biti poređani istim redom kojim su poređane njihove odgovarajuće % konstrukcije, u protivnom ćete dobiti besmislene izlaze. Uzgred, funkcija printf nije deo C jezika; ulaz i izlaz nisu definisani C-om. Nema ničeg magičnog u funkciji printf; ona je samo korisna funkcija koja je deo standardne biblioteke funkcija. Toj biblioteci mogu pristupati C programi. Da bismo se koncentrisali na sam C jezik, o ulazu i izlazu nećemo mnogo govoriti do Poglavlja 7. Do tada ćemo odložiti bavljenje ulazom podataka. Ako morate da unosite brojeve, pročitajte diskusiju o funkciji scanf. Funkcija scanf je slična funkciji printf, s tom razlikom što unosi podatke umesto da ih štampa. þ Vežba 1-3 Izmenite program za konverziju temperature tako da štampa zaglavlje iznad tabele temperatura. þ Vežba 1-4 Napišite program za štampanje tabele konvertovanih temperatura iz Celzijusovih stepeni u Farenhajtove stepene. 1.3 F O R ISKAZ Kao što ste mogli očekivati, postoji mnogo različitih načina da se napiše program; pokušajmo sa varijacijom programa za konverziju temperature.

main() /* Fahrenheit - Celsius tabela */ { int fahr; for (fahr = 0; fahr <= 300; fahr = fahr + 20) printf(„%4d %6.1f \n“, fahr, (5.0 / 9.0) * (fahr - 32)); } Ovo daje iste rezultate, ali zaista izgleda drugačije. Jedna od glavnih izmena je eliminacija najvećeg broja promenljivih: ostala je samo promenljiva fahr, koja je sada tipa int (da bi pokazala %d konverziju u printf). Donja i gornja granica i korak se pojavljuju samo kao konstante u for iskazu, koji je sada konstruisan drugačije. Izraz koji izračunava temperaturu u Celzijusovim stepenima se sada pojavljuje kao treći argument funkcije printf umesto kao odvojen iskaz. Ova poslednja izmena je primer opšteg pravila u C-u : u bilo kom kontekstu u kome se može pojaviti promenljiva nekog tipa, može se pojaviti i čitav izraz tog tipa. Kako treći argument funkcije printf mora biti realan broj da bi bio u skladu sa drugim delom %6.1f niza, to se na mestu tog argumenta može pojaviti bilo koji izraz float tipa. Iskaz for je petlja, uopštenje petlje while. Ako for uporedite sa prethodnim while, njegovo funkcionisanje bi trebalo da bude jasno. Iskaz for sadrži tri dela međusobno odvojena znakom ;. Prvi deo,inicijalizacija brojača, fahr = 0; se obavlja jedanput, pre nego što petlja uopšte počne. Drugi deo je uslov kojim se kontroliše petlja: fahr <= 300; Ovaj uslov se proverava; ako je istinit, telo petlje (ovde je to samo jedan poziv printf funkcije) se izvršava. U trećem delu petlje, reinicijalizaciji brojača, fahr = fahr + 20; vrši se uvećavanje brojača fahr za korak 20. Posle ovoga, petlja se ponovo testira sa novim fahr i izvršava sve dok je uslov ispunjen. Kao i kod while petlje, telo petlje može biti samo jedan iskaz ili grupa iskaza navedena u vitičastim zagradama { i }. Inicijalizacija i reinicijalizacija mogu biti bilo koji izrazi. Izbor između for i while je proizvoljan, zavisno od toga šta je jednostavnije. Iskaz for se obično upotrebljava kod petlji u kojima su inicijalizacija i reinicijalizacija jednostavne konstrukcije i logički povezane. U takvim slučajevima je for petlja kompaktnija nego while i sadrži kontrolu petlje na jednom mestu.

þ Vežba 1 - 5 Izmenite program za konverziju temperature tako da štampa tabelu temperatura obrnutim redom, od 300 stepeni do 0. 1.4 SIMBOLIČKE KONSTANTE Evo konačnog osvrta pre nego što zauvek napustimo konverziju temperature. Loša praksa je da ubacujemo brojeve kao što su 300 i 20 u programu. Oni prenose vrlo malo informacija onome ko bi kasnije morao da čita programe, i teško ih je izmeniti na sistematičan način. Srećom, C obezbeđuje način da se izbegne ubacivanje konkretnih brojeva u program. Sa #define konstrukcijom na početku programa moguće je definisati simboličko ime ili simboličku konstantu tako da ona predstavlja određeni niz karaktera. To izgleda ovako: #define i m e tekst Nakon toga kompajler će na svim mestima gde se polavljuje simbolička konstanta i m e izvršiti njenu zamenu ekvivalentnim nizom znakova tekst. Izuzetak je slučaj kada je i m e navedeno pod dvostrukim navodnicima. Tada kompajler neće izvršiti zamenu. Bitno je napomenuti da tekst može biti bilo kakvog oblika: on nije ograničen samo na brojeve. #include<stdio.h> #define LOWER 0 /* donja granica tabele */ #define UPPER 300 /* gornja granica */ #define STEP 20 /* korak */ /* štampaj Fahrenheit - Celsius tabelu */ main() { int fahr; for (fahr = LOWER; fahr <= UPPER; fahr = fahr + STEP) printf(„%4d %6.1f \n“, fahr, (5.0 / 9.0) * (fahr - 32)); Elementi LOWER, UPPER i STEP su konstante, pa se ne pojavljuju u deklaracijama. Simbolička imena se obično pišu velikim slovima da bi se razlikovala od imena promenljivih koje se pišu malim slovima. Primetite da ne postoji znak ; na kraju simboličke definicije. Pošto se svi znakovi posle simboličkog imena zamenjuju u tekstu, onda bi se, u slučaju kad bi simboličke definicije završavali znakom ;, i u iskazu for pojavilo previše znakova ;. 1.5 ZNAKOVNI ULAZ I IZLAZ

Sada ćemo razmotriti familiju povezanih programa koji vrše jednostavne operacije nad znakovnim podacima. Otkrićete da su mnogi programi samo proširene verzije prototipa o kojima ćemo mi diskutovati. Standardna biblioteka obezbeđuje funkcije za čitanje i upisivanje znakova. Funkcija getchar() preuzima sledeći znak sa ulaza svaki put kad je pozvana, a vraća u program vrednost koja odgovara tom znaku. To znači da posle c = getchar(); promenljiva c sadrži vrednost koja odgovara znaku koji je funkcija getchar preuzela sa ulaza. Znakovi uobičajeno dolaze sa tastature. Funkcija putchar je komplement funkcije getchar. Iskaz putchar(c); štampa sadržaj promenljive c na nekom izlazu, najčešće ekranu. Pozivi funkcija putchar i printf se mogu isprepletati; izlaz će se pojavljivati onim redosledom kojim su funkcije pozivane. Kao što je to bio slučaj sa funkcijom printf, ni funkcije getchar i putchar nisu ništa posebno. One nisu deo C jezika, ali im svaki C program može pristupiti. 1.5.1 Kopiranje datoteka Uz pomoć funkcija getchar i putchar možete napisati iznenađujuće mnogo korisnih programa ne znajući ništa o ulazu i izlazu. Najjednostavniji primer je program koji kopira svoj ulaz na izlaz, znak po znak. Algoritam izgleda ovako: pročitaj znak sa ulaza while(znak nije znak za kraj datoteke) pošalji na izlaz upravo pročitani znak pročitaj sledeći znak Napisan u C-u, program bi izgledao ovako: #include<stdio.h> /* kopiranje ulaza na izlaz; prva verzija */ main() { int c; c = getchar(); while(c != EOF) { putchar(c); c = getchar();

} } Relacioni operator != znači „različit od“. Osnovni problem je pronalaženje kraja ulaza. Po dogovoru, funkcija getchar vraća u program vrednost koja ne odgovara nijednom znaku iz važećeg skupa znakova čim naiđe na kraj ulaza. Na taj način programi mogu odrediti kada su stigli do kraja ulaznih podataka. Jedina otežavajuća okolnost je ta što postoje dva dogovora oko toga šta predstavlja kraj datoteke. Mi smo odložili izbor uvodeći simboličku konstantu EOF umesto konkretne vrednosti, koja god ona bila. U praksi, EOF će biti ili -1 ili 0, tako da će program ispravno raditi ako se na njegovom početku navede jedna od sledeće dve simboličke definicije : #define EOF -1 ili #define EOF 0 Koristeći simboličku konstantu EOF za predstavljanje vrednosti koja se pojavljuje kada program naiđe na kraj ulaznih podataka, osigurali smo se da samo jedna stvar u programu zavisi od konkretne numeričke vrednosti: to je simbolička definicija simboličke konstante EOF. Takođe, c je u programu deklarisano kao int tip promenljive, a ne char tip, tako da može čuvati vrednost koju funkcija getchar vraća u program. Kao što ćemo videti u Poglavlju 2, ova vrednost je zapravo int tipa jer, pored svih mogućih znakova, ona mora predstavljati i EOF koji je int tipa. Program za kopiranje ulaza na izlaz bi iskusniji C programeri mogli napisati u mnogo sažetijem obliku. U C-u, bilo kakvo dodeljivanje vrednosti promenljivoj, kao na primer c = getchar(); može se upotrebiti u nekom izrazu. Tamo će se vrednost izraza dodeljivanja jednostavno pridružiti promenljivoj na levoj strani, a zatim dalje operisati sa tom promenljivom. Ako se dodeljivanje vrednosti pročitanog znaka promenljivoj c stavi u uslov while petlje, program za kopiranje ulaza u izlaz može biti napisan kao #include<stdio.h> /* kopiranje ulaza na izlaz; druga verzija */ main() { int c; while( (c = getchar()) != EOF) putchar(c);

} Program čita znak sa ulaza, dodeljuje njegovu vrednost promenljivoj c, a zatim proverava da li je to znak za kraj datoteke. Ako nije, telo while petlje se izvršava štampajući znak na izlazu. Nakon toga se uslov while petlje ponovo proverava. Kada se konačno dođe do znaka za kraj datoteke petlja while se okončava, a takođe i funkcija main. Ova verzija centralizuje ulaz - sada postoji samo jedan poziv funkcije getchar - i sažima program. Umetanje dodeljivanja vrednosti promenljivoj u neko testiranje je jedno od mesta gde C omogućava korisno sažimanje programa. Moguće je i dalje sažimanje programa, ali bi to rezultiralo nečitljivom i nerazumljivom konstrukcijom, što smo želeli da izbegnemo. Veoma je važno primetiti da su zagrade ( i ) koje obuhvataju izraz dodeljivanja vrednosti promenljivoj c, zaista neophodne. Prioritet operatora != je viši od onog koji ima operator =, što znači da bi se u odsustvu zagrada dogodilo sledeće: 1ø prvo bi bio testiran uslov „da li je funkcija getchar vratila u program znak EOF za kraj ulaza“. Rezultat tog poređenja bio bi 0 ili 1, zavisno od toga da li je poređenje netačno ili tačno. 2ø Vrednost 0 ili 1 bi se operatorom = dodelila promenljivoj c, što bi imalo neželjen efekat. Dakle, iskaz c = getchar() != EOF; je ekvivalentan iskazu c = ( getchar() != EOF); þ Vežba 1 - 6 Napišite program za štampanje vrednosti EOF . 1.5.2 Brojanje znakova Sledeći program broji znake; to je mala razrada programa za kopiranje #include<stdio.h> /* brojanje znakova na ulazu; prva verzija */ main() { long nc; nc = 0; while ( getchar() != EOF)

++nc; printf(„%ld \n“, nc); } Iskaz ++nc; uvodi novi operator, ++, koji znači povećaj za jedan. Vi možete pisati i nc = nc + 1 , ali je ++nc mnogo sažetije i često efikasnije. Postoji odgovarajući operator -- za smanjivanje za jedan. Operatori ++ i -- mogu biti ili prefiks operatori (++nc), ili sufiks operatori (nc++). Ove dve varijante pisanja imaju različito značenje u izrazima, kao što ćemo videti u Poglavlju 2. Za trenutak ćemo se držati prefiks varijante. Program za brojanje znakova ima svoj brojač znakova u vidu promenljive long tipa, umesto tipa int. Najveći broj koji se može prikazati promenljivom int tipa je najčešće 32768 (int je obično dug dva bajta). To znači da bi bio dovoljan ulaz relativno male dužine da dođe do prekoračenja brojača ako je deklarisan kao int; pošto promenljiva tipa long najčešće zauzima četiri bajta, može se zaključiti da je u ovom slučaju njena primena adekvatna. Navedena specifikacija %ld naznačava funkciji printf da joj je odgovarajući argument nc veća celobrojna vrednost tipa long. Za operisanje sa još većim brojevima, možete koristiti promenljive tipa double (realan broj dvostruke veličine). Mi ćemo takođe koristiti for iskaz umesto while, kako bi prikazali alternativni način za pisanje petlje. #include<stdio.h> /* brojanje znakova na ulazu; druga verzija */ main() { double nc; for (nc = 0; getchar() != EOF; ++nc) ; printf(„%.0f \n“, nc); } Funkcija printf koristi istu specifikaciju %f i za float i za double tip promenljive; ovde %.0f odbacuje štampanje nepostojećeg dela iza decimalne tačke. Telo for petlje je u ovom slučaju prazno, stoga što se ceo posao obavlja u delu za testiranje uslova i u delu za reinicijalizaciju. Međutim, gramatička pravila C-a nalažu da for petlja mora imati telo. Izolovan znak ;, praktično nulti (nepostojeći) iskaz, je tu da zadovolji ta pravila. Stavili smo ga u zasebnu liniju da bismo ga učinili vidljivijim. Pre nego što napustimo program za brojanje znakova, primetite da ako na ulazu nema nijednog znaka, uslovi u while i for petljama nisu zadovoljeni već

pri prvom pozivu funkcije getchar, tako da program kao rezultat vraća nulu, što je ispravan odgovor. Jedna od dobrih stvari u vezi sa while i for petljama je ta da se uslov testira na početku petlje, pre nego što se uđe u telo petlje. Ako nema šta da se radi (uslov nije zadovoljen), ništa neće ni biti urađeno, čak i ako to znači da se nijedanput neće proći kroz telo petlje. Programi bi trebalo da se ponašaju inteligentno kad barataju sa ulazima bez znakova. Iskazi while i for pomažu tako što rade razumne operacije u ekstremnim situacijama. 1.5.3 Brojanje linija Sledeći program broji koliko linija čini ulaz. Pretpostavlja se da su linije međusobno odvojene znakom \n za novi red koji je u dosadašnjem tekstu striktno dodavan svakoj liniji koja je trebalo da se odštampa. Znači, brojanje linija se svodi na brojanje znakova za novi red. #include <stdio.h> /* brojanje linija na ulazu */ main() { int c, nl; nl = 0; while ( (c = getchar()) != EOF) if (c == '\n') ++nl; printf(„%d \n“, nl); } Telo while petlje se sada sastoji od if iskaza, koji kontroliše povećavanje brojača nl za jedan. Iskaz if ispituje uslov koji sledi u zagradama i, ako je tačan, izvršava iskaz (ili grupu iskaza navedenih unutar vitičastih zagrada) koji čini telo if konstrukcije. Još jednom smo hteli da istaknemo šta se čime kontroliše. Dvostruki znak jednakosti == je C oznaka za „je jednako“. Ovaj simbol je uveden da razgraniči test jednakosti od dodeljivanja vrednosti promenljivoj, koje se vrši operatorom =. Pošto je dodeljivanje vrednosti promenljivoj otprilike dvaput češće od testiranja jednakosti u tipičnim C programima, razumljivo je da je i operator dodeljivanja upola kraći. Bilo koji znak napisan između jednostrukih navodnika tretira se kao celobrojna vrednost koja odgovara tom znaku. Koja će to vrednost biti zavisi od toga koji je skup znakova ugrađen u računar. Tako napisan znak se naziva znakovna konstanta. Tako je, na primer, 'A' znakovna konstanta; u ASCII setu znakova je njena odgovarajuća vrednost 65, što je interna reprezentacija znaka A. Naravno da je bolje pisati 'A' nego 65; njeno značenje je očigledno, i ne zavisi od drugačijeg seta znakova. Eskejp sekvence korišćene u nizovima znakova se takođe mogu napisati u

obliku znakovnih konstanti, pa tako '\n' predstavlja vrednost koja odgovara znaku za novi red (u ASCII je to 10). Naročito obratite pažnju na činjenicu da je '\n' jedan znak i da se u izrazima tretira kao ceo broj. Sa druge strane, „\n“ je niz znakova koji u ovom slučaju sadrži samo jedan znak. Nizovi i znakovi biće predmet razmatranja u Poglavlju 2. þ Vežba 1 - 7 Napišite program koji broji blanko znakove, tabulatore i znake za novi red. þ Vežba 1 - 8 Napišite program koji kopira ulaz na izlaz, i pri tome zamenjuje eventualni niz blanko znakova samo jednim blanko znakom. þ Vežba 1 - 9 Napišite program koji zamenjuje svaki tabulator nizom od tri znaka: znakom >, backspace znakom i znakom - .Takva kombinacija ova tri znaka daće na izlazu -> .Neka program zamenjuje i svaki backspace znak sličnim nizom <- .To će backspace i tab znake učiniti vidljivim. 1.5.4 Brojanje reči šetvrti u našoj seriji korisnih programa broji linije, reči i znake, usvajajući definiciju po kojoj je reč bilo koja grupa znakova koja ne sadrži blanko znak, tabulator i znak za novi red. (Ovo je ogoljena verzija UNIX rutine wc). #include<stdio.h> #define YES 1 /* jeste reč */ #define NO 0 /* nije reč */ /* broji linije, reči i znake ulaza */ main() { int c, nl, nw, nc, state; state = OUT; nl = nw = nc = 0; while ( (c = getchar()) != EOF) { ++nc; if (c == '\n') ++nl; if (c == ' ' || c == '\n' || c == '\t') state = NO; else if (state == NO) { state = YES; ++nw; } } printf(„%d %d %d \n“, nl, nw, nc); } Svaki put kada program naiđe na znak, on uveća brojač znakova. Promenljiva state beleži da li je program trenutno unutar neke reči ili nije. Početno

stanje je „nije reč“, i promenljivoj state se dodeljuje vrednost simboličke konstante NO. Bolje je koristiti simboličke konstante YES i NO nego konkretne vrednosti 0 i 1, jer je sa simboličkim konstantama program čitljiviji. Naravno da u sićušnom programu kakav je ovaj to pravi malu razliku, ali u većim programima preglednost programa je od velike koristi. Takođe ćete otkriti da je mnogo lakše vršiti obimne ispravke u programima koji su pisani koristeći simboličke definicije. U takvim programima je, umesto da svuda tražite i ispravljate vrednost neke promenljive, dovoljno ispraviti simboličku definiciju i na svim mestima gde se koristi ta definicija biće unete ispravke. Linija nl = nw = nc = 0; postavlja sve promenljive na nulu. Ovo nije poseban slučaj, već posledica činjenice da i izraz dodeljivanja, ovde nc = 0, ima svoju vrednost (ovde je to nula). Uz to, dodeljivanja se vrše sa desna na levo, pa bi bilo identično da smo pisali nl = (nw = (nc = 0)); Operator || znači OR (ili) , pa linija if (c == ' ' || c == '\n' || c == '\t') znači „ako je c blanko znak ili je znak za novi red ili je tabulator ...“ . (Eskejp sekvenca \t je način da se tabulator predstavi u vidljivoj formi). Postoji i odgovarajući operator za logičko AND (i) i on se označava sa &&. Prioritet operatora && je veći od prioriteta operatora ||. Izrazi povezani međusobno sa operatorima && i || se izračunavaju sleva nadesno, a izračunavanje prestaje čim je istinitost ili neistinitost poznata. Na taj način, ako promenljiva c sadrži blanko znak, čitava konstrukcija je istinita (jer je dovoljno da je zadovoljen jedan od tri uslova vezanih logičkim „ili“), pa nema potrebe da se ispituju i druga dva uslova. Ti uslovi se tada ne ispituju. Ova činjenica nije od neke posebne važnosti ovde, ali je, kao što ćemo uskoro videti, veoma značajna kod komplikovanijih izraza. Primer takođe uvodi else iskaz, koji definiše šta treba preduzeti u slučaju da uslov koji ispituje if nije zadovoljen. Opšti oblik ovakve konstrukcije je if ( izraz ) iskaz 1 else iskaz 2 Jedan i samo jedan od dva iskaza pridružena if - else konstrukciji će biti izvršen. Ako je izraz tačan (istinit), izvršava se iskaz 1 ; ako nije, izvršava se iskaz 2. Svaki iskaz može biti prilično komplikovan. U programu

za brojanje reči, iskaz posle else je čitava if konstrukcija koja kontroliše dva iskaza unutar vitičastih zagrada. þ Vežba 1 - 10 Izmenite program za brojanje reči koristeći bolju definiciju „ reči „. Na primer, reč je niz slova, brojeva i interpunkcije koji počinje slovom. 1.6 POLJA Napišimo program koji kontroliše broj pojavljivanja svake cifre, specijalnih znakova (blanko znak, tabulator i znak za novi red) i svih ostalih znakova. Ovo je veštački napravljen primer, ali nam omogućava da prikažemo nekoliko mogućnosti jezika C. U ovom primeru može se pojaviti dvanaest različitih ulaza (deset cifara, specijalni znak, ostali znakovi), pa je pogodno upotrebiti jedno polje za čuvanje broja pojavljivanja svake cifre umesto deset zasebnih promenljivih. Evo jedne verzije programa : #include<stdio.h> /* broji cifre, specijalne i druge znake */ main() { int c, i, nwhite, nother; int ndigit[10]; nwhite = nother = 0; for (i = 0; i < 10; ++i) ndigit[i] = 0; while ( (c = getchar()) != EOF) { if (c >= '0' && c <= '9') ++ndigit[c - '0']; else if (c == ' ' || c == '\n' || c == '\t') ++nwhite; else ++nother; } printf(„Digits = „); for (i = 0; i < 10; ++i) printf(„ %d“, ndigit[i]); printf(„\n special = %d, other = %d \n“, nwhite, nother); } Deklaracija int ndigit[10];

deklariše ndigit kao polje od deset celobrojnih elemenata. Oznake za elemente polja (indeksi) uvek počinju od nule, tako da polje ndigit ima elemente ndigit[0], ndigit[1], ... , ndigit[9]. Bitno je imati ovo na umu kod pisanja for petlji. Indeks može biti bilo koji celobrojni izraz, celobrojna vrednost (kao, na primer, i) ili celobrojna konstanta. Ovaj program se zasniva na znakovnom predstavljanju cifara. Na primer, test if (c >= '0' && c <= '9') ... određuje da li je znak u promenljivoj c cifra. Ako jeste, brojna vrednost te cifre je c - '0'. Na primer, u ASCII setu znakova brojna vrednost cifre 0 iznosi 48. Ako je u promenljivoj c znak čija je brojna vrednost 52, onda je njegova brojna vrednost c - '0' = 52 - 48 = 4, a to je cifra 4. Ovo funkcioniše ako su '0', '1', itd. pozitivne rastuće vrednosti i ako sem cifara nema nikakvih drugih znakova između '0' i '9'. Srećom, ovo važi za sve uobičajene setove znakova. Po definiciji, ako u izrazu učestvuju promenljive tipa char i int, pre izračunavanja se sve konvertuje u int tip. To će reći da su promenljive i konstante char tipa u suštini identične int tipu, gledano u kontekstu aritmetike. To je sasvim prirodno i odgovara nam; na primer, c - '0' je celobrojni izraz čija je vrednost između 0 i 9 (zavisno od toga koji je znak od '0' do '9' smešten u promenljivoj c), a to je pogodno iskorišćeno za indekse polja ndigit koji takođe idu od 0 do 9. Ispitivanje da li je znak u promenljivoj c cifra, specijalni znak ili neki od preostalih znakova, je izvedeno u delu if (c >= '0' && c <= '9') ++ndigit[c -'0']; else if (c == ' ' || c == '\n' || c == '\t') ++nwhite; else ++nother; Konstrukcija if (uslov 1) iskaz 1 else if (uslov 2) iskaz 2 else iskaz 3 se često pojavljuje da bi iskazala složeno odlučivanje. Izvršava se tako što se linije čitaju od vrha sve dok neki uslov ne bude zadovoljen. Kada se to dogodi, izvršiće se njemu odgovarajući iskaz, i cela konstrukcija se napušta.

Naravno, i ovde iskaz može biti jedan iskaz ili grupa iskaza u vitičastim zagradama. Ako nijedan od uslova nije zadovoljen, iskaz koji stoji posle poslednjeg else u konstrukciji biće izvršen ukoliko postoji. Ako poslednje else i njemu odgovarajući iskaz nisu navedeni (kao što je to slučaj sa programom za brojanje reči), neće se preduzeti nikakva akcija. U konstrukciji može učestvovati proizvoljan broj else if (uslov) iskaz elemenata između početnog if i poslednjeg else . Opet kao pitanje stila, preporučljivo je da se if - else konstrukcije pišu na način na koji smo mi to uradili, da dugačke odluke ne bi prekoračile desnu ivicu strane. Iskaz switch, o kome će biti reči u Poglavlju 3, obezbeđuje drugi način pisanja složenih odluka : on je posebno pogodan za ispitivanja tipa „da li se vrednost nekog celog broja ili znaka poklapa sa nekom iz skupa konstanti“. Nasuprot primeru iz ovog odeljka, u Poglavlju 3 prikazaćemo verziju programa koja koristi switch iskaz. þ Vežba 1 - 11 Napišite program koji štampa histogram dužine reči na ulazu. Najlakše je napraviti horizontalni histogram; vertikalni histogram je mnogo veći izazov. 1.7 FUNKCIJE U C-u, funkcija je ekvivalentna funkciji u Fortranu ili proceduri u Paskalu. Funkcija obezbeđuje pogodan način da se neka izračunavanja obave u tzv. crnoj kutiji, i da se kasnije koriste bez znanja kako su nastala. Funkcije su zaista jedini način da se izađe na kraj sa eventualnom složenošću dugačkih programa. Sa pravilno oblikovanim funkcijama, moguće je ne znati kako je posao obavljen; znanje o tome šta je urađeno je dovoljno. C je napravljen tako da je korišćenje funkcija lako, elegantno i efikasno; često ćete viđati funkciju svega par linija dugačku i pozvanu samo jedanput, ali koja razjašnjava deo programa. Do sada smo koristili samo funkcije kao printf, getchar i putchar koje su nam bile obezbeđene; došlo je vreme da napišemo i sami nekoliko. Pošto u C-u ne postoji operator stepenovanja ** kakav ima, recimo, Fortran, prikačimo postupak kreiranja funkcije pišući funkciju power. Funkcija power(m, n) stepenuje ceo broj m na potenciju n koja je pozitivan ceo broj. Tako je, na primer, vrednost power(2, 5) jednaka 32. Funkcija power, istini za volju, nije široko primenljiva pošto barata samo sa malim pozitivnim potencijama malih celih brojeva, ali je najbolje rešavati problem korak po korak. Evo funkcije power i glavnog dela programa koji je poziva, tako da pred

sobom imate celu strukturu. #include<stdio.h> int power(int m, int n); /* deklaracija funkcije */ main() { int i; for (i = 0; i < 10; ++i) printf(„%d %d %d \n“, i, power(2, i), power(-3, i)); return 0; } /* power: diže osnovu na n - ti stepen; n > 0 */ int power(int base, int n) /* definicija funkcije */ { int i, p; p = 1; for (i = 1; i <= n; ++i) p = p * base; return p; } Svaka funkcija ima isti oblik: ime funkcije(deklaracije parametara, ukoliko oni postoje) { deklaracije iskazi } Funkcije se mogu pojavljivati u bilo kom redosledu, i to u jednoj ili više datoteka. Naravno, ako izvorni program čini više datoteka, potrebno je mnogo više kompajliranja i učitavanja nego da je sve smešteno u jednoj datoteci. Međutim, to je stvar operativnog sistema, a ne stvar jezika. Za trenutak, pretpostavićemo da su obe funkcije u istoj datoteci, tako da sve što ste naučili o startovanju C programa i dalje važi. Funkcija power je pozvana dva puta, u liniji printf(„%d %d %d \n“, i, power(2, i), power(-3, i)); Svaki poziv prosleđuje dva argumenta funkciji power, koja svaki put vraća u program ceo broj koji treba da se štampa. U nekom izrazu, power(2, i) je ceo broj, baš kao što su to i argumenti 2 i i. Ne vraćaju sve funkcije celobrojnu vrednost u program; time ćemo se pozabaviti u Poglavlju 4. U definiciji funkcije power, int power(int base, int n) određeni su tipovi i imena parametara, kao i tip rezultata koji funkcija

vraća u program. Mi ćemo koristiti naziv parametar za promenljivu koja je navedena na listi prilikom definisanja funkcije, a za promenljivu čija se vrednost koristi pri pozivu funkcije koristićemo naziv argument. Sa druge strane, deklaracija int power(int m, int n); kaže da je power funkcija koja očekuje dva int argumenta, a vraća u program vrednost int tipa. Ova deklaracija, koja se zove prototip funkcije, mora da se složi sa definicijom funkcije. Greška je ako se definicija funkcije ili neka njena upotreba ne složi sa deklaracijom. Nije neophodno da se imena parametara u definiciji i u prototipu slažu; u stvari, imena parametara u prototipu funkcije su proizvoljna, pa smo mogli pisati int power(int, int); Imena koja koristi funkcija power za svoje argumente su potpuno lokalna i nisu vidljiva za bilo koju drugu funkciju: druge funkcije mogu bez problema imati ista imena. To važi i za promenljive i i p; promenljiva i u funkciji power nije ni u kakvoj vezi sa promenljivom i koju koristi funkcija main. Vrednost koju funkcija power izračuna je vraćena u program iskazom return. Iskaz return može vratiti bilo kakav izraz naveden u zagradama ( i ). Funkcija ne mora da vraća neku vrednost u program; return iskaz naveden bez izraza samo vraća kontrolu programu koji je pozvao tu funkciju. Isto bi se desilo i da se došlo do kraja tela (a to je desna vitičasta zagrada) pozvane funkcije. Uglavnom, konstrukcija return 0 podrazumeva normalni završetak. þ Vežba 1 -12 Napišite program koji konvertuje ulaz u mala slova, koristeći funkciju lower(c) koja vraća c ako c nije slovo, odnosno vrednost koja odgovara malom slovu, ako je c veliko slovo. 1.8 ARGUMENTI - POZIV POMO�U VREDNOSTI Jedna karakteristika C funkcija može biti veoma čudna programerima koji poznaju neke druge jezike, posebno Fortran. U C-u funkcije, umesto da međusobno komuniciraju svojim argumentima, komuniciraju njihovim vrednostima. To znači da funkcija koja je pozvana čuva vrednosti svojih argumenata u privremenim promenljivama (tehnički, na steku) umesto u originalima (t.j. na njihovim adresama). To vodi nekim drugim osobinama od onih koje su poznate u Fortranu ili Paskalu, u kojima pozvana funkcija smešta izračunate vrednosti na adresu originalnog argumenta t.j. barata sa argumentom, a ne sa njegovom vrednošću. Osnovna razlika između jezika je ta da u C-u pozvana funkcija ne može da

izmeni vrednost promenljive u funkciji iz koje je pozvana; ona može menjati jedino njenu privremenu kopiju. Pozivanje pomoću vrednosti je prednost, nikako mana. Ono obično vodi sažetijim programima sa manje različitih promenljivih, jer se argumenti mogu tretirati kao pogodno obeležene lokalne promenljive u pozvanoj funkciji. Na primer, evo verzije funkcije power koja koristi ovu osobinu: /* power: diže osnovu na n - ti stepen; druga verzija */ int power(int base, int n) { int p; for(p = 1; n > 0; --n) p = p * base; return p; } Argument n je upotrebljen kao privremena promenljiva koja se smanjuje dok ne dođe do nule; nema više potrebe za promenljivom i. Žta god da se učini sa promenljivom n unutar funkcije power nema uticaja na argument sa kojim je funkcija power pozvana. Kada je to potrebno, moguće je obezbediti da pozvana funkcija menja promenljivu u funkciji iz koje je pozvana. Tada pozvana funkcija mora da zna adresu originalnog argumenta, a to mora da obezbedi funkcija iz koje je pozvana. Tehnički, to se izvodi pomoću pokazivača koji pokazuje na adresu argumenta. Pozvana funkcija takođe treba da deklariše pokazivač i tako preko njega pristupi promenljivoj u funkciji iz koje je pozvana. Ovo ćemo objasniti do detalja u Poglavlju 5. Sa poljima je stvar sasvim drugačija. Kada se polje pojavi kao argument, nema stvaranja privremenih kopija; pozvanoj funkciji se prosleđuje lokacija (adresa) početnog elementa tog polja. Tako pozvana funkcija može da pristupi bilo kom elementu polja i da ga izmeni. To je tema sledećeg odeljka. 1.9 POLJA ZNAKOVA Verovatno najčešći tip polja u C-u je polje znakova. Da bismo prikazali upotrebu polja znakova i funkcija koje njima manipulišu, napišimo program koji čita skup linija i štampa najdužu od njih. Algoritam je dovoljno jednostavan : while(ima još linija) if (linija je duža od dosad najduže) upamti tu liniju i njenu dužinu

štampaj najdužu liniju Ovaj algoritam jasno pokazuje da se program prirodno deli na više delova. Jedan deo čita liniju, drugi je ispituje, treći pamti i ostatak upravlja procesom. Pošto su stvari tako dobro podeljene, bilo bi dobro da ih tako i napišemo. U skladu sa tim, najpre napišimo odvojeno funkciju getline koja uzima sledeću liniju sa ulaza; ona je uopštenje funkcije getchar. Da bismo funkciju učinili upotrebljivom i u drugim situacijama, pokušaćemo da je učinimo fleksibilnom što je moguće više. Najmanje što funkcija getline mora da radi je da vrati u program signal o kraju skupa linija sa ulaza; mnogo korisnije bi bilo napisati je tako da u program vraća dužinu linije sa ulaza, ili nulu ako je došlo do kraja skupa linija na ulazu. Nula ne može biti smatrana podatkom o dužini linije pošto svaka linija mora imati bar jedan znak; čak i linija koja ima samo znak za novi red je dužine jedan. Kada otkrijemo liniju dužu od prethodne najduže, ona se mora sačuvati negde. To nas upućuje na sledeću funkciju, copy, koja sprema novu najdužu liniju na sigurno mesto. Konačno, potreban nam je glavni program koji će upravljati funkcijama getline i copy. Evo rezultata. #include<stdio.h> #define MAXLINE 1000 /* max dužina linije */ int getline(char line[], int maxline); void copy(char to[], char from[]); /* štampanje najduže linije sa ulaza */ main() { int len; /* dužina tekuće linije */ int max; /* do sada najveća dužina linije */ char line[MAXLINE]; /* tekuća linija */ char longest[MAXLINE]; /* do sada najduža linija */ max = 0; while( (len = getline(line, MAXLINE)) > 0) if (len > max) { max = len; copy(longest, line); } if (max > 0) /* ima još linija na ulazu */ printf(„%s“, longest); return 0; } /* getline: učitava liniju u polje s, vraća njenu dužinu */ int getline(char s[], int lim)

{ int c, i; for (i = 0;i<lim-1 && (c = getchar()) != EOF && c != '\n'; ++i) s[i] = c; if (c == '\n') { s[i] = c; ++i; } s[i] = '\0'; return i; } /* copy: kopira polje 'from' u 'to'; 'to' mora biti dovoljno veliko */ void copy(char to[], char from[]) { int i; i = 0; while ( (to[i] = from[i]) != '\0') ++i; } Funkcije getline i copy su uvedene na početku programa i za njih pretpostavljamo da su u jednoj datoteci. Funkcije main i getline komuniciraju preko nekoliko argumenata i vraćene vrednosti. Argumenti funkcije getline su deklarisani linijom int getline(char s[], int lim) koja određuje da je prvi argument s polje, a drugi, lim, ceo broj. Dužina polja s nije definisana funkciji getline pošto je već definisana u funkciji main. Ova linija takođe pokazuje da funkcija getline vraća u program vrednost tipa int (kako je int podrazumevani povratni tip, može se izostaviti). Funkcija getline koristi iskaz return da vrati vrednost funkciji iz koje je pozvana, baš kao što je to činila i funkcija power. Neke funkcije vraćaju u program korisnu vrednost; druge funkcije, kao što je copy, se koriste samo da bi obavile neki posao i ne vraćaju nikakve vrednosti. Povratni tip funkcije copy je void, koji govori da funkcija ne vraća nikakvu vrednost u program. Funkcija getline postavlja znak \0 (znak čija je odgovarajuća vrednost nula) na kraj polja koje formira, kako bi označila kraj niza znakova. Ovu konvenciju takođe koristi i C kompajler: kada je u programu napisana znakovna konstanta kao „hello\n“ kompajler kreira polje znakova koje sadrži znake niza hello\n i na kraj tog polja ubacuje znak \0. Na taj način funkcija kakva je, recimo, printf može pronaći kraj niza koji treba da odštampa: h e l l o \n \0

Specifikacija %s u funkciji printf očekuje da odgovarajući argument bude niz predstavljen u ovoj formi. Ako pažljivo pogledate funkciju copy, primetićete da se i ona oslanja na činjenicu da je njen ulazni argument, niz from završen znakom \0. Ona kopira taj znak (nakon ostalih) na kraj izlaznog argumenta, niza to. Sve navedeno podrazumeva da znak \0 nije deo originalnog teksta. Valja usput napomenuti da čak i tako mali program kakav je ovaj naš otkriva neke veće nedostatke. Na primer, šta će učiniti funkcija main ako naiđe na liniju dužu od dozvoljene granice? Funkcija getline radi ispravno,jer prestaje da uzima nove znakove sa izlaza čim je polje puno, čak i ako do tog trenutka nije naišla na znak za novi red. Ispitujući dužinu niza i poslednji znak koji je učitan sa ulaza, funkcija main bi mogla da ustanovi da li je linija sa ulaza bila predugačka i da zatim eventualno nešto preduzme. U interesu sažetosti, ignorisali smo spor. Ne postoji način da korisnik funkcije getline predvidi koliko će biti duga linija na ulazu, pa stoga funkcija getchar pazi da ne dođe do prekoračenja. Sa druge strane, korisnik funkcije copy već zna (ili to može saznati) koliko su dugi nizovi sa kojima se barata, pa stoga nismo u ovu funkciju ugradili proveru greške. þ Vežba 1 - 13 Izmenite funkciju main tako da program korektno štampa linije proizvoljne dužine i što je moguće više teksta. þ Vežba 1 - 14 Napišite program za štampanje svih linija dužih od osam znakova. þ Vežba 1 - 15 Napišite program koji izbacuje blanko znakove i tabulatore iz linija sa ulaza, i koji briše linije koje sadrže samo blanko znakove. þ Vežba 1 - 16 Napišite funkciju reverse(s) koja okreće naopačke niz znakova s. Koristite je da napišete program koji obrće linije sa ulaza. 1.10 SPOLJAŽNJE PROMENLJIVE I PODRUšJA Promenljive u funkciji main (max, len, itd.) su lokalne ili sopstvene promenljive za tu funkciju. Zbog toga što su deklarisane unutar funkcije main, nijedna druga funkcija nema direktan pristup do njih. Isto važi i za promenljive u drugim funkcijama; na primer, promenljiva i iz funkcije getline nije ni u kakvoj vezi sa promenljivom i iz funkcije copy. Svaka lokalna promenljiva počinje da postoji tek kada se pozove funkcija u kojoj se ona nalazi, i nestaje na izlazu iz funkcije. To je razlog zbog koga ovakve promenljive imaju naziv automatske promenljive. (U Poglavlju 4 se diskutuje o static klasi promenljivih, u kojoj lokalne promenljive zadržavaju svoje vrednosti između poziva funkcije).

Zbog toga što automatske promenljive nastaju i nestaju sa pozivom funkcije, to one ne zadržavaju svoju vrednost između dva poziva funkcije. Stoga one moraju biti postavljene na neku vrednost svaki put kada se ulazi u funkciju. Ako im nisu dodeljene vrednosti, sadržaće neku nepoznatu, proizvoljnu vrednost. Kao alternativu automatskim promenljivama, moguće je definisati promenljive koje su spoljašnje za sve funkcije, t.j. promenljive kojima se može pristupiti pozivanjem bilo koje funkcije koja njima barata. Zbog toga što su spoljasnje promenljive svima pristupačne, one mogu biti korišćene umesto liste argumenata prilikom komunikacije između dve funkcije. Osim toga, pošto postoje neprekidno (umesto da nastaju i nestaju sa pozivom i završetkom funkcije), ove promenljive zadržavaju vrednost koju im je funkcija dala čak i kad se ta funkcija završi. Spoljašnja promenljiva mora biti definisana izvan svih funkcija; time je određena njena adresa. Ta promenljiva mora takođe biti deklarisana u svakoj funkciji koja želi da joj pristupi. Deklaracija mora biti jasan extern iskaz ili iskaz razumljiv iz konteksta. Da bismo diskusiju konkretizovali, napišimo program za štampanje najduže linije sa ulaza, ovaj put koristeći line, longest i max kao spoljašnje promenljive. To zahteva izmenu poziva, deklaracija i tela sve tri funkcije. #include<stdio.h> #define MAXLINE 1000 /* max dozvoljena dužina linije */ int max; /* do sada najveća dužina linije */ char line[MAXLINE]; /* tekuća linija */ char longest[MAXLINE]; /* do sada najduža linija */ int getline(void); void copy(void); /* štampa najdužu liniju; specijalna verzija */ main() { int len; extern int max; extern char longest[]; max = 0; while ( (len = getline()) > 0) if (len > max) { max = len; copy(); } if (max > 0) /* ima linija */ printf(„%s“, longest);

return 0; } /* getline: specijalna verzija */ int getline(void) { int c, i; extern char line[]; for (i = 0;i<MAXLINE-1 && (c=getchar()) != EOF && c != '\n';++i) line[i] = c; if (c == '\n') { line[i] = c; ++i; } line[i] = '\0'; return i; } /* copy: specijalna verzija */ void copy(void) { int i; extern char line[], longest[]; i = 0; while ( (longest[i] = line[i]) != '\0') ++i; } Spoljašnje promenljive u funkcijama main, getline i copy su definisane prvim linijama gornjeg primera koje određuju njihov tip i, s obzirom na iskaz extern, rezervišu mesto u memoriji za čuvanje vrednosti koje se u te promenljive odlažu. Gramatički gledano, definicije spoljnih promenljivih su slične deklaracijama koje smo dosad koristili, ali pošto se te promenljive pojavljuju van funkcija, imaće karakter spoljnih promenljivih. Ime spoljne promenljive mora biti poznato funkciji pre nego što funkcija hoće da je upotrebi. Jedan način da se to uradi je da se navede extern deklaracija unutar funkcije; deklaracija ima isti oblik kao i do sada, s tim što joj prethodi ključna reč extern. U izvesnim slučajevima, extern deklaracija može biti suvišna; ako se definicija spoljne promenljive pojavi u programu pre nego što je upotrebljena u nekoj funkciji, onda nema potrebe za extern deklaracijom unutar funkcije. Na taj način, extern deklaracije u funkcijama main, getline i copy su opcione. U stvari, uobičajena je praksa da se sve definicije spoljnih promenljivih stave na početak izvorne datoteke, a onda se izostave sve extern deklaracije u funkcijama. Ako je program napisan u obliku više razdvojenih delova, a neka promenljiva napisana u datoteci 1 se koristi u datoteci 2, onda je neophodna extern deklaracija u datoteci 2 da bi se povezalo pojavljivanje promenljive

u dat. 2 sa njenom definicijom u dat. 1 . U praksi se obično sakupe sve extern deklaracije promenljivih i sve deklaracije funkcija u jednu posebnu datoteku zvanu zaglavlje, i koja se uvodi #include instrukcijom ispred svake od datoteka. Ova tema biće opisana detaljno u Poglavlju 4. Trebalo bi da ste primetili da koristimo termine definicija i deklaracija veoma pažljivo kad govorimo o spoljnim promenljivama u ovom odeljku. Termin 'definicija' označava mesto gde je promenljiva stvorena ili gde je za nju odvojen prostor u memoriji; termin 'deklaracija' označava mesto gde je utvrđena priroda promenljive ali za nju nije odvojen nikakav prostor. Uzgred, postoji tendencija da se sve svede na extern promenljivu jer ispada da ona pojednostavljuje komunikacije - liste argumenata su kraće i promenljive su uvek tamo gde su vam potrebne. Ali, spoljne promenljive su tu čak i kad vam nisu potrebne. Ovakav način programiranja nosi u sebi opasnost, jer vodi ka programima u kojima povezanost između pojedinih delova uopšte nije očigledna - promenljive mogu biti izmenjene sasvim neočekivano i neprimetno, a sam program će biti teško ispraviti ako to bude neophodno. Druga verzija programa za štampanje najduže linije sa ulaza je slabija u odnosu na prvu, delom zbog pomenutih razloga, a delom zato što uništava univerzalnost dve korisne funkcije ubacujući u njih imena promenljivih kojima će manipulisati. þ Vežba 1 - 17 Test u for petlji funkcije getline je prilično konfuzan. Izmenite program tako da postane jasniji, ali da se isto ponaša kad naiđe na kraj linije ili kad se polje popuni. Da li je ovakvo ponašanje najopravdanije? 1.10 ZAKLJUšAK Do ovog mesta smo obuhvatili sve što bi se moglo nazvati konvencionalnom suštinom C-a . Sa ovim mnoštvom izgrađenih funkcija moguće je pisati programe razumne dužine, i verovatno je dobra ideja da tako i uradite. Primeri koji slede treba da vam daju ideje za programe nešto veće složenosti od onih prikazanih u ovom Poglavlju. Kada savladate dosad izloženi deo C-a, vredeće truda da nastavite sa čitanjem sledećih nekoliko poglavlja, gde snaga i izražajnost jezika postaju uočljivi. þ Vežba 1 - 18 Napišite program detab koji zamenjuje znak tabulatora sa ulaza sa odgovarajućim brojem blanko znakova do sledećeg tabulatora. Usvojite određenu dužinu tab pozicija, recimo n znakova. þ Vežba 1- 19 Napišite program entab koji zamenjuje niz blanko znakova minimalnim brojem tabulatora i blanko znakova tako da ostvarite isti razmak.

Uzmite istu dužinu tab pozicije kao u prethodnoj vežbi. þ Vežba 1 - 20 Napišite program koji izbacuje sve komentare iz nekog C programa. Nemojte zaboraviti da pravilno tretirate nizove pod navodnicima i znakovne konstante. þ Vežba 1 - 21 Napišite program koji proverava neki C program i traži u njemu najosnovnije gramatičke greške kao što su nezatvorene zagrade. Ne zaboravite jednostruke i dvostruke navodnike i komentare. P o g l a v l j e 2 : TIPOVI, OPERATORI I IZRAZI Promenljive i konstante su osnovni oblici podataka kojima se operiše u programu. Deklaracije prokazuju listu promenljivih koje će biti upotrebljene, određuju kog su tipa i, eventualno, koje su im početne vrednosti. Operatori određuju koje operacije nad podacima treba da se urade. Izrazi kombinuju promenljive i konstante da bi proizveli nove vrednosti. Tip nekog objekta određuje skup vrednosti koje on može imati i operacije koje se mogu primeniti na njemu. ANSI standard je napravio mnogo neznatnih izmena i dodataka osnovnim tipovima i izrazima. Uvedene su signed (predznačene) i unsigned (nepredznačene) forme za sve tipove celobrojnih promenljivih (int, char, ...), i obeležavanje konstanti tipa unsigned i heksadecimalnih znakovnih konstanti. Operacije nad realnim brojevima mogu se uraditi sa jednostrukom tačnošću; takođe, postoji tip long double za povećanu tačnost. Nizovi konstanti se mogu povezivati u toku kompajliranja. Enumeracije su postale deo jezika. Objekti mogu biti deklarisani kao const, što će ih zaštititi od promena. Pravila za automatsku konverziju između tipova su proširena da bi se operisalo sa većim brojem tipova. 2.1 IMENA PROMENLJIVIH Iako to nismo istakli u Poglavlju 1, postoje izvesna ograničenja u izboru imena promenljivih i imena simboličkih konstanti. Imena su sastavljena od slova i brojeva; ime mora početi slovom. Znak _ se računa kao slovo; ima svoju primenu kod dugačkih imena promenljivih gde povećava čitljivost programa. Velika i mala slova se razlikuju, pa tako x i X predstavljaju dva različita imena. Praksa u C-u je da se mala slova koriste za imena promenljivih, a velika za imena simboličkih konstanti.

Barem prvih 31 znakova nekog imena su značajni. Za imena funkcija i spoljnih promenljivih broj mora biti manji od 31, zato što imena spoljnih promenljivih mogu biti korišćena od strane različitih asemblera i učitavača. Kod spoljnih imena standard garantuje jednoznačnost samo za prvih šest znakova. Ključne reči kao što su if, else, int, float itd. su rezervisane; ne možete ih koristiti kao imena promenljivih (ključne reči moraju biti napisane malim slovima). Preporučljivo je izabrati takva imena promenljivih koja se odnose na namenu promenljive, a da nisu tipografski slična. Namera nam je da koristimo kraća imena za lokalne promenljive, naročito za petlje, a duža za spoljne promenljive. 2.2 TIPOVI I VELIšINE PODATAKA U C-u postoji samo nekoliko osnovnih tipova podataka: char jedan bajt, može čuvati jedan znak iz lokalnog skupa znakova int ceo broj; obično je one veličine koje je predviđeno da budu celi brojevi na konkretnom računaru float realan broj jednostuke tačnosti double realan broj dvostruke tačnosti Uz to, postoje i kvalifikatori koje pridrujemo ovim osnovnim tipovima: tipu int se mogu dodati short, long i unsigned. Deklaracija za kvalifikatore izgleda ovako: short int x; long int y; unsigned int z; Reč int se može izostaviti u ovakvim deklaracijama, mada se uglavnom piše. Kvalifikatori short i long treba da obezbede različite dužine celih brojeva tamo gde bi to imalo praktičnu svrhu; int će biti veličine predviđene za taj računar. Tip short je obično veličine dva bajta, long četiri, a int dva ili četiri bajta. Svaki kompajler ima slobodu da izabere veličine koje su pogodne za hardver tog kompjutera, uz ograničenje da short i int budu veličine najmanje dva bajta, a long najmanje četiri. Uz to, short ne sme biti veći od int, a int ne sme biti veći od long celog broja. Kvalifikatori signed i unsigned mogu biti pridruženi tipovima int i char. Celi brojevi tipa unsigned int su uvek pozitivni i obuhvataju opseg od 0 do 2^n, gde je n broj bitova u int-u. Ako je int veličine dva bajta, unsigned int će predstavljati cele brojeve od nule do 65535 (2^16), a signed int će predstavljati brojeve od -32768 do 32767. Kod signed int brojeva najviši bit služi za određivanje znaka tog broja (1 ako je broj negativan, 0

ako je pozitivan), a ostali bitovi predstavljaju sam broj. Isto važi i za promenljive tipa char: ako je char veličine jednog bajta, promenljive tipa unsigned char imaju vrednosti između 0 i 255, dok promenljive tipa signed char imaju vrednosti između -128 i 127. Bilo da je promenljiva tipa unsigned ili signed char, vrednost znaka koji se štampa se uvek tretira kao pozitivna. Tip long double predstavlja realne brojeve uvećane tačnosti. Kao i kod celih brojeva, i veličine realnih brojeva mogu biti definisane na više načina: float, double i long double mogu predstavljati jednu, dve ili tri različite veličine. Standardna zaglavlja <limits.h> i <float.h> sadrže u sebi simboličke konstante za sve te veličine, zajedno sa ostalim osobinama računara i kompajlera. O tome više u dodatku B. 2.3 KONSTANTE Celobrojna konstanta, kao npr. 1234, je tipa int. Konstanta tipa long se piše sa l ili L na kraju, kao npr. 123456789L; ceo broj koji je suviše veliki da bi se prikazao int tipom konstante biće preveden u long tip. Konstante tipa unsigned pišu se sa u ili U na kraju, a sufiks ul ili UL označava konstantu tipa unsigned long. Konstante u obliku realnih brojeva sadrže decimalnu tačku (123.4) ili eksponent (12e-3, 12E-3) ili i jedno i drugo; njihov tip double, osim ako nemaju sufiks na kraju. Sufiksi f ili F označavaju float konstantu; sufiksi l ili L označavaju long double konstantu. Postoji notacija za oktalne i heksadecimalne brojeve. Nula (0) na početku int konstante znači da je broj predstavljen u oktalnom sistemu brojeva, a 0x ili 0X da je reč o heksadecimalnom broju. Na primer, broj 31 će biti predstavljen kao 037 u oktalnom, i kao 0x1f ili 0X1F u heksadecimalnom sistemu brojeva. Heksadecimalne i oktalne konstante se takođe mogu izraziti u long formi ako iza njih sledi slovo L, ili u unsigned formi ako iza njih stoji U: 0xFUL je unsigned long heksadecimalna konstanta koja odgovara decimalnoj vrednosti 15. Znakovna konstanta je znak naveden između jednostrukih navodnika, na primer 'x'. Svakom znaku odgovara jedna numerička vrednost, a koja će to biti zavisi od toga koji set znakova računar koristi. Na primer, u ASCII setu znakova, znak nula t.j. '0' ima svoju odgovarajuću vrednost 48, dok u EBCDIC skupu znaku '0' odgovara vrednost 240; vrednosti u oba skupa očito nemaju veze sa brojnom vrednošću nula. Pišući '0' umesto konkretnih vrednosti kakve su 48 ili 240, činimo program nezavisnim od seta karaktera koji je primenjen na svakom pojedinačnom računaru. Znakovne konstante se tretiraju u izračunavanjima kao i bilo koji drugi brojevi, iako se najviše koriste u relacijama poređenja sa drugim znakovima. Sledeći odeljak se bavi pravilima

konverzije. Određeni nevidljivi znakovi mogu biti predstavljeni kao znakovne konstante pomoću tzv. eskejp sekvenci kao što su \n (znak za novi red), \t (tabulator), \0 (nulti znak), \\ (obrnuta kosa crta), \' (jednostruki navodnik) itd. Ovako napisani izgledaju kao dva znaka, ali je to u suštini samo jedan znak. Uz to, moguće je stvoriti proizvoljan element veličine jednog bajta pišući '\ooo' gde je ooo jedna do tri oktalne cifre (0...7),ili kao '\xhh' gde je hh jedna ili više heksadecimalnih cifara (0...9, a..f,A...F). Tako možemo pisati #define FORMFEED '\014' /* ASCII form feed */ ili u heksadecimalnom kodu #define FORMFEED '\xE' /* ASCII form feed */ Kompletan set eskejp sekvenci je \a znak za zvučni signal \\ obrnuta kosa crta \b povratnik \? znak pitanja \f form feed \' jednostruki navodnik \n novi red \“ dvostruki navodnik \r carriage return \ooo oktalni broj \t horizontalni tabulator \xhh heksadecimalni broj \v vertikalni tabulator Znakovna konstanta '\0' predstavlja znak čija je odgovarajuća numerička vrednost nula. šesto pišemo '0' umesto 0 da bi naglasili znakovnu prirodu nekog izraza. Konstantni izraz je izraz u kome figurišu samo konstante. Takvi izrazi se mogu izračunati još za vreme kompajliranja umesto da se računaju u toku izvršavanja programa. U skladu sa svojom prirodom, mogu se pojaviti na bilo kom mestu u programu gde to može i konstanta. Na primer, kao u #define MAXLINE 1000 char line[MAXLINE+1]; ili #define LEAP 1 /* u prestupnim godinama */ int dani[31+28+LEAP+31+30+31+30+31+31+30+31+30+31];

Niz znakova ili string je niz od nula ili više znakova naveden unutar dvostrukih navodnika, kao „I am a string „ ili kao „„ /* string dužine nula */ Dvostruki navodnici nisu deo niza, već su tu da bi ga ograničili. Iste eskejp sekvence korišćene za znakovne konstante primenjuju se i na stringove: \“ predstavlja znak dvostruki navodnik. Nizovi znakova se mogu povezati za vreme kompajliranja „hello,“ „world“ je isto što i „hello,world“ Ovo je korisno kod dugih nizova jer se mogu podeliti u više osnovnih linija. Tehnički, string je polje čiji su elementi pojedinačni znakovi. Kompajler prema dogovoru automatski stavlja znak \0 na kraj svakog takvog stringa, kako bi programi mogli da znaju gde se string završava.Ovakvo predstavljanje znači da nisu postavljena ograničenja koliko string može biti dug, pa programi moraju da pretraže kompletan string da bi odredili njegovu dužinu. Broj lokacija u memoriji u koje se smešta string je veći od broja znakova navedenih između dvostrukih navodnika za jedan. Sledeća funkcija strlen(s) vraća dužinu stringa s ne uključujući znak \0. /* strlen: vraca dužinu stringa s */ int strlen(char s[]) { int i; i = 0; while (s[i] != '\0') ++i; return i; } Ostale funkcije koje operišu sa nizovima i funkcija strlen su deklarisane u standardnom zaglavlju <string.h>. Pažljivo razgraničite između znakovne konstante i stringa koji sadrži samo jedan karakter: 'x' nije isto što i „x“. Prvo je jedan znak, i koristi se da proizvede numeričku vrednost koja odgovara znaku x iz seta znakova, a drugo je niz znakova koji sadrži jedan znak (slovo x) i \0. Postoji i jedna druga vrsta konstanti, tzv. enumerisana konstanta.

Enumeracija je formiranje liste konstantnih celobrojnih vrednosti, kao u enum boolean { NO, YES } ; Prvi naziv u enum listi ima vrednost 0, sledeći 1, itd. dokle god se eksplicitno ne zada neka druga vrednost. Ako nisu sve vrednosti u listi zadate, one koje nisu zadate progresivno rastu od poslednje zadate vrednosti, kao u drugom od sledeća dva primera: enum escapes { BELL = '\a', BACKSPACE = '\b', TAB = '\t', NEWLINE = '\n', VTAB = '\v', RETURN = '\r' } ; enum meseci { JAN = 1, FEB, MAR, APR, MAJ, JUN, JUL, AVG, SEP, OKT, NOV, DEC } ; /* FEB je 2, MAR je 3, itd. */ Imena u različitim enumeracijama moraju se razlikovati. Vrednosti u jednoj enumeraciji se ne moraju razlikovati. Enumeracije obezbeđuju pogodan način da pridruže konstantne vrednosti imenima, kao alternativu za #define, uz prednost da nove vrednosti mogu biti generisane automatski. Kompajleri ne moraju proveravati da li je to što je smešteno u promenljivu tipa enum ispravno za enumeraciju. Ipak, enumerisane promenljive pružaju mogućnost provere zbog čega su često bolje od #define. Uz to, dibager je u mogućnosti da štampa vrednosti enumerisanih promenljivih u njihovoj simboličkoj formi. 2.4 DEKLARACIJE Sve promenljive moraju biti deklarisane pre korišćenja, mada neke deklaracije mogu biti izvedene tako da slede iz konteksta. Deklaracija navodi tip promenljive iza koga sledi lista od jedne ili više promenljivih tog tipa, kao u int lower, upper, step; char c, line[1000]; Promenljive mogu biti raspoređene po listama u bilo kakvom rasporedu: poslednji primer je mogao biti napisan kao int lower; int upper; char c; int step; char line[1000];

Poslednja forma zauzima mnogo više prostora, ali je veoma pogodna za dodavanje komentara svakoj deklaraciji ili za česte izmene programa. Promenljive takođe mogu biti postavljene na neke vrednosti unutar deklaracija, mada tu postoje izvesna ograničenja. Ako se u deklaraciji iza imena neke promenljive navedu znak jednakosti i konstantni izraz, taj deo će biti protumačen kao inicijalizator te promenljive, kao u char backslash ='\\'; int i = 0; float eps = 1.0e-5; int limit = MAXLINE + 1; Ako promenljiva nije automatska (nego je extern ili static tipa), inicijalizacija se vrši samo jednom, obično pre početka programa, a inicijalizator mora biti konstantni izraz. Eksplicitno inicijalizovane automatske promenljive se inicijalizuju svaki put kada se pozove funkcija u kojoj se nalaze. Inicijalizator može biti bilo kakav izraz. One automatske promenljive za koje nije eksplicitno navedena početna vrednost, po aktiviranju funkcije sadržaće nedefinisane, proizvoljne vrednosti. Ako im se eksplicitno ne dodeli neka vrednost, promenljive tipa extern i static imaće početnu vrednost nula. Ipak, dobro je i u tom slučaju naglasiti inicijalizaciju. Kvalifikator const može biti naveden ispred deklaracije bilo koje promenljive da bi naglasio da se ona neće menjati. Za polje, na primer, kvalifikator const pokazuje da se njegovi elementi neće menjati. const double e = 2.71828182845905; const char msg[] = „pažnja: „; Kvalifikator const se može primeniti i na argumente funkcije, da bi naznačio da ih funkcija neće menjati. Kada je argument funkcije polje, onda da ga funkcija ne bi izmenila piše se int strlen(const char s[]); Rezultat je jasno određen ako dođe do pokušaja promene objekta deklarisanog kao const . 2.5 ARITMETIšKI OPERATORI Binarni (primenjuju se na dva operanda) aritmetički operatori su +, -, *, /, i modul operator % . Postoji unarni operator -, ali nema unarnog operatora +.

Deljenje dva cela broja daje ceo broj i ostatak koji se odbacuje. Izraz x % y će proizvesti ostatak deljenja vrednosti x vrednošću y. Ako y deli x tačno ceo broj puta, gornji izraz daće nulu. Modul operator deljenja je upotrebljen u sledećem primeru: godina je prestupna ako je deljiva sa 4 a nije deljiva sa 100, ili ako je deljiva sa 400 . Može se napisati if (year % 4 == 0 && year % 100 != 0 || year % 400 == 0) printf(„leap year“); else printf(„not a leap year“); Operator % ne može biti primenjen na vrednosti tipa float ili double. Operatori + i - imaju isti prioritet pri izračunavanju izraza, i on je niži od prioriteta operatora * , / i % , a koji su, opet, nižeg prioriteta od unarnog -. Aritmetički operatori su asocijativni sleva nadesno. Tabela na kraju ovog poglavlja prikazuje prioritet i asocijativnost za sve operatore. Za asocijativne i komutativne operacije kakve su sabiranje i množenje, redosled izračunavanja nije određen. Kompajler može preurediti izraz koji sadrži ove operacije. Tako, a + (b + c) može biti izračunato kao (a +b) + c. Akcija koja se preduzima kada dođe do prekoračenja rezultata u bilo kom pravcu, zavisi od računara do računara. 2.6 RELACIONI I LOGIšKI OPERATORI Relacioni operatori su > >= < <= i svi imaju isti prioritet. Odmah ispod njih po prioritetu su operatori jednakosti == != koji imaju isti prioritet. Relacioni operatori imaju niži prioritet od aritmetičkih operatora, pa će izraz kakav je i < lim-1 biti protumačen kao i < (lim-1), što se i moglo očekivati. Mnogo interesantniji su logički operatori && i ||. Izrazi povezani operatorima && i || se izračunavaju sleva nadesno, a izračunavanje se zaustavlja čim istinitost ili neistinitost rezultata postane poznata. Ova

pravila su od kritičnog značaja za pisanje ispravnih programa. Na primer, evo petlje iz funkcije getline koju smo napisali u Poglavlju 1: for (i = 0; i<lim-1 && (c = getchar()) != '\n' && c != EOF; ++i) s[i] = c; Jasno, pre čitanja novog znaka sa ulaza treba proveriti ima li u polju s mesta za njegovo smeštanje, pa stoga test i < lim-1 mora biti ispitan prvi. Ne samo to, nego ako ovaj test nije zadovoljen, nema potrebe da se dalje ispituju drugi testovi: istinitost (tačnost) rezultata je poznata i cela petlja se već tu okončava. Slično tome, bilo bi nelogično da se testira da li je c znak za kraj linije a da se prethodno nije zvala funkcija getchar. Poziv funkcije getchar mora biti izveden pre nego što znak u promenljivoj c bude testiran. Prioritet operatora && je veći od prioriteta operatora ||, a oba prioriteta su niža od prioriteta koji imaju relacioni operatori i operatori jednakosti. Tako izrazi kao što je i < lim-1 && (c = getchar()) != '\n' && c != EOF ne zahtevaju dodatne zagrade. Ali, kako je prioritet operatora != veći od prioriteta operatora dodeljivanja (=), to su zagrade u izrazu (c = getchar()) != '\n' neophodne da bi se obezbedilo prvo dodeljivanje vrednosti promenljivoj c, a zatim upoređivanje te vrednosti sa vrednošću '\n' . Po definiciji, numerička vrednost relacionog ili logičkog izraza je jednaka 1 ako je relacija tačna (istinita), odnosno jednaka 0 ako je relacija netačna. Operator unarne negacije ! konvertuje vrednost istinitog operanda (tj. 1) u nulu, a vrednost neistinitog operanda u 1. Najčešća upotreba operatora ! je u konstrukcijama kao što je if ( !inword ) umesto if ( inword == 0 ) Teško je reći koji je oblik bolji. Konstrukcije kao !inword se sasvim lepo čitaju ('ako nije ...'), ali bi se komplikovaniji izrazi teže razumeli. Testove koji zahtevaju kombinaciju operatora &&, ||, ! ili zagrada treba u principu izbegavati.

þ Vežba 2.1 Napišite petlju ekvivalentnu gornjoj petlji for, ali bez korišćenja operatora && i ||. 2.7 KONVERZIJE TIPOVA Kada se u izrazu pojave operandi različitih tipova, oni se konvertuju u zajednički tip u skladu sa manjim brojem pravila. U celini, jedine konverzije koje se odigravaju automatski su one koje ne dovode do gubitka informacija i koje imaju smisla, kao što je pretvaranje celog broja u realni u izrazu tipa 'realni broj + ceo broj'. Izrazi koji nemaju smisla, kao što je uzimanje realnog broja za indeksiranje polja, nisu dozvoljeni. Izrazi koji dovode do gubitka informacija, kao što je dodeljivanje vrednosti dužeg tipa kraćem, ili tipa realnih brojeva tipu celih brojeva, nisu nedozvoljeni! Najpre, tipovi char i int mogu se slobodno tretirati na isti način u aritmetičkim izrazima: char tip u nekom izrazu se automatski konvertuje u int tip. Ovo dozvoljava primenu kod određenih transformacija sa znakovima. Jedan takav primer je i funkcija atoi, koja konvertuje niz cifara u odgovarajući numerički ekvivalent. /* atoi: konverzija niza s u ceo broj */ int atoi(char s[]) { int i, n; n = 0; for (i = 0; s[i] >= '0' && s[i] <= '9'; ++i) n = 10 * n + (s[i] - '0'); return n; } Kao što je pomenuto u Poglavlju 1, izraz s[i] - '0' daje numeričku vrednost znaka smeštenog u s[i]. Odatle se vidi da je char tip polja s tretiran u izrazu kao int tip da bi se izračunala vrednost promenljive n koja je int tipa. Još jedan primer konverzije char tipa u int tip je funkcija lower koja pretvara velika slova u mala isključivo za ASCII set znakova. Ako znak nije veliko slovo, funkcija lower ga vraća neizmenjenog. /* lower: konverzija velikih u mala slova ; ASCII set */ int lower(int c) { if (c >= 'A' && c <= 'Z')

return c + 'a' - 'A'; else return c; } Ova funkcija ispravno radi za ASCII set znakova, jer je kod tog seta fiksno rastojanje između numeričke vrednosti malog slova i numeričke vrednosti njemu odgovarajućeg velikog slova. Takođe, abeceda je neprekidna - između A i Z nema ničeg osim slova. Poslednja primedba ne važi za EBCDIC set znakova, pa ova funkcija greši kod računara koji imaju ugrađen ovaj set znakova: biće konvertovani i znaci koji nisu slova. Standardno zaglavlje <ctype.h>, opisano u Dodatku B, definiše familiju funkcija koje obezbeđuju test i konverziju u zavisnosti od seta znakova. Na primer, funkcija tolower(c) vraća vrednost malog slova ako je u promenljivoj c veliko slovo. To znači da je ova funkcija neka vrsta zamene za našu funkciju lower(c). Postoji jedna osetljiva tačka u vezi sa konverzijom znakova u cele brojeve. Jezik ne precizira da li promenljiva tipa char sadrži predznačenu ili nepredznačenu vrednost. Kada se char tip konvertuje u int tip, hoće li ikad moći da se proizvede negativan ceo broj? Nažalost, odgovor na ovo pitanje varira od računara do računara, odražavajući razlike u unutrašnjoj arhitekturi. Na nekim mašinama (PDP-11, na primer) će char promenljiva čiji je krajnji levi bit setovan (1) biti konvertovan u negativan ceo broj ('broj sa predznakom'). Na drugim računarima, char tip se konvertuje u int tip uz postavljanje krajnjeg levog bita na nulu, čineći dobijeni ceo broj uvek pozitivnim. Definicija C-a garantuje da će bilo koji znak iz seta znakova koji je ugrađen uvek biti pozitivan, tako da se znakovi u izrazima mogu slobodno tretirati kao pozitivne veličine. Međutim, proizvoljan niz bitova smešten u char promenljivu može biti tretiran kao negativan broj na jednim, a kao pozitivan broj na drugim računarima. Najčešće pojavljivanje ovakve situacije je slučaj kada je vrednost -1 upotrebljena za oznaku kraja datoteke (EOF). Razmotrite sledeće: char c; c = getchar(); if (c == EOF) ... Na mašini na kojoj se ne koriste predznačeni brojevi, c je uvek pozitivno jer je char tipa, a EOF je negativan broj. Kao posledica toga, test je uvek neistinit. Da bismo izbegli ovo, bili smo oprezni i svaki put smo koristili promenljivu int tipa kada je trebalo čuvati vrednost koju vraća funkcija getchar. Stvarni razlog za upotrebu int tipa umesto char nije u vezi sa mogućim predznačavanjem brojeva. Jednostavno je u pitanju to što funkcija getchar

mora vratiti vrednost za sve moguće znakove (da bi mogla da čita proizvoljan ulaz) i, uz to, određenu EOF vrednost. Kako vrednost EOF ne može biti predstavljena kao znak, ona mora biti čuvana u int promenljivoj. Još jedan oblik automatske konverzije tipa je da relacioni izrazi i logičkih izrazi povezani operatorima && i || dobijaju vrednost 1 ako su istiniti (tačni), odnosno 0 ako nisu. Odatle dodeljivanje isdigit = c >= '0' && c <= '9'; postavlja promenljivu isdigit na 1 ako je c cifra, odnosno na 0 ako nije. U delovima if, while, for, itd. konstrukcija koji ispituju neki uslov, 'istinito' jednostavno znači 'sve osim nule'. Uzgred, isdigit(c) je funkcija iz biblioteke <ctype.h>, i koristi se kada treba ispitati uslov c >= '0' && c <= '9' Implicitne aritmetičke konverzije rade uglavnom kako se i očekuje. Opšte uzevši, ako jedan operator kao + ili * koji se primenjuje na dva operanda ('binarni operator') ima operande različitog tipa, tada će operand 'nižeg' tipa biti preveden u 'viši' tip pre nego što počne izračunavanje. Rezultat će biti višeg tipa. Tačnije, za svaki aritmetički operator važe sledeća pravila: Tipovi char i short se prevode u int tip. Nakon toga, ako je jedan operand tipa long double, i drugi se prevodi u long double tip. Rezultat je takođe long double tipa. Ako to nije slučaj, a jedan operand je tipa double, i drugi se prevodi u double tip. Rezultat je double tipa. Ako to nije slučaj, a jedan operator je float tipa, i drugi se prevodi u float tip. Rezultat je float tipa. Ako to nije slučaj, a jedan operand je tipa long int, i drugi se prevodi u long int tip. Rezultat je long int tipa. Ako nije nastupio nijedan od prethodnih slučajeva, oba operanda mora da su int tipa, pa će i rezultat biti int tipa. Primetite da se u nekom izrazu float tipovi ne konvertuju automatski u double tip. Ovo je izmena u odnosu na originalnu definiciju. Glavni razlog za upotrebu float tipa je da bi se sačuvao memorijski prostor u velikim poljima ili, što je ređe, da bi se uštedelo vreme na računarima kod kojih je aritmetika sa dvostrukom tačnošću prilično spora. šitava aritmetika realnih brojeva (sve matematičke funkcije) je u C-u izvedena u dvostrukoj preciznosti. Konverziona pravila postaju komplikovanija kada se u igri pojave operandi tipa unsigned.

Konverzije se odigravaju i kroz dodeljivanja; vrednost na desnoj strani se prevodi u tip koji ima leva strana, što je i tip rezultata. Znak se pretvara u ceo broj, predznačen ili ne, kako je ranije opisano. Obrnuta transformacija, int tipa u char tip nije problematična: int i; char c; i = c; c = i; vrednost u promenljivoj c ostaje neizmenjena, bez obzira na to računa li se sa predznakom ili ne. Ako je x promenljiva float tipa, a i promenljiva int tipa, onda oba slučaja, x = i; i i = x; prouzrokuju konverziju; konverzija float tipa u int tip izaziva gubitak dela iza decimalne tačke. Tip double se konvertuje u float tip ili zaokruživanjem, ili tako što se gubi decimalni deo (zavisno od primene). Takođe, long int brojevi se prevode u short int ili u char tipove odbacivanjem krajnje levih bitova. Kako je i argument neke funkcije ustvari izraz, to se konverzije tipova primenjuju i kad se argumenti prosleđuju funkciji; konkretno, char i short tipovi prelaze u int tip, a float tip prelazi u double tip. Eto zašto smo mi deklarisali argumente funkcije kao int i double tipove čak i kad je funkcija pozvana argumentima char i float tipa. Konačno, eksplicitne konverzije tipa mogu se primeniti i na čitave izraze, operatorom koji se naziva cast. Opšti oblik je (tip) i z r a z i njime će i z r a z biti konvertovan u tip prema već navedenim pravilima. U suštini, cast operator se može shvatiti kao da je i z r a z dodeljen promenljivoj tipa tip, koja se onda dalje koristi umesto cele konstrukcije. Na primer, funkcija sqrt iz standardne biblioteke očekuje argument tipa double, i ako se primeni na neki drugi tip, proizvešće besmislicu. Tako, ako je n ceo broj, možemo koristiti cast operator za sqrt( (double) n) i pretvoriti n u double tip pre nego što ga prosledimo funkciji sqrt.

Primetite da cast konstrukcija proizvodi ispravnu vrednost n; stvarni sadržaj n nije izmenjen. Operator cast ima isti prioritet kao i drugi unarni operatori, što se vidi iz tabele na kraju poglavlja. Ako su tipovi argumenata deklarisani prototipom funkcije, to deklarisanje prouzrokuje automatsku konverziju svih argumenata prilikom poziva funkcije. Odatle, za dati prototip funkcije sqrt double sqrt(double); će poziv root2 = sqrt(2); pretvoriti ceo broj 2 u vrednost 2.0 tipa double bez potrebe za operatorom cast. Standardna biblioteka sadrži mini model generatora slučajnih brojeva i funkciju za njegovu inicijalizaciju; sledeći primer ilustruje upotrebu operatora cast: unsigned long int next = 1; /* rand: vraca slucajan ceo broj između 0 i 32767 */ int rand(void) { next = next * 1103515245 + 12345; return (unsigned int) (next / 65536) % 32768; } /* srand: postavljanje pocetne vrednosti za rand */ void srand(unsigned int pocetak) { next = pocetak; } þ Vežba 2 - 2 Napišite funkciju htoi koja pretvara niz heksadecimalnih brojeva u ekvivalentnu celobrojnu vrednost. Važeće cifre su od 0 do 9, a -f ili A -F. 2.8 OPERATORI UVEĆAVANJA I UMANJIVANJA Jezik C pruža dva neobična operatora za uvećavanje (inkrementiranje) i umanjivanje (dekrementiranje) vrednosti promenljive. Operator uvećavanja ++ uvećava svoj operand za jedan; operator umanjivanja -- oduzima jedan od svog operanda. šesto smo koristili operator ++ da uvećamo neku promenljivu, kao u

if (c == '\n') ++nl; Ono neobično u vezi sa operatorima ++ i -- je to da mogu biti upotrebljeni kao prefiks operatori (ispred promenljive, kao ++n), ili kao sufiks operatori (posle promenljive: n++). U oba slučaja efekat je uvećavanje promenljive n za jedan. Ali, izraz ++n uvećava promenljivu n pre nego što se bilo gde upotrebi, dok izraz n++ uvećava promenljivu n tek nakon što se negde upotrebi. To znači da, u slučaju da se vrednost promenljive n negde upotrebljava, neće samo efekat izraza n++ i ++n biti različit: biće to i promenljiva kojoj je dodeljena vrednost promenljive n. Ako je n = 5, biće posle x = n++; (x = 5, n = 6) x = ++n; (x = 6, n = 6) x = n--; (x = 5, n = 4) x = --n; (x = 4, n = 4) Operatori uvećavanja i umanjivanja mogu biti primenjeni isključivo na promenljive: izrazi tipa x = (i + j)++ nisu dozvoljeni. U situacijama gde se ne barata sa vrednošću promenljive, već ona služi samo kao brojač, kao u if (c == '\n') nl++; sasvim je svejedno da li ćete upotrebiti prefiks ili sufiks varijantu. Međutim, postoje situacije kada to nije svejedno. Na primer, razmotrimo funkciju squeeze(s, c) koja uklanja sve znakove c iz niza s. /* squeeze : brisanje svih znakova c iz niza s */ void squeeze(char s[], int c) { int i, j; for (i = j = 0; s[i] != '\0'; i++) if (s[i] != c) s[j++] = s[i]; s[j] = '\0'; } Svaki put kada se pojavi znak različit od c, on se kopira na trenutnu j poziciju, i samo onda se j uvećava da bi bilo spremno za novi znak. To je potpuno ekvivalentno sa if (s[i] != c) { s[j] = s[i];

j++; } Još jedan primer slične konstrukcije stiže iz funkcije getline koju smo napisali u Poglavlju 1. U njoj sada možemo if (c == '\n') { s[j] = c; j++; } zameniti sa kompaktnijim if (c == '\n') s[j++] = c; Kao treći primer uzmimo funkciju strcat(s, t) koja nadovezuje niz t na kraj niza s. Funkcija strcat podrazumeva da je u nizu s dovoljno mesta da prihvati kombinaciju. Kao što smo napisali, funkcija strcat ne vraća u program nikakvu vrednost; verzija ove funkcije iz standardne biblioteke vraća u program pokazivač na rezultujući niz. /* strcat: nadovezivanje niza t na niz s; s je dovoljno velik */ void strcat(char s[], chart[]) { int i, j; i = j = 0; while (s[i] != '\0') /* nađi kraj niza */ i++; while ( (s[i++] = t[j++]) != '\0') /* kopira t u s */ ; } Kako se svaki znak kopira iz niza t u niz s, to se sufiks ++ dodaje i promenljivoj i i promenljivoj j da bi smo bili sigurni da su na pravoj poziciji za sledeći prolaz kroz petlju. þ Vežba 2 - 3 Napišite funkciju any(s1, s2) koja u program vraća prvu lokaciju u nizu s1 gde se neki znak iz niza s2 pojavljuje, odnosno vraća 1 ako niz s1 ne sadrži nijedan znak koji sadrži niz s2. þ Vežba 2 - 4 Napišite alternativnu verziju funkcije squeeze(s1, s2) koja briše svaki znak niza s1 koji postoji i u nizu s2. 2.9 BIT - OPERATORI

C obezbeđuje određen broj operatora za manipulaciju bitovima; ovi operatori mogu biti primenjeni isključivo na celobrojne operande, dakle, na operande tipa char, short, int i long, bez obzira na to da li su uz to signed ili unsigned tipa. Evo liste bit-operatora: & AND (i) | OR (ili) ^ XOR (isključivo ili) << šiftovanje ulevo >> šiftovanje udesno ~ komplement (unarni) Bit-operator & (AND) je binarni operator: primenjuje se na dva operanda i to na svaki par njihovih bitova posebno. Neki bit rezultata biće postavljen na 1 samo ako su u odgovarajućem paru bitova operanada oba bita bila postavljena na 1. Ovaj operator se često koristi da maskira (postavi na nulu) neku grupu bitova; na primer, c = n & 31; postavlja na nulu sve bitove osim eventualno pet najnižih. Broj 31, predstavljen u binarnoj formi, je oblika 0000000000011111 (ako je veličine dva bajta). Koji god da je n broj, biće 1 1 0 0 0 1 0 1 1 0 0 1 1 0 1 0 (neko proizvoljno n) AND 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 (broj 31) ----------------------------------- 0 0 0 0 0 0 0 0 0 0 0 1 1 0 1 0 (c = 26) Bit-operator | (OR) se primenjuje na dva operanda na isti način na koji to čini i AND operator. Neki bit rezultata biće postavljen na 1 ako je bar jedan iz odgovarajućeg para bitova operanada bio postavljen na 1. Ovaj operator se često koristi da postavi neke bitove na jedinicu: x = x | MASK; postavlja na jedan one bitove u promenljivoj x koji su postavljeni na jedinicu u konstanti MASK. Ako je, recimo, x = 1 a MASK je 165, biće 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 (x = 1) OR 0 0 0 0 0 0 0 0 1 0 1 0 0 1 0 1 (MASK = 165) ----------------------------------- 0 0 0 0 0 0 0 0 1 0 1 0 0 1 0 1 (x | MASK = 165) Bit-operator ^ (XOR) je takođe binarni operator; u rezultatu setuje bitove na mestima gde operandi imaju različite bitove, a resetuje na mestima gde su im bitovi isti. Morate razlikovati bit-operatore & i |, od logičkih operatora && i ||, koji istinitost izraza izračunavaju sleva nadesno. Na primer, ako je x

= 1 i y = 2, onda će x & y proizvesti vrednost nula, a x && y proizvesti vrednost jedan. Opratori šiftovanja << i >> izvode pomeranje njihovog levog operanda ulevo i udesno za broj bitova određen desnim operandom. Tako će izraz x << 2 šiftovati x za dva bita ulevo, a upražnjene pozicije popuniti nulama. Ako je x = 8, onda će biti 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 (x = 8) 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 (x << 2) Žiftovanje udesno će upražnjene pozicije nepredznačenih, unsigned brojeva popuniti nulama. Ako je broj predznačen, šiftovanje udesno će upražnjena mesta popuniti jedinicama na nekim računarima, a nulama na nekim drugim. Unarni operator ~ proizvodi binarni komplement nekog celog broja: on pretvara svaki 1-bit u 0-bit i obrnuto. Ovaj operator obično ima primenu kod izraza tipa x & ~077 gde maskira poslednjih šest bitova vrednosti x na nulu. Primetite da je izraz x & ~077 nezavisan od dužine i da je stoga u prednosti nad, recimo, izrazom x & ~07700 što podrazumeva da je x u oba slučaja šesnaestobitna vrednost. Kraći oblik ne menja ništa, pošto je ~077 izraz koji se izračunava još za vreme kompajliranja. Da bismo ilustrovali upotrebu nekih bit - operatora, posmatrajmo funkciju getbits(x, p, n) koja vraća u program (desno poravnatu) grupu od n bitova počev od pozicije p, neke vrednosti x. Pretpostavili smo da je nulta bit pozicija krajnja desna pozicija, i da su n i p razumno velike pozitivne vrednosti. Na primer, getbits(x, 4, 3) vraća u program desna tri bita počev od pozicije 4 (a to su bitovi 4, 3 i 2), pomerena sasvim uz desnu stranu tako da su na drugoj, prvoj i nultoj bit poziciji respektivno. /* getbits: vraca desnih n bitova pocev od pozicije p */ unsigned getbits(unsigned x, int p, int n) { return (x >> (p + 1 - n)) & ~(~0 << n) } Levi deo return izraza, x >> (p + 1 - n), pomera željenu grupu bitova do desne ivice reči (najčešće: reč = dva bajta). Deklarišući argument x kao unsigned tip, obezbedili smo da kada se x šiftuje udesno, na upražnjena dođu nule, a ne eventualno jedinice zbog predznaka. Zbog toga će program raditi na svim mašinama.

U ovom primeru, za getbits(x, 4, 3),posle šiftovanja udesno, x izgleda ovako: 0 0 0 . . . . . . . . . . bit4 bit3 bit2 gde umesto ...... stoje nule ili jedinice, zavisno od x. Sada je potrebno sve ostale bitove osim desnih n = 3 bita postaviti na nulu. To praktično znači da treba napraviti masku u kojoj će svi bitovi biti na nuli, osim krajnja desna tri bita koja će biti na jedinici. Maska predstavlja desni deo return izraza, i ovako je napravljena: 1. Operatorom ~ primenjenim na broj 0 svi bitovi tog broja su postavljeni na jedan: 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 (~0) 2. Potom je izvršeno šiftovanje ulevo za n = 3 pozicije kako bi se na krajnja desna tri mesta pojavile nule: 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 (~0 << n) 3. Ovo je upravo komplement maske koja nam je potrebna, pa se stoga jednostavno ponovo primeni unarni operator ~: 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 ~(~0 << n) Posle primene operatora & na vrednost x i upravo kreiranu masku, dobiće se željeni efekat: 0 0 0 . . . . . . . . . . bit4 bit3 bit2 (x) & 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 ~(~0 << n) ----------------------------------------------- 0 0 0 0 0 0 0 0 0 0 0 0 0 bit4 bit3 bit2 þ Vežba 2 - 5 Izmenite funkciju getbits tako da označava bitove sleva nadesno (krajnji levi bit je nulti). þ Vežba 2 - 6 Napišite funkciju wordlength() koja dužinu reči na vašem kompjuteru, tj. broj bitova u int celom broju. þ Vežba 2 - 7 Napišite funkciju rightrot(n, b) koja rotira ceo broj n za b bit pozicija udesno (bit koji 'ispadne' sa desne strane upisuje se u upražnjeno mesto na levoj strani). þ Vežba 2 - 8 Napišite funkciju invert(x, p, n) koja invertuje (pretvara jedinice u nule i obrnuto) n bitova broja x počevši od pozicije p, ostavljajući ostale bitove neizmenjene.

2.8 OPERATORI I IZRAZI DODELJIVANJA Izrazi kao što je i = i + 2; u kojima se izraz na levoj strani ponovljen na desnoj strani mogu biti napisani u skraćenoj formi kao i += 2; koristeći operator dodeljivanja +=. Većina binarnih operatora (operatori kao što je + imaju levi i desni operand) imaju odgovarajući operator dodeljivanja op=, gde je op neki od sledećih operatora: + - * / % << >> & ^ | Ako su e1 i e2 izrazi, onda je e1 op= e2; ekvivalentno e1 = (e1) op (e2); osim što se u prvom slučaju e1 računa samo jedanput. Primetite zagrade oko e2, jer je x *= y + 1; ustvari x = x * (y + 1); a ne x = x * y + 1; Kao primer, evo funkcije bitcount koja vraća u program broj bitova postavljenih na jedinicu u nekom celom broju. /* bitcount: broji 1-bitove broja n */

int bitcount(unsigned n) { int b; for (b = 0; n != 0; n >>= 1) if (n & 01) b++; return b; } Osim sažetosti, operatori dodeljivanja su u prednosti nad uobičajenim konstrukcijama jer više odgovaraju ljudskom načinu razmišljanja. Mi kažemo 'dodaj 2 promenljivoj i' ili 'uvećaj vrednost i za 2', a ne 'uzmi promenljivu i, dodaj 2, i rezultat vrati nazad u i'. Otud i += 2. Uz to, kod komplikovanijih izraza kao što je yyval[yypv[p3 + p4] + yypv[p1 + p2]] += 2; operator dodeljivanja čini program lakšim za čitanje. Onaj koji čita ne mora da se muči proveravajući da li je izraz na desnoj strani zaista isti kao i onaj na levoj, ili da se čudi zašto nije. Operator dodeljivanja može čak pomoći kompajleru da napravi efikasniji izvršni program. Već smo koristili činjenicu da izraz dodeljivanja ima svoju vrednost i da se kao takav može pojaviti u drugim izrazima: najčešći primer je while ( (c = getchar()) != EOF) ... Izrazi dodeljivanja koji koriste ostale operatore dodeljivanja (+=, -=, itd.) mogu se takođe pojaviti u drugim izrazima, mada je to ređi slučaj. Tip izraza dodeljivanja je određen tipom njegovog levog operanda. 2.11 USLOVNI IZRAZI Konstrukcija if (a > b) z = a; else z = b; smešta u promenljivu z veću od vrednosti a i b. Uslovni izraz, napisan pomoću operatora ?: omogućuje alternativni način da se napišu ovakva i slične konstrukcije. U konstrukciji

e1 ? e2 : e3 prvo se ispita uslov e1. Ako je istinit (vrednost mu je različita od nule), onda će se izračunati izraz e2, a to je i vrednost celog uslovnog izraza. Ako uslov e1 nije istinit, izračunaće se izraz e3, i to će biti vrednost celog uslovnog izraza. Izračunava se samo jedan od izraza e2 i e3. Zato, da bi u promenljivu z stavili veću od vrednosti a i b, pisaćemo z = (a > b) ? a : b; /* z = max(a, b) */ Treba primetiti da je uslovni izraz zaista izraz, i da se može koristiti kao i svaki drugi. Ako su izrazi e2 i e3 različitih tipova, tip rezultata je određen pravilima konverzije o kojima je već bilo reči. Na primer, ako je f tipa float, a n tipa int, onda će izraz (n > 0) ? f : n; biti tipa float bez obzira da li je n manje ili veće od nule. Zagrade nisu neophodne oko uslovnog dela u uslovnom izrazu, pošto je prioritet operatora ?: veoma nizak, tek iznad prioriteta operatora dodeljivanja. Preporučljivo je, svejedno, da se ipak pišu jer time čine uslovni deo uočljivijim. Uslovni izrazi često vode sažetijem programu. Na primer, sledeća petlja štampa N elemenata nekog polja, deset po liniji, u kolonama međusobno odvojenim jednim blanko znakom i sa znakom za novi red na kraju svake linije (uključujući i poslednju). for (i = 0; i < N; i++) printf(„%6d%c“, a[i], (i % 10 == 9 || i == N - 1) ? '\n' : ' '); Znak za novi red se štampa posle svakog desetog elementa, i posle N-tog. Posle svih ostalih elemenata sledi blanko znak. Iako ovaj primer možda izgleda kao trik, preporučujemo vam da pokušate da napišete ekvivalentnu petlju bez korišćenja uslovnog izraza. Vežba 2 - 9 Napišite ponovo funkciju lower, koja konvertuje velika slova u mala, koristeći uslovni izraz umesto konstrukcije if - else. 2.12 PRIORITET I REDOSLED IZRAČUNAVANJA Donja tabela sumira sva pravila za određivanje prioriteta i asocijativnosti operatora, uključujući tu i one operatore o kojima još nismo diskutovali. Operatori u istom redu imaju isti prioritet; svaki red ispod ima

niži prioritet: tako, na primer, operatori *, /, i % imaju isti prioritet, koji je viši od prioriteta operatora u liniji ispod, + i -.

O P E R A T O R asocijativnost () {} -> . sleva nadesno ! ~ ++ -- + - * & tip sizeof sleva nadesno * / % sleva nadesno + - sleva nadesno < <= > >= sleva nadesno == != sleva nadesno & sleva nadesno ^ sleva nadesno | sleva nadesno && sleva nadesno || sleva nadesno ? : zdesna nalevo = += -= *= /= &= ^= | = <<= >>= zdesna nalevo , sleva nadesno

(unarni operatori +, - i * imaju viši prioritet od istih binarnih) Operatori -> i . se koriste da bi se pristupilo članovima neke strukture; biće opisani u Poglavlju 6, zajedno sa operatorom sizeof. U Poglavlju 5 se diskutuje o operatorima * (preusmeravanje,ili što je na adresi od) i & (adresa od). Primetite da je prioritet bit-operatora &, ^ i | niži od prioriteta operatora == i !=. To znači da izrazi koji vrše testiranje bitova, kao što je if ( (x & MASK) == 0) ... moraju biti navedeni u zagradama da bi dali pravilne rezultate. Kao što smo ranije pomenuli, izrazi koji sadrže asocijativne i komutativne operatore (*, +, &, ^, |) mogu biti preuređeni prilikom izračunavanja čak i ako su upotrebljene zagrade. U većini slučajeva to ne pravi nikakvu razliku; u situacijama gde bi moglo, koriste se određene privremene promenljive da bi se obezbedio željeni redosled izračunavanja. C, kao i većina drugih jezika, ne definiše kojim će se redom izvršavati operacije u nekom izrazu. Na primer, u izrazu kao što je x = f() + g(); f može biti izračunato prvo, a može biti i obrnuto; zbog toga ako f menja neku spoljnu promenljivu od koje g zavisi (ili obrnuto), vrednost promenljive x može zavisiti od redosleda izračunavanja. Još jednom, međurezultati se mogu smeštati u privremene promenljive da bi se obezbedio određeni redosled izračunavanja.

Slično ovome, redosled kojim se izračunavaju argumenti funkcije takođe nije definisan. Zato izraz printf(„%d %d \n“, ++n, power(2,n)); /* pogrešno */ može proizvesti (i proizvodi) različite rezultate na različitim mašinama, zavisno od toga da li je promenljiva n uvećana pre poziva funkcije power ili posle. Rešenje je, naravno, pisati ++n; printf(„%d %d \n“, n, power(2, n)); Pozivi funkcija, umetnuti iskazi dodeljivanja i operatori uvećavanja i umanjivanja izazivaju tzv. 'usputni efekat' - izračunavanjem nekog izraza usput je promenjena i neka promenljiva. Ako neki izraz proizvodi 'usputne efekte', redosled kojim su promenljive tog izraza smeštane može postati osetljivo pitanje. Jedna nezgodna situacija je predstavljena izrazom a[i] = i++; Pitanje je da li je indeks nova ili stara vrednost promenljive i. Kompajler može ovo prevesti na različite načine, i stvoriti različita rešenja. Kada se pojave usputni efekti, sve je prepušteno kompajleru pošto optimalni redosled zavisi od arhitekture konkretne mašine. Pouka ove diskusije je da je pisanje konstrukcija koje zavise od redosleda izračunavanja loša praksa u bilo kom jeziku. Naravno, neophodno je znati šta treba izbeći, ali ako ne znate kako se stvari odvijaju na drugim mašinama, ta naivnost vam može pomoći. Postoje C rutine koje otkrivaju većinu takvih mesta koja zavise od redosleda izračunavanja. P o g l a v l j e 3: KONTROLA TOKA Iskazi kontrole toka nekog jezika definišu redosled kojim će se neka izračunavanja izvršiti. Kroz prethodne primere smo već upoznali najosnovnije konstrukcije za kontrolu toka u C-u; u ovom poglavlju ćemo kompletirati skup tih konstrukcija i detaljno opisati one već pomenute. 3.1 ISKAZI I BLOKOVI Izrazi kakvi su x = 0 ili i++ ili printf(...) postaju iskazi kada za njima sledi znak ;: x = 0;

i++; printf(...); U C-u, znak ; predstavlja oznaku za kraj iskaza. Vitičaste zagrade { i } se koriste da grupišu deklaracije i iskaze u složeni iskaz ili blok tako da su sintaksno ekvivalentni jednom iskazu. Zagrade oko iskaza koji čine neku funkciju su očigledan primer; zagrade oko grupe iskaza u if, else, while ili for konstrukcijama su drugi primer. Promenljive mogu biti deklarisane unutar bilo kog bloka; o ovome će biti reči u Poglavlju 4. Posle desne zagrade } koja ograničava neki blok nikad ne sledi znak ;. 3.2 IF - ELSE Konstrukcija if - else se koristi kod donošenja nekih odluka u programu. Njen formalni oblik je if (izraz) iskaz1 else iskaz2 gde se else deo konstrukcije može i izostaviti. Uslov izraz se izračunava; ako je istinit (tj. tačan: izraz ima vrednost različitu od nule), izvršiće se iskaz1. Ako nije tačan (izraz ima vrednost nula) i postoji else deo, izvršiće se iskaz2. Pošto if testira numeričku vrednost izraza koji predstavlja uslov, to su moguća izvesna skraćenja u pisanju programa. Najočiglednije je pisanje if (izraz) umesto if (izraz != 0) Ponekad je ovo prirodno i jasno; ponekad nije. Zbog toga što je else deo u if - else konstrukciji opcion, to postaje nejasno šta će se dogoditi kada se else izostavi iz konstrukcije u kojoj se očekuje. Ovo se rešava na uobičajen način - pridružuje se najblizoj if konstrukciji u kojoj nema else dela. Na primer, u if (n > 0) if (a > b) z = a; else

z = b; else deo se pridružuje uz unutrašnje if, što smo i naglasili uvlačenjem teksta. Ako to nije ono što želite, morate da upotrebite zagrade da biste ostvarili željeno pridruživanje: if (n > 0) { if (a > b) z = a; } else z = b; Nejasnoća je posebno opasna u situaciji kao {to je : if (n > 0) for (i = 0; i < n; i++) if (s[i] >= 0) { printf(„...“); return i; } else /* pogresno */ printf(„greska - n je negativno\n“); Uvlačenje teksta nedvosmisleno pokazuje šta želite, ali kompajler to neće tako shvatiti, i pridružiće else najbližoj, unutrašnjoj if konstrukciji. Ovu vrstu grešaka je veoma teško otkriti; dobra predostrožnost je korišćenje vitičastih zagrada kada se pojavljuje više umetnutih if konstrukcija. Uzgred, primetite da u konstrukciji if (a > b) z = a; else z = b; iza izraza z = a stoji znak ;. To stoga jer gramatički gledano posle if dela sledi iskaz, pa je izraz koji sledi iza if dela uvek završen znakom ;. 3.3 ELSE - IF Konstrukcija if (izraz1) iskaz1 else if (izraz2) iskaz2 else if (izraz3)

iskaz3 else iskaz4 se pojavljuje toliko često u programima da je vredna kraće diskusije. Ovakva konstrukcija je najopštiji način da se izrazi složena odluka. Uslovi (izraz1 - 4) se ispituju (izračunavaju) po redu: čim je jedan od njih istinit, izvršava se iskaz koji je njemu pridružen, i cela konstrukcija se odmah napušta. I ovde, kao i ranije, iskaz može biti jedan iskaz ili grupa iskaza navedena u vitičastim zagradama. Iskaz uz poslednji else deo u gornjoj konstrukciji (iskaz4) biće izvršen u slučaju da nijedan od prethodno testiranih uslova nije zadovoljen. Dakle, biće izvršen kao preostali slučaj. Ponekad neće biti potrebno preduzeti neku akciju u preostalom slučaju; tada se else iskaz4 može izostaviti, ili se iskoristiti za konstatovanje greške 'nemoguća varijanta'. Da bismo ilustrovali troznačno odlučivanje, napisali smo funkciju binsearch koja pretražuje da li se određena vrednost x pojavljuje u nekom polju v čiji su elementi poređani po rastućem redosledu. Funkcija u program vraća poziciju elementa koji je jednak x (tj. broj između nula i n-1), odnosno -1 ako se x ne pojavljuje među elementima polja v. Algoritam pretraživanja je sledeći: ako je x manje od vrednosti središnjeg elementa polja, pretraživanje se premešta u donju polovinu polja. U suprotnom, premešta se u gornju polovinu. U oba slučaja, sledeći korak je upoređivanje x sa središnjim elementom izabrane polovine. Proces deljenja opsega na dva dela se nastavlja sve dok se ne pronađe tražena vrednost ili se opseg ne iscrpi. /* binsearch: traži x u polju v[0] ..... v[n-1] */ int binsearch(int x, int v[], int n) { int low, high, mid; low = 0; high = n - 1; while (low <= high) { mid = (low + high) / 2; if (x < v[mid]) high = mid - 1; else if (x > v[mid]) low = mid + 1; else /* pronađeno */ return mid; }

return -1; /* nije pronađeno */ } Suštinska odluka je da li je x manje, veće ili jednako središnjem elementu v[mid] u svakom koraku; prirodno je da je upotrebljena else - if konstrukcija. þ Vežba 3 - 1 Naša funkcija obavlja dva testa unutar petlje, mada bi i jedan bio dovoljan (po cenu većeg broja spoljnih testova). Napišite verziju sa samo jednim testom unutar petlje i uporedite razliku u vremenu izvršavanja programa. 3.4 SWITCH Korišćenje switch iskaza je način da se u programu donese neka višeznačna odluka. Njegova konstrukcija je switch (izraz) { case konst_izr_1 : iskaz1 case konst_izr_2 : iskaz2 case konst_izr_3 : iskaz3 default: iskaz4 } Iskazom switch se poredi vrednost celobrojnog izraza izraz sa konstantnim izrazima konst_izr_1 - 3 (konst_izr može biti celobrojna konstanta, znakovna konstanta ili konstantni izraz; ako ih je više, odvajaju se dvotačkom). Kada se nastupi neki od slučajeva case, tj. kada se ustanovi jednakost sa nekim od konstantnih izraza, izvršava se iskaz koji je pridružen tom konstantnom izrazu. I ovde je iskaz jedan ili više iskaza, ovaj put navedenih bez vitičastih zagrada. Slučaj označen sa default je neobavezan; ako je naveden u programu, i ako izraz ne odgovara nijednom od konst_izr iznad, biće izvršen iskaz4. Slučajevi mogu biti navedeni u programu bilo kojim redosledom, ali se konst_izr svih slučajeva moraju međusobno razlikovati. U Poglavlju 1 smo napisali program koji broji koliko se puta na ulazu pojavila cifra, koliko specijalni znaci, a koliko svi ostali znaci. Tada smo koristili niz if - else konstrukcija, a sada evo istog programa napisanog korišćenjem konstrukcije switch: #include <stdio.h> main() /* broji cifre,spec. znakove i ostalo */ { int c, nwhite, nother, ndigit[10]; nwhite = nother = 0;

for (i = 0; i < 10; i++) ndigit[i] = 0; while ( (c = getchar()) != EOF) { switch (c) { case '0' : case '1' : case '2' : case '3' : case '4': case '5' : case '6' : case '7' : case '8' : case '9': ndigit[c - '0']++; break; case ' ' : case '\n' : case '\t' : nwhite++; break; default: nother++; break; } } printf(„cifre = „); for (i = 0; i < 10; i++) printf(„ %d“, ndigit[i]); printf(„, spec_znaci = %d, ostalo = %d\n“, nwhite, nother); return 0; } Iskaz break izaziva trenutni izlazak iz switch konstrukcije. Slučajevi su samo različito označeni, a ne i odvojeni međusobno. To znači da će u slučaju da se izvrši akcija vezana za neki slučaj, izvršenje biti nastavljeno kroz sledeći slučaj, i tako redom sve dok se eksplicitno ne naznači izlazak iz konstrukcije. Iskazi break i return su uobičajeni način da se na licu mesta izađe iz switch konstrukcije. Iskaz break takođe može poslužiti za trenutni izlazak iz for, while i do petlji, o čemu će detaljno biti reči kasnije. Prolazak kroz slučajeve ima dobre i loše strane. Dobro je to što obuhvata više slučajeva jednom akcijom, kao što je to slučaj kod specijalnih znakova u ovom primeru. Međutim, zbog toga svaki slučaj mora završavati break iskazom da bi sprečio prolazak kroz sledeći. Prolazak kroz slučajeve je sklon raspadu pri modifikaciji programa. Sa izuzetkom višestrukih oznaka za jedno izračunavanje, ovu konstrukciju treba shvatiti kao racionalnu i koristiti je. Kao pitanje dobrog stila, stavite break iskaz čak i na kraj poslednjeg slučaja (ovde default), iako je to logički nepotrebno. Jednog dana ćete dodati još neki slučaj na kraj vaše switch konstrukcije, i tada će vam ova predostrožnost pomoći. þ Vežba 3 - 2 Napišite funkciju expand(s, t) koja prilikom kopiranja niza s u niz t konvertuje znak za novi red i tabulator u vidljive eskejp sekvence \n i \t.

3.5 WHILE I FOR PETLJE Do sada smo se već sretali sa while i for petljama. U konstrukciji while (izraz) iskaz uslov izraz se izračunava. Ako je njegova vrednost različita od nule, izvršava se deo iskaz i ponovo se ispituje uslov izraz . Ovaj ciklus se nastavlja sve dok vrednost uslova izraz ne postane jednaka nuli. Tada se preskače deo iskaz i izvršavanje programa se nastavlja iza njega. Konstrukcija for oblika for (izr1 ; izr2 ;izr3) iskaz je ekvivalentna konstrukciji izr1; while (izr2) { iskaz izr3; } Sintaksno gledano, tri dela for konstrukcije su izrazi. Najčešće, izr1 i izr3 su izrazi dodeljivanja ili pozivi funkcija, dok je izr2 relacioni izraz. Bilo koji od ova tri izraza može biti izostavljen, ali znaci ; moraju ostati. Ako se deo u kome je test (izr2) izostavi, smatra se da je stalno istinit, tako da je for (;;) { ... } beskonačna petlja iz koje je moguće izaći samo na neki drugi način (iskazima break ili return). Stvar je afiniteta da li ćete koristiti konstrukciju for ili while. Na primer, u while ( (c = getchar()) == ' ' || c = '\n' || c = '\t') ; /* preskoči spec. znake */ nema inicijalizacije i reinicijalizacije nekog brojača, pa se while konstrukcija čini najprirodnijom. Konstrukcija for je bez dileme superiornija kada je u pitanju

jednostavna inicijalizacija i reinicijalizacija nekog brojača, jer drži sve iskaze koji kontrolišu petlju na jednom mestu - na vrhu petlje. To je najočiglednije na primeru for (i = 0; i < N; i++) koji predstavlja način da se u C-u obradi prvih N elemenata nekog polja, slično DO petlji u Fortranu. Analogija nije potpuna, s obzirom na to da granice brojača for petlje mogu biti menjane iz same petlje, a kontrolna promenljiva i zadržava svoju vrednost kad se petlja okonča iz bilo kog razloga. Zbog toga što su delovi for konstrukcije izrazi proizvoljnog oblika, to for petlje nisu ograničene samo na aritmetičke progresije. I pored svega toga, nije dobro ubacivati u for konstrukciju izraze koji nisu u vezi sa samom petljom; bolje je da su tu umesto njih operacije koje kontrolišu petlju. Kao bolji primer, evo još jedne verzije funkcije atoi za konvertovanje stringa u njegov numerički ekvivalent. Ova verzija je još opštija; ona manipuliše čak i sa eventualno ubačenim specijalnim znacima i sa predznacima + i -. Poglavlje 4 prikazuje funkciju atof koja obavlja ovakvu konverziju sa realnim brojevima. Osnovna struktura programa je prilagođena obliku ulaza: preskoči specijalni znak, ako postoji uzmi znak sa ulaza, ako još ima znakova uzmi celobrojni deo i konvertuj ga Svaki korak obavlja jedan deo posla, i ostavlja stvari spremne za delovanje sledećeg dela. Ceo program se okončava onog trenutka kada naiđe na prvi znak koji ne može predstavljati neku cifru. include <ctype.h> /* atoi: konvertuje niz s u ceo broj; verzija 2 */ int atoi(char s[]) { int i, n, sign; for (i = 0; s[i] == ' ' || s[i] == '\n' || s[i] == '\t' ; i++) ; /* preskoči specijalne znakove */ sign = 1; if (s[i] == '+' || s[i] == '-') /* predznak */ sign = (s[i++] == '+') ? 1 : -1; for (n = 0; s[i] >= '0' && s[i] <= '9'; i++) n = 10 * n + s[i] - '0'; return sign * n; } Prednosti držanja iskaza koji kontrolišu petlju na jednom mestu su još očiglednije u situacijama kada postoji nekoliko umetnutih nivoa petlji. Sledeća funkcija predstavlja Shell-ov algoritam za sortiranje polja celih

brojeva. Osnovna ideja ovog algoritma je da se još u ranoj fazi porede udaljeni elementi umesto susednih, kako se radi kod jednostavnijih algoritama za sortiranje. Ovo vodi brzoj eliminaciji većih neuređenih delova, tako da se u kasnijim fazama obavlja manje posla. Razmak između elemenata koji se porede se postepeno smanjuje do jedinice, na kom stepenu se sortiranje jednostavno svodi na izmenu susednih elemenata. /* shellsort: sortira v[0] ... v[n] u rastućem nizu */ void shellsort(int v[], int n) { int gap, i, j, temp; for (gap = n / 2; gap > 0; gap /= 2) for (i = gap; i < n; i++) for (j = i - gap; j > 0 && v[j] > v[j + gap]; j -= gap) { temp = v[j]; v[j] = v[j + gap]; v[j + gap] = temp; } } U ovom primeru postoje tri umetnute petlje. Spoljna petlja kontroliše razmak gap između elemenata koji se porede, i koji je najpre n / 2 , a zatim se sa svakim prolazom smanjuje deljenjem sa dva dok ne postane nula. Petlja u sredini poredi svaki par elemenata čiji je razmak veličine gap ; unutrašnja petlja obrće mesta para elemenata tako da budu u rastućem redosledu. Pošto se razmak gap smanjuje do jedinice, to će svi elementi biti pravilno sortirani. Primetite da se oblik spoljne petlje ne razlikuje od oblika središnje i unutrašnje petlje, iako u spoljnoj petlji nije upotrebljena aritmetička progresija. Jedan od C operatora je i zarez (,), koji najčešće nalazi upotrebu u for konstrukciji. Par izraza odvojenih zarezom se izračunava sleva nadesno, a tip i vrednost rezultata biće tipa i vrednosti desnog operanda. Na taj način je moguće u for konstrukciji smestiti više izraza u različite delove petlje i tako, na primer, paralelno kontrolisati dva brojača. To je ilustrovano funkcijom reverse(s) koja naopačke okreće redosled elemenata u nizu s . #include <string.h> /* reverse: obrtanje niza s */ void reverse(char s[]) { int c, i, j; for (i = 0 , j = strlen(s) - 1 ; i < j ; i++ , j--) { c = s[i]; s[i] = s[j]; s[j] = c; } } Zarezi koji odvajaju argumente funkcija, promenljive u deklaracijama

itd. nisu operatori (,) , i ne garantuju izračunavanje sleva nadesno. Najbolje je koristiti operatore (,) kod izračunavanja izraza koji su upućeni jedni na druge, kao u for (i = 0 , j = strlen(s) - 1 ; i < j ; i++ , j--) { c = s[i] , s[i] = s[j] , s[j] = c; þ Vežba 3 - 3 Napišite funkciju expand(s1, s2) koja proširuje skraćeni zapis a - z iz niza s1 u ekvivalentnu kompletnu listu abc...xyz u nizu s2. Predvidite u programu mogućnost korisćenja malih i velikih slova i cifara, i pripremite program za slučajeve oblika a - b - c ili a - z0 - 9 ili -a-z. Usvojite konvenciju da se znak - na početku ili na kraju skraćenog oblika shvati kao slovo. 3.6 DO - WHILE PETLJE Za petlje while i for je karakteristično da na vrhu petlje ispituju uslovni deo petlje, umesto da to čine na dnu. Treća varijanta petlje u C-u testira ovaj uslov na dnu petlje, posle svakog prolaza kroz petlju; dakle, telo petlje biće izvršeno bar jedanput. Ova konstrukcija je oblika do iskaz while (izraz); Prvo se izvršava telo iskaz, a zatim se ispituje uslov izraz. Ako je istinit, iskaz se izvršava ponovo, ponovo se testira uslov i tako sve dok je izraz istinit. Kad postane netačan, petlja se okončava. Kao što se moglo očekivati, petlja do - while se ređe koristi od petlji while i for, otprilike u svakom dvadesetom slučaju koji zahteva rešenje pomoću petlje. I pored toga, ova konstrukcija je s vremena na vreme korisna, kao što je to slučaj u sledećoj funkciji itoa, koja konvertuje broj u odgovarajući string (obrnuto od funkcije atoi). Posao je malo teži nego što bi se moglo u prvi mah pomisliti, jer jednostavni metodi generišu niz cifara u pogrešnom redosledu. Izabrali smo da se niz cifara generiše u obrnutom redosledu, a zatim da se okrene. /* itoa: konvertuje broj n u niz s */ void itoa(int n, char s[]) { int i, sign; if ( (sign = n) < 0) /* utvrđuje predznak */ n = -n; /* ucini n pozitivnim */ i = 0; do { /* generisi cifre u obrnutom redosledu */

s[i++] = n % 10 + '0'; /* uzmi cifru */ } while ( (n /= 10) > 0); /* obrisi je */ if (sign < 0) s[i++] = '-'; s[i] = '\0'; reverse(s); } Konstrukcija do - while je neophodna, ili bar pogodna, pošto bar jedan znak mora biti smešten u niz s, bez obzira na vrednost broja n. Takođe, dodali smo vitičaste zagrade oko jednog jedinog izraza koji čini telo do - while petlje. Iako su nepotrebne, one će sprečiti nepažljivog čitaoca da deo while smatra početkom neke while konstrukcije. þ Vežba 3 - 4 Napišite sličnu funkciju itob(n, s) koja konvertuje nepredznačen ceo broj n u njegov ekvivalentni binarni oblik koji se smešta u nizu s . Napišite i funkciju itoh koja konvertuje nepredznačen ceo broj u njegovu heksadecimalnu prezentaciju. þ Vežba 3 - 5 Napišite verziju funkcije itoa koja prihvata tri argumenta umesto dva. Treći argument neka bude minimalna veličina polja; dobijeni niz mora se popuniti blanko znakovima ako je potrebno da se ostvari željena veličina polja. 3.7 BREAK I CONTINUE Ponekad je pogodno da postoji mogućnost kontrole petlje ne samo na vrhu i na dnu, već i na nekom drugom mestu. Iskaz break obezbeđuje prevremeni izlazak iz petlji for, while i do, kao i iz switch konstrukcije. Ovaj iskaz izaziva trenutan izlazak čak i iz najuvučenije od nekoliko umetnutih petlji (ili iz switch konstrukcije). Sledeći program uklanja blanko znakove, znakove za novi red i tabulatore polazeći od kraja linije sa ulaza, i koristeći iskaz break za izlazak iz petlje onog trenutka kad naiđe na znak niza koji nije jedan od specijalnih znakova. /* trim: uklanja specijalne znakove sa kraja linije */ int trim(char s[]) { int n; for (n = strlen(s) - 1; n >= 0; n--) if (s[n] != ' ' && s[n] != '\n' && s[n] != '\t') break; s[n+1] = '\0'; return n; }

Funkcija strlen vraća u program dužinu niza s. Petlja for počinje pretraživanje od kraja niza uklanjajući specijalne znake sve dok ne naiđe na znak koji nije iz ove grupe znakova , ili dok brojač znakova niza ne postane negativan (tj. kad se cela linija pretraži). Trebalo bi da zaključite da se program ispravno ponaša čak i kad je linija prazna ili sadrži samo specijalne znake. Iskaz continue je povezan sa iskazom break, ali se ređe koristi; on izaziva početak sledećeg prolaska kroz petlju u kojoj je naveden (for, while, do). U petljama while i do ovo znači da će njihov test deo biti smesta izvršen, a u for petlji znači da će se smesta izvršiti reinicijalizacija brojača. Iskaz continue se primenjuje samo na petlje, ne i na konstrukciju switch. Ako je switch konstrukcija ubačena unutar neke petlje, tada će iskaz continue naveden u switch konstrukciji izazvati sledeću iteraciju petlje. Kao primer upotrebe iskaza continue, evo petlje koja operiše samo sa pozitivnim elementima nekog polja a; negativne vrednosti se preskaču. for (i = 0; i < N; i++) { if (a[i] < 0) /* preskoci negativne elemente */ continue; ... /* operacije nad pozitivnim elementima */ } Iskaz continue se koristi u situacijama kada je deo petlje koji sledi komplikovan, pa bi obrtanje uslova i uvlačenje još jednog nivoa programa bilo previše. þ Vežba 3 - 6 Napišite program koji kopira liniju sa ulaza na izlaz, ali tako što štampa samo jednu iz grupe uzastopnih identičnih linija. (Ovo je uprošćena verzija UNIX rutine uniq). 3.8 GOTO I LABELE C obezbeđuje ne mnogo korisnu naredbu goto, i labele (oznake) kojima se označavaju mesta na koja se skok vrši. Opšte uzev, iskaz goto se nikad ne mora upotrebljavati, a i u praksi je gotovo uvek jednostavnije napisati program bez njega. U ovoj knjizi nismo koristili iskaz goto. I pored toga, pokazaćemo vam par situacija u kojima bi iskaz goto mogao naći primenu. Najčešći slučaj je potreba trenutnog izlaza iz neke duboko umetnute strukture, kao što je izlazak iz dve ili više petlja istovremeno. U ovakvom slučaju ne može biti upotrebljen iskaz break, jer on obezbeđuje izlazak iz samo jedne petlje. for ( ... ) for ( ... ) { ... if (katastrofa)

goto greska; } ... greska: sredi stanje Ova organizacija je pogodna u situacijama kada rutina za otklanjanje greške nije jednostavna i stoga mora biti izdvojena, ili ako se greška može pojaviti na više mesta i nije moguće na svakom od tih mesta praviti posebnu rutinu za njeno otklanjanje. Labela (oznaka) ima isti oblik kao i ime promenljive, i praćena je dvotačkom. Može biti pridružena bilo kom iskazu unutar funkcije u kojoj je goto. Kao drugi primer, razmotrite problem nalaženja prvog negativnog elementa u dvodimenzionalnom polju. Višedimenzionalna polja su opisana u Poglavlju 5. for (i = 0; i < N; i++) for (j = 0; j < M; j++) if (v[i][j] < 0) goto nađeno; /* element nije pronađen */ . . . nađeno: /* nađen je element na poziciji (i,j) */ . . . Program pisan uz upotrebu iskaza goto uvek može biti napisan i bez njega, premda možda po cenu nekih ponavljanja testova ili neke dodatne promenljive. Na primer, isti program za traženje negativnog elementa u dvodimenzionalnom polju izgledao bi ovako: nađeno = 0; for (i = 0; i < N && !nađeno; i++) for (j = 0; j < M && !nađeno; j++) nađeno = v[i][j] < 0; if (nađeno) /* pronađen je jedan element na (i-1,j-1) */ . . . else /* nije pronađen */ . . . Sa izvesnim izuzecima koji su ovde navedeni, program pisan uz upotrebu goto naredbi je ipak teži za razumevanje od onog koji je pisan bez njih. Iako nismo dogmatični u odnosu na ovu materiju, čini nam se da goto iskaze treba koristiti veoma retko, ako ih uopšte treba koristiti. P o g l a v l j e 4 : FUNKCIJE I PROGRAMSKE STRUKTURE

Funkcije razbijaju veća izračunavanja na manje celine, i omogućuju ljudima da nastave razvoj programa na već pripremljenoj osnovi umesto da sve počinju ispočetka. Dobro napisane funkcije često mogu odvojiti detalje neke operacije od drugih celina u programu koje ne moraju da znaju za njih. To čini program jasnijim i olakšava posao oko njegovog ispravljanja. C je dizajniran tako da čini funkcije efikasnim i lakim za upotrebu: C programi se u opštem slučaju sastoje iz većeg broja manjih funkcija umesto iz dve tri ogromne. Programi mogu biti izvedeni u vidu jednog ili više izvornih programa; ti programi se mogu kompajlirati svaki za sebe, a onda učitati zajedno i povezati sa ranije kompajliranim funkcijama koje čine neku biblioteku. Mi se ovde nećemo baviti tim procesima, pošto se detalji razlikuju od sistema do sistema. Deklaracija i definicija funkcije predstavljaju oblast u kojoj je ANSI standard izvršio najuočljivije promene C-a. Ono što smo već mogli zapaziti u prvom poglavlju, je da je sada moguće deklarisati tipove argumenata pre deklarisanja funkcije. Sintaksa definicije funkcije se sada takodje menja, tako da se deklaracije i definicije medjusobno prepliću. To omogućava kompajleru da otkrije mnogo više grešaka nego ranije, i ne samo to: kada su argumenti pravilno deklarisani, automatski se vrši smanjivanje odgovarajućeg broja tipova. Standard razjašnjava pravila koja se tiču imena; konkretno, on zahteva da postoji samo jedna definicija svakog spoljašnjeg objekta. Inicijalizacija je uopštenija: automatska polja i strukture se sada mogu inicijalizovati. Većina programera je već upoznata sa ulazno izlaznim funkcijama (getchar, putchar) i sa numeričkim funkcijama (sin, cos, sqrt). U ovom poglavlju ćemo govoriti više o pisanju novih funkcija. 4.1 OSNOVNE NAPOMENE Za početak, dizajnirajmo i napišimo program koji štampa svaku liniju nekog ulaza koja sadrži odredjen niz znakova. (To je specijalni slučaj UNIX programa grep). Na primer, traženje niza 'the' u grupi linija Now is the time for all good men to come to the aid of their party.

proizvešće na izlazu Now is the time men to come to the aid of their party. Osnovna struktura posla prirodno se deli na tri celine: while (još ima linija) if (linija sadrži traženi niz) štampaj liniju Iako je zaista moguće smestiti sve tri rutine u jedan, glavni program, bolje je upotrebiti prirodnu strukturu stvaranjem svakog dela kao odvojene funkcije. Sa tri manja dela je lakše baratati nego sa jednim većim, jer manje važni detalji mogu biti sklonjeni u funkcije, pa će i mogućnost nekih neželjenih uticaja biti minimalna. Delovi programa mogu čak biti upotrebljeni i sami za sebe. Deo 'while (ima još linija)' je getline, funkcija koju smo napisali u Poglavlju 1, a deo 'štampaj liniju' je funkcija printf koja je već napisana za nas. To znači da nam ostaje da napišemo rutinu koja ispituje da li se traženi niz pojavljuje u liniji. Problem možemo rešiti na sledeći način: funkcija index(s, t) vraća u program poziciju ili indeks mesta u nizu s od kog počinje niz t, odnosno -1 ako niz t nije pronadjen u nizu s. Izabrali smo da označimo startnu poziciju u nizu s sa nula umesto sa jedan, jer u C-u polja počinju sa indeksom nula. Kada nam kasnije budu potrebne inteligentnije rutine za pretraživanje niza, moraćemo zameniti samo funkciju index; ostali deo programa može ostati nepromenjen. Sa programom napisanim u ovakvom obliku, pristup detaljima programa je trenutan. Evo celog programa, tako da možete videti kako se delovi uklapaju jedan u drugi. Za sada, niz koji se traži je ograničen na slova, što nije najopštiji slučaj. Vratićemo se za kratko na diskusiju o tome kako se polje znakova inicijalizuje (elementi postavljaju na početne vrednosti), a u Poglavlju 5 ćemo pokazati kako da se niz predstavi kao parametar koji se podešava u toku programa. Ovde je takodje prikazana i nova verzija funkcije getline; korisno je uporediti je sa onom iz Poglavlja 1. #include <stdio.h> #define MAXLINE /* max duzina linije sa ulaza */ int getline(char line[], int max); int strindex(char source[], char searchfor[]); char pattern[] = „the“; /* uzorak koji se trazi */ main() /* nadji sve linije koje sadrze uzorak */ { char line[MAXLINE]; int found = 0;

while (getline(line, MAXLINE) > 0) if (index(line, pattern) >= 0) { printf(„%s“, line); found++; } return found; } /* getline: unesi liniju u niz s , vrati njenu duzinu */ int getline(char s[], int lim) { int c, i; i = 0; while (--lim > 0 && (c = getchar()) != EOF && c != '\n') s[i++] = c; if (c == '\n') s[i++] = c; s[i] = '\0'; return i; } /* index: vrati lokaciju niza t, odnosno -1 ako ga nema */ int index(char s[], char t[]) { int i, j, k; for (i = 0; s[i] != '\0'; i++) { for (j = i, k = 0; t[k] != '\0' && s[j] == t[k];j++, k++) ; if (k > 0 && t[k] == '\0') return i; } return -1; } Program za traženje niza znakova završava se izlaskom iz main funkcije, i vraća broj pronadjenih podudarnosti. Ovu vrednost može iskoristiti okruženje koje je pozvalo program. Svaka funkcija ima oblik: povratni tip ime funkcije(deklaracije argumenata) { deklaracije i izrazi } Pri tome, mogu nedostajati različiti delovi ove strukture; najkraća funkcija je dummy() {} koja ne radi ništa, i ne vraća ništa. Funkcija koja ne radi ništa je ponekad korisna za čuvanje prostora tokom razvoja programa. Imenu funkcije može

prethoditi tip povratne vrednosti u slučaju da funkcija vraća u program vrednost drugačijeg tipa od tipa int; ovo je tema sledećeg odeljka. Program je samo skup pojedinačnih definicija funkcija. Komunikacija izmedju funkcija je (u ovom slučaju) preko argumenata i vrednosti koje u program vraćaju funkcije; komunikacija takodje može biti ostvarena i preko spoljnih promenljivih. Funkcije se u izvornom programu mogu pojavljivati u bilo kom redosledu, a izvorni program može biti podeljen na više datoteka, s tim da se funkcije ne mogu deliti. Iskaz return predstavlja način da se iz pozvane funkcije neka vrednost vrati onom delu programa koji ju je pozvao (nekoj funkciji). Iza iskaza return može slediti bilo kakav izraz return (izraz) Funkcija iz koje je upućen poziv ima slobodu da ignoriše vraćenu vrednost ako joj to odgovara. Žtaviše, iza iskaza return ne mora biti naveden nikakav izraz; tada se funkciji iz koje je upućen poziv ne vraća nikakva vrednost. Funkciji iz koje je upućen poziv kontrola se takodje vraća i kada se u toku izvršavanja programa naidje na desnu vitičastu zagradu koja označava kraj pozvane funkcije. Ni tada se ne vraća nikakva vrednost. Nije nedozvoljeno, ali može biti znak nekog problema kada funkcija sa jednog svog mesta vraća vrednost a sa drugog ne. U svakom slučaju je 'vrednost' funkcije, deklarisane da ne vraća vrednost, nedefinisana (nepoznata). Način na koji se kompajlira i učitava program koji se sastoji iz više izvornih programa razlikuje se od računara do računara. U UNIX operativnom sistemu, na primer, komanda cc pomenuta u Poglavlju 1 obavlja taj posao. Pretpostavimo da smo tri funkcije, main.c, getline.c i index.c pisali odvojeno i da se stoga one nalaze u tri odvojena izvorna programa. Tada će komanda cc main.c getline.c index.c kompajlirati sva tri programa praveći objektne datoteke main.o, getline.o i index.o i povezati ih u izvršni program a.out. Ako, recimo, u delu main.c postoji greška, on se može ponovo zasebno kompajlirati i rezultat ubaciti u već napravljene objektne datoteke, komandom cc main.c getline.o index.o Komanda cc upotrebljava nastavke .c i .o da bi razgraničila izvorne programe od objektnih. þ Vežba 4 - 1 Napišite funkciju rindex(s, t) koja vraća poziciju krajnjeg desnog pojavljivanja niza t u nizu s, odnosno -1 ako niz t nije pronadjen.

4.2 FUNKCIJE KOJE NE VRAĆAJU I N T VREDNOSTI Do sada, nijedna od deklarisanih funkcija nije u program vraćala vrednosti tipa drugačijeg od void ili int. Žta ako funkcija mora da vrati neki drugi tip? Mnoge numeričke funkcije, kao što su sin, cos ili sqrt vraćaju vrednost tipa double; druge specijalizovane funkcije vraćaju vrednosti drugog tipa. Da bi pokazali kako da postupamo sa tim, napišimo i upotrebimo funkciju atof koja konvertuje realan broj predstavljen nizom s u njegovu ekvivalentnu numeričku vrednost predstavljenu u formatu realnog broja. Funkcija atof je proširenje funkcije atoi koju smo pisali u Poglavljima 2 i 3; ona barata sa eventualno navedenim predznakom i decimalnom tačkom, kao i sa varijantama zapisa bez celobrojnog dela ili bez dela iza decimalne tačke. To ipak nije visokokvalitetna rutina za konverziju; takva rutina bi zauzimala mnogo više prostora nego što mi imamo. Ako tip vrednosti koji neka funkcija vraća nije naveden, podrazumeva se da je int. Dakle, funkcija atof mora najpre deklarisati tip vrednosti koji će vratiti, pošto to neće biti tip int. Ako želimo da se vraćena vrednost predstavi u formatu dvostruke preciznosti, deklarisaćemo funkciju atof da vrati vrednost tipa double. Ime tipa prethodi imenu funkcije: #include <ctype.h> /* atof: konvertuje niz s u broj tipa double */ double atof(char s[]) { double val, power; int i, sign; for (i = 0; isspace(s[i]); i++) /* preskace spec. znake */ ; sign = (s[i] == '-') ? -1 : 1 ; if (s[i] == '+' || s[i] == '-') i++; for (val = 0.0; isdigit(s[i]); i++) val = 10.0 * val + (s[i] - '0'); if (s[i] == '.') i++; for (power = 1.0; isdigit(s[i]); i++) { val = 10.0 * val + (s[i] - '0'); power *= 10.0; } return sign * val / power; } Drugo, i ne manje važno, je da funkcija koja upućuje poziv zna da funkcija atof vraća vrednost koja nije ceo broj. Jedan od načina da se to ostvari je da se funkcija atof jasno deklariše u funkciji iz koje se upućuje poziv. Deklaracija je prikazana na sledećem primeru jednostavnog kalkulatora

(dovoljnog samo za kontrolu salda), koji čita jedan broj po liniji (eventualno predznačen) i štampa zbir posle svakog sabirka: #include <stdio.h> #define MAXLINE /* osnovni kalkulator */ main() { double sum, atof(char[]); char line[MAXLINE]; int getline(char line[], int max); sum = 0; while (getline(line, MAXLINE) > 0) printf(„\t%g\n“, sum += atof(line)); return 0; } Deklaracija double sum, atof(char[]); govori da je promenljiva sum tipa double, a da je atof funkcija koja očekuje argument tipa char, a u program vraća vrednost tipa double. Ako funkcija atof nije pravilno deklarisana na svim mestima u programu, C pretpostavlja da u program vraća vrednost tipa int - u tom slučaju ćete dobiti besmislene rezultate. Ako su funkcija atof i njen poziv u funkciji main neusaglašeni, a nalaze se u istom izvornom programu, kompajler će otkriti grešku. Medjutim, ako se (što je verovatnije) funkcija atof kompajlira odvojeno, onda neslaganje tipova neće biti otkriveno i stoga će funkcija atof vraćati vrednost tipa double koju će funkcija main tretirati kao int i dobiće se besmisleni rezultati. Pomenuta činjenica da deklaracije moraju odgovarati definicijama, možda predstavlja iznenadjenje. Do neusaglašenosti dolazi zato što se, ako nema prototipa funkcije, funkcija implicitno deklariše svojim prvim pojavljivanjem u izrazu, kao u sum += atof(line) Ako se ime koje dotad nije nigde deklarisano pojavi u nekom izrazu, a posle njega sledi leva mala zagrada, ono se po kontekstu smatra imenom funkcije; za funkciju se pretpostavlja da vraća int tip, a o njenim argumentima se ništa ne zna. Žtaviše, ako funkcija ne navodi argumente, kao u double atof(); onda to znači da se ništa ne može zaključiti o argumentima funkcije atof; sve provere parametara se isključuju. Prazna lista argumenata ima za cilj da omogući starijim C programima kompajliranje pomoću novih kompajlera. Ali, nije dobro da se oni koriste uz nove programe. Ako funkcija ima argumente,

deklarišite ih; ako ne uzima, koristite tip void. Kada je data funkcija atof pravilno deklarisana, možemo napisati funkciju atoi (koja konvertuje niz u ceo broj) na sledeći način: /* atoi: konverzija niza u ceo broj korišcenjem funkcije atof */ int atoi(char s[]) { double atof(char s[]); return (int) atof(s); } Obratite pažnju na strukturu deklaracija i iskaz return. Vrednost izraza izraz u konstrukciji return izraz; se konvertuje u tip koji ta funkcija vraća. Na taj način, vrednost funkcije atof, tipa double, je konvertovana automatski u tip int onog trenutka kada se pojavila u iskazu return, jer funkcija atoi vraća vrednost tipa int. Konverzija realnog broja u ceo broj rezultira odbacivanjem dela iza decimalne tačke, kao što je to pomenuto u Poglavlju 2. Ovakva operacija može da učini informaciju nekorisnom, i zato neki kompajleri upozoravaju na to. Dati model jasno pokazuje da se ovakva operacija očekuje i stoga povlači upozorenje. þ Vežba 4 - 2 Proširite funkciju atof tako da radi i sa naučnom notacijom brojeva 123.45e-6 gde iza realnog broja može slediti slovo e ili E i eventualno predznačeni eksponent. 4.2.1 Argumenti funkcija U Poglavlju 1 smo diskutovali o činjenici da se izmedju funkcija komunikacija odvija preko vrednosti, tj. da pozvana funkcija prima privremenu, lokalnu kopiju svakog argumenta, a ne njegovu stvarnu adresu. To znači da funkcija ne može da izmeni originalni argument u funkciji iz koje je pozvana. Unutar pozvane funkcije, svaki argument je predstavljen lokalnom promenljivom koja je postavljena na vrednost koja je prosledjena funkciji prilikom poziva. Kada se ime polja pojavi kao argument funkcije, prosledjuje se početak polja, pa nema potrebe da se kopiraju elementi. Pozvana funkcija može izmeniti neki element polja tako što će dodati indeks na početnu lokaciju i dobiti lokaciju tog elementa. Ovde se prenose elementi polja, a ne njihove vrednosti. U poglavlju 5 ćemo diskutovati o upotrebi pokazivača u cilju omogućavanja pozvanoj funkciji da izmeni argument u funkciji iz koje je pozvana. Uzgred, ne postoji potpuno zadovoljavajući način da se napiše prenosiva

funkcija koja prihvata promenljiv broj argumenata, jer nema pogodnog načina da pozvana funkcija odredi koliko joj je argumenata zaista poslato datim pozivom. Zbog toga, vi niste u stanju da napišete potpuno prenosivu funkciju koja izračunava najveći od proizvoljnog broja argumenata. U opštem slučaju je bezbedno raditi sa promenljivim brojem argumenata ako pozvana funkcija ne koristi argument koji nije obezbedjen. Funkcija printf, najopštija C funkcija sa promenljivim brojem argumenata, koristi informaciju dobijenu od svog prvog argumenta da odredi koliko je još argumenata prisutno i kojeg su tipa. Do kraha dolazi ako funkcija koja je uputila poziv nije obezbedila dovoljan broj argumenata, ili ako se tipovi argumenata razlikuju od tipova koji su navedeni u prvom argumentu. Ova funkcija takodje nije prenosiva i mora se modifikovati za različita okruženja. Sa druge strane, ako su argumenti poznatih tipova, moguće je označiti kraj liste argumenata na neki unapred dogovoren način, kakav je posebna vrednost argumenta (obično nula) koja označava kraj liste. 4.3 SPOLJNE PROMENLJIVE C program se sastoji od skupa spoljnih objekata, koji mogu biti ili promenljive ili funkcije. Pridev 'spoljni' se uglavnom koristi kao kontrast pridevu 'unutrašnji' koji opisuje argumente i automatske promenljive definisane unutar neke funkcije. Spoljne promenljive su definisane izvan svih funkcija, i stoga su potencijalno pristupačne mnogim funkcijama. Funkcije su same po sebi spoljne, jer C ne dozvoljava definisanje funkcija unutar drugih funkcija. Za spoljne promenljive se podrazumeva da su 'opšte'. To znači da su sva pristupanja takvoj promenljivoj pomoću istog imena (čak i ako su funkcije kompajlirane odvojeno), ustvari pristupanja jednom istom objektu. Kasnije ćemo videti kako se definišu spoljne promenljive i funkcije koje nisu svima pristupačne, već su vidljive samo za objekte iz njihove izvorne datoteke. Zbog činjenice da su svima pristupačne, spoljne promenljive predstavljaju alternativni način komunikacije podacima izmedju funkcija, umesto argumenata i vraćenih vrednosti. Bilo koja funkcija može pristupiti spoljnoj promenljivoj navodeći njeno ime, ako je to ime pre toga na neki način deklarisano. Ako je potrebno da funkcije razmenjuju veći broj promenljivih, spoljne promenljive su pogodnije i efikasnije od dugih lista argumenata. Kako je već ukazano u Poglavlju 1, ovakvo razmišljanje mora biti praćeno odredjenom dozom opreza, jer može imati loše posledice po strukturu programa, i voditi programima sa isprepletanim vezama izmedju funkcija. Drugi razlog za korišćenje spoljnih promenljivih leži u opsegu u kome

one važe i u vremenu njihovog trajanja. Automatske promenljive postoje samo unutar funkcije. One nastaju sa ulaskom u pozvanu funkciju, i nestaju na njenom izlazu. Sa druge strane, spoljne promenljive su trajne. One se ne pojavljuju i ne nestaju sa pozivom funkcije, pa tako zadržavaju svoju vrednost i izmedju dva poziva funkcije. Stoga ako dve funkcije moraju pristupati istom podatku, a nijedna ne poziva onu drugu, najpogodnije je da se zajednički podatak čuva u spoljnoj promenljivoj umesto da mu se pristupa preko argumenata. Ispitajmo ove konstatacije na složenijem primeru. Problem je napisati još jedan program koji će raditi kao kalkulator, bolji od prethodnog. Ovaj treba da dozvoli operacije +, -, *, / i = (da bi odštampao rezultat). Zbog toga što ju je iz nekog razloga lakše realizovati, kalkulator će koristiti obrnutu poljsku notaciju umesto infiks notacije. U obrnutoj poljskoj notaciji svaki operator se navodi iza svojih operanada; infiks notacija kao (1 - 2) * (4 + 5) = treba da se unese kao 1 2 - 4 5 + * = Zagrade nisu potrebne. Realizacija je sasvim jednostavna. Svaki operand se stavlja na stek; kada program naidje na operator, sa steka se skida odgovarajući broj operanada (dva za binarne operatore), nad njima se obavlja ta operacija, i rezultat se ponovo stavlja na stek. U gornjem primeru, recimo, operandi 1 i 2 se stavljaju na stek, program nailazi na binarni operator -, skida dva operanda sa steka i na njih primenjuje operaciju oduzimanja. Rezultat -1 se zatim vraća na stek. Zatim se na stek stavljaju operandi 4 i 5, program nailazi na operator +, izvršava operaciju nad operandima koje skida sa steka, i na stek vraća rezultat, broj 9. Na steku su sada dva rezultata: -1 i 9. Program nailazi na operator *, množi dva operanda skinuta sa steka, i rezultat -9 vraća na stek. Naišavši na operator =, program štampa element koji je poslednji stavljen na stek (pri tome ga ne skida sa steka). Realizacija stavljanja elemenata na stek i skidanja sa njega je krajnje jednostavna, ali pošto su joj u programu dodati delovi za otkrivanje i otklanjanje greške, postala je dovoljno velika da bi se napisala kao odvojena funkcija. To je bolja varijanta nego da se njeni delovi ponavljaju kroz ceo program. Takodje je potrebno da postoji funkcija koja preuzima sledeći operand ili operator sa ulaza. Zbog toga program ima sledeću strukturu: while (sledeći operator ili operand nije kraj ulaza) if (jeste broj) stavi ga na stek else if (jeste operator) skini operand(e) sa steka izvrši operaciju

stavi rezultat na stek else greška - izbaci poruku Ključno pitanje o kome još nije bilo reči je gde je stek, tj. koje funkcije mu direktno pristupaju. Jedna mogućnost je da se stek izvede u okviru funkcije main, a da se on i njegova trenutna pozicija prosledjuju rutinama koje stavljaju ili skidaju podatke sa njega. Ali, funkcija main ne mora da zna ništa o promenljivama koje kontrolišu stek; ona treba jedino da vodi računa o tome kada treba staviti ili skinuti podatak sa steka. Stoga smo odlučili da stek i njemu pridružene informacije izvedemo u vidu spoljnih (extern) promenljivih kojima mogu pristupiti funkcije push i pop, ali ne i funkcija main. Pretvaranje algoritma u program je dovoljno lako. Funkcija main je u suštini velika switch konstrukcija koja se grana u zavisnosti od tipa operatora i operanda; ovo je možda čak i tipičnija upotreba iskaza switch od one pokazane u Poglavlju 3. Ako sada pretpostavimo da su sve funkcije u istoj izvornoj datoteci, onda će program imati sledeću strukturu: #include(s) #define(s) deklaracije funkcija osim za funkciju main main() {...} spoljne promenljive za funkcije push i pop ... push(...) {...} ... pop(...) {...} ... getop(...) {...} funkcije koje poziva funkcija getop Kasnije ćemo razmotriti kako bi se ovaj program mogao podeliti na dve ili više izvornih datoteka. #include <stdio.h> #include <math.h> /* za f-ju atof */ #define MAXOP 100 /* max velicina operanada ili operatora */ #define NUMBER '0' /* signal da je broj pronadjen */ int getop(char []); void push(double); double pop(void); void clear(void); /* kalkulator sa inverznom poljskom notacijom */ main() { int type; double op2; char s[MAXOP]; while ( (type = getop(s) ) != EOF) {

switch (type) { case NUMBER: push (atof(s)); break; case '+': push(pop() + pop()); break; case '*': push(pop() * pop()); break; case '-': op2 = pop(); push(pop() - op2); break; case '/': op2 = pop(); if (op2 != 0.0) push(pop() / op2); else printf(„greska: deljenje nulom\n“); break; case '=': printf(„\t%f\n“, push(pop())); break; case 'c': clear(); break; default: printf(„Nepoznata komanda %c\n“, type); break; } } return 0; } Pošto su + i * komutativni operatori, nije bitan redosled kojim se operandi skinuti sa steka kombinuju; već za operatore - i / nije svejedno koji je levi a koji desni operand. Da smo za operaciju oduzimanja pisali push(pop() - pop()); /* pogresno */ sa steka bi prvim pozivom funkcije pop() bio skinut operand koji se oduzima, a drugim pozivom operand od koga se oduzima. Funkcijom push bi se na stek poslao rezultat oduzimanja drugog skinutog operanda od prvog, što je pogrešno. Zato je uvedena promenljiva op2 koja omogućava pravilno oduzimanje. Isto važi i za operaciju deljenja. #define MAXVAL 100 /* max dubina steka */ int sp = 0; /* pokazivac pozicije na steku */ double val[MAXVAL]; /* stek */

/* push: stavi vrednost f na stek */ void push(double f) { if (sp < MAXVAL) val[sp++] = f; else printf(„greska: stek je pun, nema mesta za %g\n“, f); clear(); } /* pop: skini vrednost sa steka */ double pop(void) { if (sp > 0) return val[--sp]; else { printf(„greska: stek je prazan\n“); clear(); return 0.0; } } /* clear: brise stek */ void clear(void) { sp = 0; } Komanda c briše stek pomoću funkcije clear koju takodje koriste i funkcije push i pop u slučaju greške. Uskoro ćemo se vratiti pisanju funkcije getop. Kako je rečeno u Poglavlju 1, promenljiva je spoljna ako je definisana izvan tela bilo koje funkcije. Zbog toga su, s obzirom na to da im pristupaju funkcije push, pop, i clear, stek i njegov pokazivač lokacije definisani izvan ove tri funkcije. Sama funkcija main se ne bavi stekom i pozicijom na njemu, pa iskazi u vezi sa stekom i nisu navedeni. Okrenimo se sada realizaciji funkcije getop, koja sa ulaza uzima sledeći operator ili operand. U osnovi, zadatak je lak: preskočiti blanko znakove, tabulatore i znake za novi red. Ako sledeći znak sa ulaza nije cifra ili decimalna tačka, kontrola se vraća funkciji main. Ako jeste, skuplja se niz cifara (koji može uključivati i decimalnu tačku) i u program vraća vrednost NUMBER, signal da je skupljanje cifara završeno i broj kompletiran. Funkcija se postepeno komplikuje pokušajem da se pravilno rukuje jednačinom u situaciji kada je ulazni broj predugačak. Funkcija getop čita cifre (sa eventualnom decimalnom tačkom) sve dok ne nadje više nijednu, a smešta i čuva samo one za koje ima mesta. Ako nije došlo do prekoračenja, ova funkcija će vratiti u program vrednost NUMBER i niz cifara. Ako je broj na

ulazu bio predugačak, funkcija getop će odbaciti ostatak ulazne linije koji nije stao. #include <ctype.h> int getch(void); void ungetch(int); /* getop: uzima sledeci operator ili numerički operand */ int getop(char[s]) { int i, c; while ((s[0] = c = getch()) == ' ' || c == '\t' || c =='\n') ; s[1] = '\0'; if (!isdigit(c) && c != '.') return c; /* nije broj */ i = 0; if (isdigit(c)) while (isdigit(s[++i] = c = getch())) /* celobrojni deo */ ; if (c == '.') /* deo iza decimalne tačke */ while (isdigit(s[++i] = c = getch())) ; s[i] = '\0'; if (c != EOF) ungetch(c); return NUMBER; } Žta predstavljaju funkcije getch i ungetch? šest je slučaj da program ne može da odredi da li je učitao dovoljno sve dok ne učita previše. Jedan primer za to je skupljanje znakova koji čine broj: sve dok se ne pojavi prvi znak koji nije cifra, broj nije kompletiran. Ali, onda je program očitao jedan znak previše, znak za koji nije pripremljen. Problem bi se mogao rešiti da je moguće ne očitati neželjeni znak. Onda bi, svaki put kada očita jedan znak previše, program vratio taj znak nazad na ulaz i ostatak programa bi se ponašao kao da znak nikad nije ni bio učitan. Srećom, lako je izvesti simulaciju vraćanja znaka; to ćemo postići pisanjem dve sadejstvujuće funkcije. Jedna je funkcija getch, koja uzima sledeći znak koji treba ispitati, a druga je funkcija ungetch koja vraća znak nazad na ulaz tako da ga odande novi poziv funkcije getch može opet uzeti. Način na koji ove funkcije rade zajedno je jednostavan. Funkcija ungetch će prekobrojne znakove staviti u za to odredjeni bafer - polje znakova. Funkcija getch će čitati te znakove iz bafera ako ih tamo ima; ako je bafer prazan (nije bilo prekobrojnih znakova), ova funkcija će se obratiti funkciji getchar da bi uzela novi znak sa ulaza. Za tu priliku mora postojati i promenljiva koja drži trenutnu poziciju znaka koji treba uzeti iz bafera. Ta promenljiva će praktično držati indeks nekog elementa polja koje predstavlja bafer.

Pošto polje (bafer) i indeks moraju zadržavati svoje vrednosti izmedju poziva funkcija, i pošto ih zajednički koriste i funkcija getch i funkcija ungetch, to ove promenljive moraju biti deklarisane kao extern (spoljne) u odnosu na ove dve rutine. Tako možemo funkcije getch, ungetch i njihove zajedničke promenljive napisati kao #define BUFSIZE 100 char buf[BUFSIZE]; /* polje koje predstavlja bafer */ int bufp = 0; /* sledeća slobodna pozicija u baferu */ int getch(void) /* uzmi (mozda ranije vraćeni) znak */ { return (bufp > 0) ? buf[--bufp] : getchar(); } void ungetch(int c) /* vraća znak nazad na ulaz */ { if (bufp >= BUFSIZE) printf(„ungetch: previše znakova\n“); else buf[bufp++] = c; } Kao bafer smo upotrebili polje umesto samo jedne char promenljive, jer će nam to uopštenje možda dobro doći kasnije. þ Vežba 4 - 3 Kada je poznata osnovna struktura, nije teško izvršiti proširivanje kalkulatora. Dodajte modul (%) i odredbe za negativne brojeve. þ Vežba 4 - 4 Dodajte mogućnost pristupa funkcijama standardne biblioteke kao što su sin, exp, i pow. Pogledajte <math.h> u dodatku B, Odeljak 4. þ Vežba 4 - 5 Napišite funkciju ungets(s) koja će vratiti ceo niz nazad na ulaz. Da li funkcija ungets treba da zna za promenljive buf i bufp, ili treba da koristi samo funkciju ungetch? þ Vežba 4 - 6 Pretpostavite da nikad neće biti više od jednog znaka koji treba vratiti na ulaz. Shodno tome, izmenite funkcije getch i ungetch. 4.4 PRAVILA OPSEGA Funkcije i spoljne promenljive koje čine jedan C program ne moraju biti kompajlirane u isto vreme; izvorni tekst se može čuvati u više datoteka, a prethodno kompajlirane rutine mogu se učitati iz biblioteka. Evo nekoliko za nas važnijih pitanja:

ú kako napisati deklaracije da bi promenljive bile pravilno deklarisane tokom kompajliranja ? ú kako rasporediti deklaracije tako da delovi budu pravilno rasporedjeni prilikom učitavanja programa ? ú kako organizovati deklaracije da se pojavi samo jedna kopija ? ú kako inicijalizovati spoljne promenljive ? Razmotrimo ova pitanja da bismo reorganizovali program za kalkulator i raščlanili ga na nekoliko datoteka. Praktično, program je suviše mali da bi se delio, ali može poslužiti kao lepa ilustracija problema koji se javljaju u većim programima. Opseg nekog imena je onaj deo programa u okviru koga je to ime definisano i poznato. Za automatsku promenljivu deklarisanu na početku neke funkcije, opseg predstavlja ta funkcija. Lokalne promenljive u drugim funkcijama koje imaju isto ime nisu u vezi sa tom promenljivom. Isto važi i za argumente funkcije. Opseg spoljne promenljive se prostire od mesta na kome je deklarisana u izvornom programu, pa do kraja izvornog programa. Na primer, ako su main, sp, val, push i pop definisani u istoj datoteci po gore prikazanom redosledu, tj. main() { ... } int sp = 0; double val[MAXVAL]; void push(double f) { ... } double pop(void) { ... } onda se promenljive val i sp mogu biti korišćene u funkcijama push i pop jednostavnim navođenjem njihovih imena; nikakve dodatne deklaracije nisu potrebne. Sa druge strane, ako je potrebno da se neka spoljna promenljiva upotrebi u programu pre mesta na kome je definisana, ili je mesto gde je definisana u nekoj drugoj datoteci, onda je na tom mestu upotrebe neophodna i deklaracija extern. Važno je uočiti razliku između deklaracije neke spoljne promenljive i njene definicije. Deklaracija uvodi karakteristike promenljive: tip, veličinu u bajtovima itd.; definicija uz to vrši odvajanje memorijskog prostora u kome će promenljiva čuvati svoju vrednost. Ako se izrazi int sp; double val[MAXVAL]; pojave izvan bilo koje funkcije, oni definišu spoljne promenljive sp i val, prouzrokuju odvajanje memorije i uz sve to služe kao njihove deklaracije za ostali deo izvornog programa. Sa druge strane, izrazi extern int sp;

extern double val[]; deklarišu u ostatku datoteke promenljivu sp kao int i promenljivu val kao polje čija veličina je definisana na nekom drugom mestu i čiji su elementi tipa double, ali se pri tome ne stvaraju te promenljive niti se rezerviše memorija za njih. Mora postojati samo jedna definicija spoljne promenljive i biti smeštena u jednoj od datoteka koje zajedno čine izvorni program; ostale datoteke mogu koristiti extern deklaracije da bi pristupile toj promenljivoj. Deklaracija extern, naravno, može postojati i u datoteci u kojoj je i definicija spoljne promenljive. Bilo kakva inicijalizacija spoljne promenljive može se sprovesti samo kroz njenu definiciju. Žto se tiče polja, njihova veličina mora biti navedena u definiciji, dok u extern deklaraciji ne mora. Iako je malo verovatno da bi se tako izvela, organizacija ovog programa mogla je biti sledeća: val i sp mogli su biti definisani i inicijalizovani u jednoj datoteci, a funkcije push, pop i clear u drugoj. Tada bi sledeće definicije i deklaracije bile neophodne za njihovo povezivanje: U datoteci 1 extern int sp; extern double val[]; void push(double f) { ... } double pop(void) { ... } void clear(void) { ... } U datoteci 2 int sp = 0; double val[MAXVAL]; Pošto extern deklaracije u datoteci 1 leže ispred i izvan definicija pomenute tri funkcije, one se odnose i primenjuju na sve funkcije; jedan set deklaracija dovoljan je za celu datoteku 1. Ista ovakva organizacija bi bila potrebna i u slučaju da se u programu definicije sp i val pojavljuju posle njihove upotrebe. 4.5 ZAGLAVLJA Razmotrimo sada podelu programa za kalkulator na nekoliko osnovnih datoteka, što se inače i radi kada je svaka celina dovoljno velika da bi se izvela sama za sebe. Funkcija main bi bila u jednoj datoteci, recimo main.c; funkcije push, pop i njihove promenljive bi bile u drugoj datoteci, recimo stack.c; u trećoj datoteci, getop.c, bi bila funkcija getop; najzad, u

četvrtoj datoteci (getch.c) bi bile funkcije getch i ungetch. Funkcije getch i ungetch smo izdvojili od ostalih jer bi u jednom pravom programu došle iz posebno kompajlovane biblioteke. Postoji još jedna stvar oko koje se treba pobrinuti: to su definicije i deklaracije, zajedničke za više datoteka. Naš cilj je da ih nekako centralizujemo, što je moguće više, tako da se one pojave samo na jednom mestu u čitavom programu. Shodno tome, ove zajedničke deklaracije i definicije se smeštaju u tzv. datoteku zaglavlja, nazvanu calc.h (.h - 'header' je oznaka za datoteke zaglavlja). Ova datoteka, a time i njen sadržaj, će se uključivati u program po potrebi komandom #include o kojoj će biti reči u Odeljku 4.11. Konačna forma programa je sledeća: calc.h #define NUMBER '0' void push(double); double pop(void); int getop(char []); int getch(void); void ungetch(int); main.c getop.c stack.c #include <stdio.h> #include <stdio.h> #include <stdio.h> #include <math.h> #include <ctype.h> #include „calc.h“ #include „calc.h“ #include „calc.h“ #define MAXVAL 100 #define MAXOP 100 getop() { int sp = 0; main() { ... double val[MAXVAL];³ ... } void push(double) { } ... getch.c ³double pop(void) { ³ ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿³ ... ³ ³#include <stdio.h> ³³ } ³ ³#define BUFSIZE 100³ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ ³char buf[BUFSIZE]; ³ ³int bufp = 0; ³ ³int getch(void) { ³ ³ ... ³ ³ } ³ ³void ungetch(int) {³ ³ ... ³ ³ } ³ ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ Postoji nesklad između želje da svaka datoteka može da pristupi samo onoj informaciji potrebnoj za obavljanje posla, i činjenice da je teško opsluživati više datoteka zaglavlja. Za neku umerenu veličinu programa najbolje je imati jednu datoteku zaglavlja koja sadrži sve što je zajedničko za ma koje delove programa; to je odluka koju smo ovde doneli. Za znatno veći program bila bi potrebna bolja organizacija i više zaglavlja.

4.6 STATIšKE PROMENLJIVE Statičke promenljive su treća vrsta promenljivih, pored extern i automatskih promenljivih koje smo do sada upoznali. Statičke (static) promenljive mogu biti unutrašnje i spoljne. Unutrašnje static promenljive su lokalne u istom smislu u kom su to i automatske promenljive: vidljive su samo unutar funkcije u kojoj su definisane. Međutim, za razliku od automatskih promenljivih one ne nastaju i ne nestaju sa funkcijom, već su trajne. To znači da će pri ponovnom ulasku u neku funkciju unutrašnja static promenljiva imati onu vrednost koju je imala prilikom poslednjeg izlaska iz funkcije. Nizovi znakova koji se pojavljuju unutar funkcija (npr. argumenti funkcije printf), su upravo unutrašnji static nizovi. Spoljna static promenljiva je vidljiva i pristupačna od mesta na kome je definisana pa sve do kraja izvornog programa, a za druge datoteke nije. Spoljne static promenljive predstavljaju način da se sakriju imena promenljivih kao što su buf i bufp koje moraju biti spoljne (da bi im obe funkcije - getch i ungetch - mogle pristupati), a uz to ipak ne treba da su vidljive korisnicima getch i ungetch funkcija. Ako se dve funkcije i dve promenljive kompajliraju u istoj datoteci, kao static char buf[BUFSIZE]; /* bafer za ungetch */ static int bufp = 0; /* sledece slobodno mesto u baferu */ int getch(void) { ... } void ungetch(int c) { ... } onda nijedna druga funkcija neće imati pristup promenljivama buf i bufp: to takođe znači da imena ovih promenljivih neće biti u konfliktu sa eventualnim istim imenima u nekim odvojeno kompajliranim datotekama istog programa. Statička promenljiva, bilo spoljna bilo unutrašnja, je određena navođenjem reči static ispred uobičajene deklaracije. Statička promenljiva će biti spoljna ako je definisana izvan svih funkcija, odnosno biće unutrašnja ako je definisana unutar neke funkcije. Deklaracija static se najčešće koristi za promenljive, ali se može primeniti i na funkcije. Naravno, funkcije su same po sebi spoljni objekti; njihova imena su vidljiva za sve datoteke jednog programa. Pa ipak, deklarisanjem funkcije kao static, funkcija će postati nevidljiva izvan datoteke u kojoj je deklarisana. U C-u, termin 'static' ne označava samo trajnost, već i stepen nečega što bi se moglo nazvati 'privatnost'. Unutrašnji static objekti su vidljivi samo unutar jedne funkcije; spoljni static objekti (promenljive ili funkcije) su vidljivi samo unutar datoteke u kojoj se pojavljuju, i njihova imena se

neće preklopiti sa eventualnim istim imenima promenljivih ili funkcija u drugim datotekama jednog programa. Spoljne static promenljive ili funkcije predstavljaju način da se podaci, objekti i rutine koje njima manipulišu sakriju, tako da druge rutine i podaci ni slučajno ne mogu doći u konflikt sa njima. Na primer, funkcije getch i ungetch čine 'modul' za unošenje i vraćanje znakova sa ulaza; promenljive buf i bufp treba da su static tipa kako bi bile nepristupačne za ostale funkcije. Na isti način, funkcije push, pop i clear formiraju modul za manipulaciju stekom; val i sp bi takođe trebalo da su spoljne static promenljive. 4.7 REGISTARSKE PROMENLJIVE šetvrtu i poslednju klasu promenljivih čine registarske promenljive. Navođenjem reči register ispred uobičajene deklaracije naglašava se kompajleru da će promenljiva o kojoj je reč biti veoma često korišćena. Kada je to moguće, vrednost te promenljive će umesto u memoriji biti čuvana u nekom od registara procesora, što može rezultirati kraćim i bržim programima. Kompajler može i da zanemari ovaj savet. Deklaracija register tipa ima oblik register int x; register char c; i tako dalje; ako se tip promenljive ne navede, podrazumeva se int tip. Reč register može biti primenjena samo na automatske promenljive i na formalne parametre funkcije. U ovom drugom slučaju deklaracija ima oblik f(register unsigned m, register long n) { register int i; ... } U praksi, postoje neka ograničenja vezana za register promenljive koja su posledica hardverskih mogućnosti računara. Samo par promenljivih u svakoj funkciji može biti čuvano u registrima, i to samo određenih tipova. Veći broj registarskih deklaracija, međutim, nije štetno: reč register će biti ignorisana kod nedozvoljenih deklaracija ili u slučaju da više nema slobodnih registara za tu svrhu. Uz to, nije moguće pristupiti adresi registarske promenljive (o tome će biti više govora u Poglavlju 5), bez obzira na to da li je promenljiva zaista smeštena u registru. Ograničenja se razlikuju od mašine do mašine.

4.8 BLOK STRUKTURA C nije blokovski strukturiran jezik, u smislu da funkcije ne mogu biti definisane u okviru drugih funkcija. Sa druge strane, promenljive mogu biti definisane na blokovski strukturiran način. Deklaracije promenljivih (uključujući i njihovu inicijalizaciju) mogu se navesti posle leve vitičaste zagrade, koja uvodi bilo koji složeni iskaz, a ne samo onaj kojim počinje funkcija. Promenljive deklarisane na ovaj način potiskuju sve jednako imenovane promenljive u spoljašnjim blokovima i ostaju u upotrebi sve dok program ne naiđe na desnu vitičastu zagradu. Na primer, u if (n > 0) { int i; /* deklaracija nove promenljive i */ for (i = 0; i < n; i++) ... } opseg postojanja promenljive i je deo if konstrukcije koji se izvršava u slučaju da je uslov zadovoljen. Ova promenljiva i nije ni u kakvoj vezi sa eventualnom drugom promenljivom i u istom programu. Automatska promenljiva koja je deklarisana i inicijalizovana u bloku se inicijalizuje pri svakom ulazu u blok. Promenljiva static tipa se inicijalizuje samo pri prvom ulasku u blok. Automatske promenljive i formalni parametri takođe potiskuju spoljašnje promenljive i funkcije sa istim imenom. Deklaracijama int x; int y; f(double x) { double y; ... } će se unutar funkcije f svako pojavljivanje promenljive x odnositi na unutrašnju double promenljivu. Svako pojavljivanje promenljive x izvan funkcije f odnosiće se na spoljnu promenljivu int tipa. Navedeno važi i za promenljivu y: unutar funkcije f, y se odnosi na formalni parametar a ne na spoljnu promenljivu. Treba izbegavati takva imena promenljivih koja potiskuju imena u spoljašnjem delu programa, jer postoji velika mogućnost da dođe do zabune ili greške.

4.9 INICIJALIZACIJA Inicijalizacija je pominjana mnogo puta do sada, ali svaki put usputno, tek da bi se obradile druge teme. Ovaj odeljak objedinjuje neka pravila, jer smo tek sada uveli sve klase promenljivih. Ako se eksplicito ne inicijalizuju, spoljne i statičke promenljive će sigurno biti postavljene na nulu; automatske i registarske promenljive će u tom slučaju dobiti neke nedefinisane (nepredvidive) vrednosti. Jednostavne promenljive (nikako polja ili strukture) mogu se inicijalizovati onda kada se i deklarišu, tako što će se posle njihovog imena navesti znak jednakosti i neki konstantan izraz: int x = 1; char jednostruki_navodnik = '\''; long dan = 60L * 24L; /* minuti u jednom danu */ Inicijalizacija spoljnih i statičkih promenljivih se obavlja samo jedanput, obično za vreme kompajliranja. Inicijalizacija automatskih i registarskih promenljivih se vrši sa svakim ulaskom u funkciju ili blok u kojima se nalaze. Automatske i registarske promenljive se ne moraju inicijalizovati konstantnim izrazom; u suštini, to može biti bilo kakav važeći izraz koji može sadržati i neke prethodno definisane vrednosti ili čak pozive funkcija. Na primer, funkcija binsearch iz Poglavlja 3 mogla je biti napisana kao int binsearch(int x, int v[], int n) { int low = 0; int high = n - 1; int mid; ... } umesto što je pisana kao int low, high, mid; low = 0; high = n - 1; Ustvari, inicijalizacije automatskih promenljivih su samo skraćene varijante iskaza dodeljivanja. Koju formu izabrati je ponajviše pitanje afiniteta. Mi smo najčešće koristili eksplicitno dodeljivanje jer se inicjalizacija u deklaracijama teže uočava.

Polja se mogu inicijalizovati navođenjem liste inicijalizatora iza deklaracije. Pri tome, lista inicijalizatora mora biti navedena u vitičastim zagradama a sami inicijalizatori moraju biti međusobno odvojeni zarezom. Na primer, program za brojanje znakova iz Poglavlja 1, koji je počinjao sa #include <stdio.h> main() { int c, i, nwhite, nother; int ndigit[10]; nwhite = nother = 0; for (i = 0; i < 10; i++) ndigit[i] = 0; ... } može biti napisan kao #include <stdio.h> int nwhite = 0; int nother = 0; int ndigit[10] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; main() { int c, i; ... } Ovakve inicijalizacije su u suštini nepotrebne jer bi promenljive i onako bile postavljene na nulu, ali je to dobar način da se takva inicijalizacija naglasi. Ako je broj inicijalizatora manji od preciziranog, za spoljne i statičke promenljive uzeće se da su svi ostali jednaki nuli. Za automatske promenljive biće nedefinisani. Višak inicijalizatora smatra se greškom. Na žalost, ne postoji način da se zada ponavljanje nekog inicijalizatora, niti postoji način da se inicijalizuje neki element iz sredine polja a da se prethodno ne inicijalizuju svi njemu prethodni elementi. Ako polje sadrži znakove, može se primeniti specijalan slučaj inicijalizacije: umesto da se lista inicijalizatora odvojenih zarezom stavlja u zagrade, kao inicijalizator za čitavo polje može se upotrebiti niz. char pattern[] = „niz“; Navedena varijanta predstavlja skraćenje ekvivalentnog, dužeg oblika char pattern[] = { 'n' , 'i' , 'z' , '\0' }; Kada veličina polja bilo kog tipa nije navedena, kompajler će je izračunati

prebrojavanjem inicijalizatora. U ovom posebnom slučaju, veličina polja je četiri: tri znaka i završni \0. 4.10 REKURZIJA C funkcije se mou upotrebljavati rekurzivno; to znači da funkcija može, direktno ili indirektno, pozivati samu sebe. Uzmimo primer štampanja broja kao niza znakova. Kao što smo ranije pomenuli, cifre se generišu po obrnutom redosledu: cifre koje su niže po značaju su pristupačne pre onih koje su više po značaju, dok štamanje mora biti obavljeno upravo obrnutim redosledom. Postoje dva rešenja ovog problema. Jedan je da se cifre po generisanju odmah smeštaju u jedno polje, pa da se zatim štampaju obrnutim redosledom, kako smo to učinili u Poglavlju 3 pišući funkciju itoa. Prva verzija funkcije printd sledi ovu ideju: #include <stdio.h> /* printd: stampa n kao decimalan broj */ void printd(int n) { char s[10]; int i; if (n < 0) { putchar('-'); n = -n; } i = 0; do { s[i++] = n % 10 + '0'; /* uzmi sledeci znak */ } while ( (n /= 10) > 0 ); while (--i >= 0) putchar(s[i]); } Alternativu predstavlja rekurzivno rešenje, u kome funkcija printd poziva samu sebe da bi obavila posao sa vodećim ciframa, a zatim štampa preostalu cifru. #include <stdio.h> /* printd: stampa n kao decimalan broj (rekurzija) */ void printd(int n) { if (n < 0) { putchar('-'); n = -n; }

if (n / 10) printd(n /10); putchar(n % 10 + '0'); } Kada funkcija rekurzivno pozove sebe, sa svakim pozivom pojavljuje se novi skup svih automatskih promenljivih, nezavisan od prethodnog skupa. Na taj način u printd(123), prva funkcija printd dobija argument n = 123. Ona poziva drugu funkciju printd sa printd(12), i štampa 3 tek kada se izvrši povratak iz druge funkcije. Druga funkcija printd poziva treću funkciju printd sa printd(1), i štampa 2 tek kada se izvrši povratak iz treće funkcije. Treća funkcija printd ima kao argument samo jednu cifru (1); dakle, nema vodećih cifara pa tako ni daljih rekurzivnih poziva i preostaje samo da se ta cifra odštampa. Zatim se vrši povratak u drugu funkciju koja štampa preostalu cifru svog argumenta (a to je 2) i potom vrši povratak prvu funkciju printd. Prva funkcija printd štampa preostalu cifru svog argumenta (3), i čitav program se tu okončava. Sledeći dobar primer rekurzije je algoritam za brzo sortiranje, koji je C.A. Hoare otkrio 1962. godine. Za dato polje se izabere jedan element, a ostali elementi se razlože na dva podskupa: na one koji su manji od izabranog elementa i na one koji su veći ili su mu jednaki. Isti postupak se zatim rekurzivno primeni na ova dva podskupa. Kad u podskupu ima manje od dva elementa, njemu nije ni potrebno sortiranje; to zaustavlja rekurziju. Naša verzija programa za sortiranje nije najbrža, ali je jedna od najjednostavnijih. Za razlaganje ćemo koristiti srednji element svakog skupa. /* qsort: sort v[left] ... v[right] u rastućem nizu */ void qsort(int v[], int left, int right) { int i, last; void swap(int v[], int i, int j); if (left >= right) return; swap(v, left, (left + right) / 2); last = left; for (i = left + 1; i <= right; i++) if (v[i] < v[left]) swap(v, ++last, i); swap(v, left, last); qsort(v, left, last - 1); qsort(v, last + 1, right); } Premestili smo operaciju zamene elemenata u posebnu funkciju swap, jer se ona tri puta pojavljuje u funkciji qsort. /* swap: izmena v[i] i v[j] */ void swap(int v[], int i, int j)

{ int temp; temp = v[i]; v[i] = v[j]; v[j] = temp; } Standardna biblioteka sadrži verziju funkcije qsort koja može da sortira objekte bilo kog tipa. Rekurzija u opštem slučaju ne obezbeđuje čuvanje vrednosti u promenljivama, budući da se negde mora održavati stek sa vrednostima koje se obrađuju. Rekurzija nije čak ni brže rešenje nekog problema. Međutim, program pisan na rekurzivni način je celovitiji, i često lakši za pisanje i razumevanje. Rekurzija je posebno pogodna za rekurzivno definisane strukture podataka kao što su stabla; lep primer za to biće prikazan u Odeljku 6.5. þ Vežba 4 - 7 Prilagodite ideje iz funkcije printd kako biste napisali rekurzivnu varijantu funkcije itoa, tj. konvertujte ceo broj u string rekurzivnom rutinom. þ Vežba 4 - 8 Napišite rekurzivnu verziju funkcije reverse(s) koja obrće string s naopako. 4.11 C PRETPROCESOR C jezik omogućava izvesne olakšice ako se koristi pretprocesor koji pre samog kompajlera dolazi u kontakt sa programom i u stanju je da izvrši neke transformacije na njemu. Njegova najčešća primena je definisanje simboličkih konstanti koje C sam po sebi ne prepoznaje, i uključivanje drugih datoteka u proces kompajliranja. Ostale osobine opisane u ovom odeljku odnose se na uslovno kompajliranje i makroe sa argumentima. 4.11.1 Uključivanje datoteke Da bi olakšao rad sa većim brojem deklaracija i #define izraza (između ostalog), C omogućava uključivanje datoteka u proces kompajliranja. Svaka linija programa koja je oblika #include „ime datoteke“ ili #include <ime datoteke>

i sadržaj datoteke ime datoteke biće sastavni deo programa. šesto se ove direktive stavljaju na početak svake datoteke programa koji se kompajlira, kako bi zajedničke #define direktive i extern deklaracije promenljivih bile vidljive u svakoj datoteci programa. Unutar jedne datoteke uključene #include direktivom može se nalaziti čak i čitav niz novih #include direktiva, ali je pretprocesor sposoban da obradi sve rekurzije kako treba. Direktiva #include je najbolji način da se u nekom velikom programu sve deklaracije skupe na jednom mestu. To je garancija da će sve datoteke koje čine program biti snabdevene istovetnim definicijama i deklaracijama promenljivih, i da će na taj način biti sprečeno eventualno nastajanje veoma teško uočljive greške. Naravno, ako datoteka koja se uvodi #include direktivom pretrpi promene, onda i sve datoteke koje zavise od njih moraju biti ponovo kompajlirane. 4.11.2 Zamena makroima Definicija oblika #define ime tekst zamene je u stvari direktiva pretprocesoru (sve pretprocesorske direktive počinju znakom #) i nakon obrade kompajler je neće videti u programu. Pretprocesor će na osnovu zadate definicije izvršiti zamenu teksta posle koje će na svim mestima u programu tekst ime biti zamenjen tekstom tekst zamene. Pri tome, ime mora biti u skladu sa pravilima C-a, dok tekst zamene može biti proizvoljan tekst. U normalnim okolnostima kraj reda se smatra i krajem definicije, pa obrnuta kosa crta (\) služi kao oznaka da se tekst definicije nastavlja u sledećem redu. Ovako se pišu duže definicije. Opseg imena definisanog #define direktivom se proteže od mesta na kome je uvedeno, pa do kraja datoteke koja se kompajlira. Definicija može koristiti prethodne definicije. Zamene se ne vrše na tekstovima navedenim pod dvostrukim navodnicima. Na primer, ako je YES definisano ime, neće se izvršiti zamena u printf(„YES“) ili u YESMAN. Pošto pretprocesor ne radi ništa što bi imalo veze sa samim C-om ili analizom sintakse, to postoji veoma malo ograničenja vezanih za sintaksu. Otud svako ime može biti definisano pomoću bilo kog teksta zamene. Na primer, ljubitelji Alogol-a mogu uvesti #define then #define begin { #define end ;} i zatim pisati if (i > 0) then begin a = 1;

b = 2 end Takođe je moguće definisati makroe koji imaju parametre, i tada će zamena teksta zavisiti od načina na koji je makro pozvan. Na primer, definišite makro max na ovaj način: #define max(A, B) ( (A) > (B) ? (A) : (B) ) x = max(p + q, r + s); Kada pretprocesor obavi posao, kompajler će zateći x = ( (p + q) > (r + s) ? (p + q) : (r + s) ); Treba uočiti da nazivi parametara A i B nemaju nikakve veze sa promenljivama ili bilo kojim delom C programa. Njihova namena je da unutar definicije makroa posluže kao 'markeri' za mesta na koja treba ubaciti stvarne parametre. Sve dok se sa argumentima dosledno postupa, ovaj makro će funkcionisati sa bilo kojim tipom podataka; ne postoji potreba za različitim oblicima max-a za različite tipove podataka, kao što bi to bio slučaj da su korišćene funkcije. Ako dobro pogledate gornju max definiciju, uočićete neke zamke ovakvog pisanja. Izrazi se izračunavaju dvaput: ako se u tim izrazima nalaze operatori uvećavanja ili umanjivanja ili poziv funkcije, nepotrebno će se ponoviti uvećavanje/umanjivanje ili poziv funkcije. Na primer, max(i++, j++) /* pogresno */ će dvaput uvećati veću vrednost. Mora se, takođe, voditi računa o zagradama da bi se očuvao redosled izračunavanja. Kada bi se makro #define square(x) (x) * (x) napisao kao #define square(x) x * x i pozvao sa square(z + 1), nastala bi greška. Postoje i neki čisto leksički problemi vezani za upotrebu makroa: između imena makroa i leve zagrade koja otvara listu promenljivih ne sme biti razmaka. Pa ipak, makroi su veoma korisni. Jedan praktičan primer za to je standardna biblioteka <stdio> funkcija ulaza i izlaza. U njoj su funkcije putchar i getchar definisane kao makroi da bi se izbeglo dodatno povećanje vremena pozivanja funkcije po prosleđenom znaku. Da bi se ime funkcije obezbedilo od eventualnog mešanja sa imenom makroa, koristi se direktiva #undef koja nalaže pretprocesoru da 'zaboravi'

da je to ime ikad definisano: #undef getchar int getchar(void) { ... } Ako u tekstu zamene imenu nekog parametra prethodi znak #, taj će parametar posle zamene u nekom delu teksta biti naveden pod navodnicima. Na primer, definisanjem makroa #define dprint(expr) printf( #expr „ = %g \n“, expr) će poziv dprint(x / y); biti zamenjen sa printf( „x / y“ „ = %g \n“, x / y); i posle povezivanja nizova će imati oblik printf(„x / y = %g \n“, x / y); Pretprocesorski operator ## se koristi za povezivanje parametara na sledeći način: #define paste(front, back) front ## back Pozivom makroa sa paste(name, 1) kreiraće se simbol name 1. 4.11.3 Uslovno uključivanje datoteka Pretprocesor je u stanju da vrši elementarne logičke operacije i da na osnovu njih donosi odluke o daljem toku procesa zamene, i odluke o uključivanju pojedinih delova teksta. Direktivom #if se ispituje tačnost konstantnog celobrojnog izraza; ako je tačan, naredne linije, sve do direktive #endif, #elif (pretprocesorskog #else if) ili #else, biće uključene u proces kompajliranja. Termin defined u #if konstrukciji je takođe deo pretprocesorskog jezika i izraz #if defined(ime) se smatra tačnim ako je na bilo koji način (upotrebom #define direktive) definisana konstanta ime, pri čemu je njena vrednost nebitna. Na primer, da bismo bili sigurni da je sadržaj datoteke hdr.h uključen u program samo jednom, možemo pisati:

#if !defined(HDR) #define HDR /* sadrzaj datoteke HDR se prosledjuje */ #endif Prvo uključivanje datoteke hdr.h definiše i ime HDR; kod kasnijih pokušaja uključivanja, pretprocesor će pronaći definisano ime i spustiće se niže na #endif. Na ovaj način je moguće izbeći velik broj uključivanja datoteka. Ako se ovakav stil dosledno koristi, onda svako pojedino zaglavlje može da uključi neka druga zaglavlja od kojih zavisi, a da pri tome korisnik ne mora da razmišlja o njihovoj međusobnoj zavisnosti. Kombinacija #if defined() se može zameniti i kraćim oblikom: #ifdef ime ili inverznim oblikom #ifndef ime Gornji primer se stoga može i ovako napisati: #ifndef HDR #define HDR /* sadrzaj HDR se prosledjuje */ #endif U sledećem primeru pretprocesor testira ime SYSTEM da bi odlučio koju će verziju zaglavlja uključiti: #if SYSTEM == SYSV #define HDR „sysv.h“ #elif SYSTEM == BSD #define HDR „bsd.h“ #elif SYSTEM == MSDOS #define HDR „msdos.h“ #else #define HDR „default.h“ #endif #include HDR Upotreba ovakvih logičkih izraza pruža gotovo neograničene mogućnosti za kreiranje izuzetno preglednih programa podložnih lakoj izmeni. P o g l a v l j e 5 : POINTERI I POLJA

Pointer (pokazivač) je promenljiva koja čuva adresu neke druge promenljive. Pointeri se veoma često koriste u C-u, delom zbog toga što su oni ponekad jedini način da se obave neka izračunavanja, a delom i zbog toga što obično vode celovitijim i efikasnijim programima od onih pisanih na neki drugi način. Pokazivači i polja su tesno povezani; ovo poglavlje ispituje njihovu međusobnu vezu i pokazuje kako se ona koristi. Pointeri su, kao i goto naredba, sjajan način da se pišu programi koje je nemoguće razumeti. Ovo je svakako istina ako se nepažljivo koriste, i zato je veoma lako stvoriti pointere koji pokazuju na neko nepredvidivo mesto. Međutim, uz opreznost i disciplinu, pointeri mogu postati sredstvo za postizanje jasnoće i preglednosti programa. Upravo to je svojstvo koje smo želeli ovde da ilustrujemo. 5.1 POINTERI I ADRESE Kako pointer čuva adresu nekog objekta, to je moguće pristupiti objektu 'indirektno' preko pointera. Pretpostavimo da je x promenljiva, recimo int tipa, i neka je px pointer, stvoren na neki za sada nepoznat način. Unarni operator & daje adresu nekog objekta, pa otud izraz px = &x; smešta adresu promenljive x u promenljivu px. Za promenljivu px se sada kaže da 'pokazuje' na promenljivu x. Operator & se može primeniti samo na promenljive i elemente polja (dakle objekte u memoriji): konstrukcije kakve su &(x + 1) ili &3 su nedozvoljene. Takođe nije dozvoljena primena ovog operatora na register promenljive. Unarni operator * smatra svoj operand za adresu objekta kome želi da pristupi, i uzima sadržaj te adrese. Na taj način će, ako je y promenljiva int tipa, izrazom y = *px; promenljivoj y biti dodeljen sadržaj objekta na koji pokazuje px. Tako ćemo izrazima px = &x; y = *px; smestiti u promenljivu y istu vrednost koju bi smestili izrazom y = x; Takođe, neophodno je deklarisati sve pomenute promenljive koje učestvuju: int x, y;

int *px; Deklaracije promenljivih x i y su nam poznate. Deklaracija pointera px je novina. int *px; je zamišljen kao mnemonik: on kaže da je kombinacija *px tipa int, tj. ako se promenljiva px pojavljuje u obliku *px, onda se tretira istovetno kao i promenljiva int tipa. Kao posledica ove činjenice, sintaksa deklaracije pointera istovetna je kao i sintaksa izraza u kojima se pojavljuju promenljive. Ovakvo razmatranje je korisno u slučajevima složenijih deklaracija. Na primer, iskaz double atof(char *), *dp; određuje da će u izrazima funkcija atof() vraćati tip double i da će pointer *dp takođe biti tog tipa. Iskazom je takođe određeno da je argument funkcije atof jedan pokazivač na objekte char tipa. Treba primetiti da je deklaracijom pointer ograničen na pokazivanje samo određenog tipa objekata. Pointeri se mogu pojavljivati u izrazima. Na primer, ako pointer px pokazuje na promenljivu x tipa int, onda se *px može pojaviti u bilo kom kontekstu u kom može i promenljiva x. Izraz y = *px + 1; postavlja promenljivu y na vrednost x + 1 ; printf(„%d\n“, *px); štampa trenutnu vrednost promenljive x ; najzad, d = sqrt((double) *px); smešta u promenljivu d vrednost kvadratnog korena od x, pri čemu se vrednost promenljive x pre prosleđivanja funkciji sqrt konvertuje u double tip (videti Poglavlje 2). U izrazima kakav je y = *px + 1; unarni operatori * i & imaju viši prioritet od aritmetičkih, pa će tako ovim izrazom sadržaju objekta na koji *px pokazuje biti dodana jedinica, i rezultat će biti smešten u promenljivu y. Uskoro ćemo se vratiti na ono što bi

y = *(px + 1); moglo značiti. Pointeri se takođe mogu pojaviti i na levoj strani izraza dodeljivanja. Ako, recimo, px pokazuje na x, onda izraz *px = 0; postavlja promenljivu x na nulu, a izraz *px += 1; je uvećava za jedan, baš kao što to radi i (*px)++; U poslednjem primeru zagrade su neophodne; bez njih, izrazom bi se uvećao pointer px, umesto sadržaja objekta na koji pokazuje: to stoga jer se unarni operatori kakvi su * i ++ izračunavaju sa desna na levo. Konačno, pošto su pointeri promenljive, sa njima se može manipulisati na isti način kao i sa promenljivama. Ako je py još jedan pointer na objekte int tipa, onda će izraz py = px; pridružuje sadržaj objekta na koji px pokazuje pointeru py, čineći na taj način da i py pokazuje na isti objekat. 5.2 POINTERI I ARGUMENTI FUNKCIJA Pošto se u C-u funkcijama ne prosleđuju sami argumenti već njihove vrednosti, to pozvana funkcija nije u stanju da direktno pristupi argumentu i izmeni mu vrednost. Žta učiniti u slučaju da je zaista neophodno izmeniti vrednost nekom argumentu? Na primer, rutina za sortiranje može izmeniti redosled dva elementa pozivanjem neke funkcije swap. Nije dovoljno napisati swap(a, b); gde je swap funkcija definisana kao void swap(int x, int y) /* pogresno */ { int temp; temp = x; x = y;

y = temp; } Zbog toga što su joj prosleđene vrednosti promenljivih, a ne same promenljive, funkcija swap ne može uticati na promenljive a i b koje su u rutini iz koje je usledio poziv. Srećom, postoji način da se ostvari željeni efekat. Program koji upućuje poziv proslediće pointere, pokazivače na promenljive čiji sadržaj želimo da menjamo: swap(&a, &b); Pošto operator & daje adresu promenljive, to je onda &a pointer na promenljivu a. Žto se tiče funkcije swap, njeni argumenti se deklarišu kao pointeri, a stvarnim operandima će onda biti pristupljeno kroz njih. void swap(int *px, int *py) { int temp; temp = x; *px = *py; *py = temp; } Ovo se može predstaviti i grafički: funkcija iz ÚÄÄÄÄÄÄÄÄÄ¿ koje je ³ ÚÄÄ¿ ³ upućen poziv³ a: ³ ÅÄÅÄÄÄÄÄ¿ ³ ÀÄÄÙ ³ ³ ³ ÚÄÄ¿ ³ ³ ³ b: ³ ÅÄÅÄÄÄÄÄÅÄÄÄÄÄ¿ ³ ÀÄÄÙ ³ ³ ³ ÀÄÄÄÄÄÄÄÄÄÙ ³ ³ ³ ³ swap ÚÄÄÄÄÄÄÄÄÄ¿ ³ ³ ³ ÚÄÄ¿ ³ ³ ³ ³ px:³ ÅÄÅÄÄÄÙ ³ ³ ÀÄÄÙ ³ ³ ³ ÚÄÄ¿ ³ ³ ³ py:³ ÅÄÅÄÄÄÄÄÄÄÄÄÙ ³ ÀÄÄÙ ³ ÀÄÄÄÄÄÄÄÄÄÙ Jedna od čestih upotreba argumenata koji su pointeri je u funkcijama koje u program moraju vraćati više od jedne vrednosti (možete smatrati da funkcija swap vraća dve vrednosti: nove vrednosti promenljivih a i b). Kao primer, razmotrite funkciju getint koja sa ulaza vrši konverziju niza

karaktera proizvoljnog formata pretvarajući ih u celobrojne vrednosti, jedan ceo broj po pozivu. Funkcija getint mora u program vratiti neku celobrojnu vrednost ako je bilo znakova na ulazu, odnosno vratiti signal za kraj ulaza ako više nema znakova. Ove vrednosti moraju biti vraćene u program odvojenim putevima jer je i signal za kraj ulaza neki ceo broj, pa bi mogao biti pogrešno shvaćen kao neka konvertovana vrednost sa ulaza. Jedno rešenje, zasnovano na principima scanf funkcije ulaza, je da naša funkcija getint pomoću iskaza return vrati u program EOF znak za kraj ulaza, a preko svojeg argumenta vraća konvertovane brojeve. Naravno, da bi se argument mogao menjati, mora biti izveden kao pointer. Ovakva organizacija odvaja broj koji predstavlja kraj ulaza od brojeva dobijenih konvertovanjem. Sledeća petlja popunjava polje array celim brojevima sa svakim pozivom funkcije getint : int n, v, array[SIZE], getint(int *); for (n = 0; n < SIZE && getint(&v) != EOF; n++) array[n] = v; Svaki poziv funkciji getint prosleđuje joj adresu promenljive v i u nju se privremeno smešta sledeći ceo broj sa ulaza. Potom se iz promenljive v taj broj prebacuje u sledeći element polja array. Primetite da je od ključnog značaja pisanje argumenta funkcije getint u formi &v umesto v, jer se samo na taj način omogućava funkciji da preko pointera pristupa promenljivoj v i da je menja. Ako bi kojim slučajem funkciji getint prosledili kao argument promenljivu v umesto njene adrese, verovatno bi došlo do smeštanja podataka na pogrešnu adresu, pošto funkcija getint smatra da joj je poslat pointer. Gornja petlja je mogla biti napisana i ovako: int n, array[SIZE], getint(int *); for (n = 0; n < SIZE && getint(&array[n]) != EOF; n++) ; Naša verzija funkcije getint u program vraća EOF kao kraj datoteke, nulu ako sledeći znak na ulazu nije broj, i pozitivnu vrednost ako ulaz sadrži valjan broj. Ona predstavlja modifikaciju funkcije atoi koju smo napisali ranije: #include <ctype.h> int getch(void); void ungetch(int); /* getint: uzima sledeći ceo broj sa ulaza */ int getint(int *pn) { int c, sign; while ( isspace( c = getch()) ) /* preskoci blanko znak */ ; if (!isdigit(c) && c != EOF && c != '+' && c != '-') { ungetch(c); /* nije broj */

return 0; } sign = (c == '-') ? -1 : 1; if (c == '+' || c == '-') c = getch(); for (*pn = 0; isdigit(c); c = getch() ) *pn = 10 * *pn + (c - '0'); *pn *= sign; if (c != EOF) ungetch(c); return c; } Kao što se može videti, funkcija getint tretira plus i minus znak kao nulu ako iza njih ne sledi neki broj. Kroz funkciju getint, pointer *pn je korišćen kao najobičnija promenljiva int tipa. Takođe, upotrebljene su funkcije getch i ungetch (opisane u Poglavlju 4), kako bi onaj prekobrojno učitani znak mogao biti vraćen nazad na ulaz. þ Vežba 5 - 1 Napišite funkciju getfloat, varijantu funkcije getint koja radi sa realnim brojevima. Koji tip vrednosti funkcija getfloat vraća u program iskazom return? 5.3 POINTERI I POLJA U C-u postoji čvrsta veza između pokazivača i polja, dovoljno čvrsta da pointere i polja zaista treba proučavati uporedo. Bilo koja operacija koja se može izvesti indeksiranjem (numerisanjem) elemenata polja, može se izvesti i pointerima. Verzija sa pointerima će u principu biti brža, ali za neupućene teže razumljiva. Deklaracija int a[10]; definiše polje od deset elemenata, tj. blok od deset uzastopnih objekata nazvanih a[0], a[1], ..., a[9]. ÚÄÄÄÂÄÄÄÂÄÄÄÂÄÄÄÂÄÄÄÂÄÄÄÂÄÄÄÂÄÄÄÂÄÄÄÂÄÄÄ¿ a: ³ ³ ³ ³ ³ ³ ³ ³ ³ ³ ³ ÀÄÄÄÁÄÄÄÁÄÄÄÁÄÄÄÁÄÄÄÁÄÄÄÁÄÄÄÁÄÄÄÁÄÄÄÁÄÄÄÙ a[0] a[1] Notacija a[i] znači 'i-ti element polja a '. Ako je pa pointer na objekte int tipa, deklarisan kao

int *pa; onda će izrazom dodeljivanja pa = a[0]; pointer pa pokazivati na nulti element polja a; to znači da će pa čuvati adresu nultog elementa polja i da je na taj način preko pointera moguće pristupiti tom elementu i eventualno izmeniti njegov sadržaj. Sada će izrazom x = *pa; u promenljivu x biti smešten sadržaj a[0] elementa polja. Ako pointer pa pokazuje na određeni element polja a, onda će po definiciji pa + 1 pokazivati na sledeći element polja. Uopšte uzevši, pa - i će pokazivati na i-ti element pre onog na koji pokazuje pa, dok će pa + i pokazivati na i-ti element posle onog na koji pokazuje pa. Odatle, ako pointer pa pokazuje na element a[0], onda se *(pa + 1) odnosi na sadržaj elementa a[1]. Dakle, pa + i predstavlja adresu i-tog elementa polja a, dok *(pa + i) predstavlja sadržaj elementa a[i] polja a. ÚÄÄÄÄ¿ pa + 1: ÄÄ¿ pa + 2: Ä¿ pa: ³ ÄÅÄÄÄÄ¿ ³ ³ ÀÄÄÄÄÙ ³ ³ ³ ÚÄÄÄÅÄÄÄÄÂÄÄÄÄÅÄÄÄÄÄÂÄÄÄÄÅÄÄÄÄÂÄÄÄÄÄ ÄÂÄÄÄÄÄÄÄÄÄ¿ a: ³ ³ ³ ³ . . . ³ ³ ÀÄÄÄÄÄÄÄÄÁÄÄÄÄÄÄÄÄÄÄÁÄÄÄÄÄÄÄÄÄÁÄÄÄ ÄÄÁÄÄÄÄÄÄÄÄÄÙ a[0] a[1] a[2] a[9] Ove primedbe važe bez obzira na tip elemenata polja a. Smisao 'dodavanja jedan pointeru' i, šire gledano, cele pointerske aritmetike, je u tome da se uvećanje vrši za jedan element, ma koliko on bajtova zauzimao. Na taj način će u izrazu pa + i nova adresa na koju pa treba da pokazuje biti dobijena kao pa + (i * veličina elementa u bajtovima) Povezanost između indeksiranja elemenata polja i pointera je očigledno veoma velika. Žta više, kompajler će, kada u tekstu na njega naiđe, automatski konvertovati ime polja u pointer na početni element polja. Posledica ovoga je da je ime polja u suštini pokazivač na početni element polja. Ovo ima prilično mnogo korisnih primena. Na primer, pošto je ime a polja sinonim za početni element polja, izraz pa = a[0];

može biti napisan i kao pa = a; Još interesantnija, bar na prvi pogled, je činjenica da se a[i] može pisati i kao *(a + i). Prilikom manipulisanja elementom a[i], C će ga smesta konvertovati u *(a + i); oba oblika su potpuno ekvivalentna. Primenom operatora & na oba ova oblika sledi da su i &a[i] i a + i takođe identični: a + i je adresa i-tog elementa polja. Sa druge strane, ako je pa pointer, izrazi ga mogu koristiti ako mu se dodeli indeks: pa[i] je identično kao *(pa + i). Ukratko, bilo koje polje i indeks mogu biti napisani kao pointer i ofset, i obrnuto, čak i unutar jednog iskaza. Postoji jedna razlika između imena polja i pointera koju treba uvek imati na umu. Pointer je promenljiva, pa su pa = a i pa++ operacije koje imaju smisla. Međutim, ime polja je konstanta, a ne promenljiva: konstrukcije tipa a = pa ili p = &a nisu dozvoljene. Kada se ime polja prosleđuje funkciji, ono što se zaista prosleđuje je adresa početnog elementa polja. Unutar pozvane funkcije, ovaj argument se tretira kao lokalna promenljiva, baš kao i bilo koja druga. Otud je ime polja zaista pointer, tj. promenljiva u kojoj se čuva neka adresa. Ovu činjenicu možemo iskoristiti da napišemo novu verziju funkcije strlen, koja izračunava dužinu stringa. /* strlen : vraca duzinu stringa */ int strlen(char *s) { int n; for (n = 0; *s != '\0'; s++) n++; return n; } Uvećavanje pointera s je dozvoljeno, s obzirom na to da je on promenljiva. Pri tome, s++ neće uticati na string u funkciji iz koje je upućen poziv, već će samo uvećati kopiju adrese. Kao formalni parametri u definiciji funkcije, char s[]; i char *s; su u potpunosti ekvivalentni; koji će od njih biti napisan, u najvećoj meri zavisi od toga kako će izrazi biti pisani unutar funkcije. Bolje je koristiti ovaj drugi, jer on jasnije pokazuje da je parametar jedan pointer. Kada se

funkciji prosleđuje ime polja, funkcija po svom nahođenju može smatrati da joj je prosleđeno ili polje ili pointer, i u skladu sa tim će preduzeti dalju manipulaciju njime. Funkcija čak može koristiti obe notacije ako je to prikladno i jasno. Moguće je funkciji proslediti samo deo nekog polja, tako što će joj biti prosleđen pointer na početak tog dela polja. Na primer, ako je a neko polje, onda će i f(&a[2]) i f(a + 2) proslediti funkciji f adresu elementa a[2], jer su i &a[2] i a + 2 pointerski izrazi koji se odnose na treći element polja a. Unutar funkcije f, deklaracija argumenta može da glasi f(int arr[]) { ... } ili f(int *arr) { ... } tako da, što se tiče funkcije f, činjenica da se argument zapravo odnosi samo na deo polja nema značaja. Ako smo sigurni da ti elementi postoje, moguće je da u polju budu indeksirani unazad. Tako su p[-1], p[-2] itd. sintaksno dozvoljeni i odnose se na elemente koji upravo prethode elementu p[0]. Naravno, nije dozvoljeno da se odnose na objekte koji nisu u granicama polja. 5.4 ADRESNA ARITMETIKA Ako je p pointer, onda p++ uvećava p tako da pokazuje na sledeći element ma koje vrste on bio, dok p += i uvećava p tako da pokazuje na i-ti element posle onog na koji trenutno pokazuje. Ovakve i slične konstrukcije su najjednostavniji i najčešći oblici pointerske ili adresne aritmetike. C je dosledan u svom pristupu problemu adresne aritmetike; sadejstvo pointera, polja i adresne aritmetike je jedna od glavnih snaga jezika. Ilustrujmo to pisanjem najosnovnijeg oblika alokatora memorije (alokacija = odvajanje, rezervisanje), koji je uprkos svojoj jednostavnosti ipak koristan. Alokator se sastoji iz dve rutine: prva je alloc(n) koja u program vraća pointer na n uzastopnih elemenata (svaki ima kapacitet da se u njega smesti

jedan znak), i koju može pozvati neka funkcija kojoj je potrebno da niz znakova smesti u memoriju. Druga rutina je afree(p), koja obavlja obrnut proces: ona oslobađa memoriju rezervisanu putem rutine alloc, i ta se memorija može kasnije eventualno ponovo upotrebiti za iste svrhe. Rutine su rudimentarne jer se pozivi funkciji afree moraju ostvarivati obrnutim redosledom od onog kojim su upućivani funkciji alloc. To je posledica činjenice da je memorija kojom manipulišu obe rutine izvedena u obliku steka, a za takve memorijske strukture je karakteristično da se poslednji podatak stavljen na njih mora prvi uzeti. Standardna C biblioteka obezbeđuje slične funkcije koje nemaju ovakvih ograničenja. Svejedno, mnogim programima je dovoljna i jednostavna verzija alloc funkcije kako bi u trenutku koji se ne može predvideti iskoristili manje delove memorije čija se veličina takođe ne može predvideti. Najjednostavnije je alokator izvesti tako da funkcija alloc rukuje sa delovima nekog velikog polja koje ćemo nazvati allocbuf. Ovo polje je pristupačno samo funkcijama alloc i afree. Pošto ove rutine operišu sa pointerima, a ne sa indeksima polja, i uz to nijednoj drugoj rutini nije potrebno da zna za ovo polje, to će polje biti deklarisano kao static i to izvan obe funkcije - dakle, kao spoljna promenljiva. Na taj način će ovo polje biti vidljivo samo unutar datoteke u kojoj su alloc i afree. U praksi, polje čak ni ne mora imati ime; memorijski prostor se može dobiti i pozivom funkcije malloc iz standardne biblioteke, ili pitanjem operativnom sistemu koji pointer pokazuje na neki neimenovani, slobodni blok memorije. Druga neophodna informacija je koliko je prostora u polju allocbuf već zauzeto. Mi smo uveli pointer na sledeći slobodan element, i nazvali ga allocp. Kada se funkciji alloc uputi zahtev za prostorom veličine n elemenata, ona proverava da li je u polju allocbuf ostalo dovoljno prostora za potvrdan odgovor. Ako jeste, funkcija alloc će u program vratiti trenutnu vrednost pokazivača allocp (tj. adresu početka slobodnog bloka), a potom će uvećati pointer za vrednost n da bi pokazivao na sledeće slobodno područje. Ako nema mesta, u program će biti vraćen NULL. Pozivom afree(p) će jednostavno pointer allocp biti postavljen na položaj p, ako je p vrednost unutar polja allocbuf. úpre poziva funkcije alloc : allocp: ÄÄÄ¿ ÚÄÂÄÄÂÄÄÄÄÄÄÄÄÂÄÄÄÄÄÂÅÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ allocbuf: ³ ³ ³ ³ ³ ³ ÀÄÁÄÄÁÄÄÄÄÄÄÄÄÁÄÄÄÄÄÁÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ ³úúú rezervisano úúú³úúúúúú slobodan prostor úúúúúú³ únakon poziva funkcije alloc : allocp: ÄÄÄÄÄÄÄ¿

ÚÄÂÄÄÂÄÄÄÄÄÄÄÄÂÄÄÄÄÄÂÄÄÄÄÄÄÄÄÄÄÂÅÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ allocbuf: ³ ³ ³ ³ ³úúúúnúúúúú³ ³ ÀÄÁÄÄÁÄÄÄÄÄÄÄÄÁÄÄÄÄÄÁÄÄÄÄÄÄÄÄÄÄÁÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ ³úúúúúúúú rezervisano úúúúúúúúú³úúúú slobodno úúúúú³ #define NULL 0 /* vrednost pointera u slučaju greške */ #define ALLOCSIZE 10000 /* velicina pristupacnog prostora */ static char allocbuf[ALLOCSIZE]; /* polje za alloc */ static char *allocp = allocbuf; /* sledeca slobodna pozicija */ char *alloc(int n) /* vraca pointer na blok od n znakova */ { if (allocp + n <= allocbuf + ALLOCSIZE) { /* odgovara */ allocp += n; return (allocp - n); /* stara pozicija p */ } else /* nema dovoljno mesta */ return (NULL); } void afree(char *p) /* slobodna memorija na koju pokazuje p */ { if (p >= allocbuf && p < allocbuf + ALLOCSIZE) allocp = p; } Sledi nekoliko objašnjenja. U opštem slučaju pointer može biti inicijalizovan baš kao i svaka druga promenljiva, mada jedine vrednosti koje mu ima smisla dodeliti jesu NULL (o kojoj će biti reči uskoro), ili neki izraz u kome figurišu adrese. Pri tome, naravno, na adresama moraju biti ranije definisani podaci i to onog tipa za koji je pointer i deklarisan. Deklaracija static char *allocp = allocbuf; definiše pointer allocp za objekte tipa char i postavlja ga da pokazuje na početak polja allocbuf, što je početna pozicija slobodne memorije u trenutku kad se program startuje. Ova deklaracija je mogla biti napisana i ovako: static char *allocp = &allocbuf[0]; jer, kako je već rečeno, ime polja isto što i adresa početnog (nultog) elementa; u programima koristite ono što vam se čini prirodnijim. Test

if (allocp + n <= allocbuf + ALLOCSIZE) proverava ima li dovoljno prostora da bi se potvrdno odgovorilo na zahtev za prostorom veličine n elemenata. Ako ima, pointer allocp će u ekstremnom slučaju pokazivati na prvi element iza kraja allocbuf polja. Ako zahtev može biti zadovoljen, funkcija alloc u program vraća pointer usmeren na početak slobodnog bloka znakova (obratite pažnju na deklaraciju same funkcije !). U slučaju da nema dovoljno prostora, ova funkcija mora u program vratiti neki signal o tome. C garantuje da nula nikad ne može biti važeća adresa nekog podatka, tako da se vraćena vrednost nule može iskoristiti da se signalizira bilo kakva nepravilnost, u našem slučaju nedostatak prostora. Pisali smo NULL umesto same nule kako bismo jasnije istakli da je to specijalan slučaj vrednosti pointera. Konstanta NULL je definisana u datoteci <stdio.h>. U opštem slučaju, pointeru se ne mogu dodeljivati celobrojne vrednosti; nula je poseban slučaj. Testovi kao što su if (allocp + n <= allocbuf + ALLOCSIZE) i if (p >= allocbuf && p < allocbuf + ALLOCSIZE) pokazuju nekoliko važnih karakteristika pointerske aritmetike. Prvo, pointeri se pod određenim okolnostima mogu međusobno upoređivati. Ako pointeri p i q pokazuju na elemente istog polja, onda relacije <, >=, i druge funkcionišu ispravno. Na primer, iskaz p < q biće istinit ako pointer p pokazuje na neki element polja koji se u polju nalazi bilo gde ispred elementa na koji pokazuje pointer q. Relacije tipa == i != takođe funkcionišu. Bilo koji pointer može se porediti u smislu (ne)jednakosti sa konstantom NULL. Međutim, uopšte se ne može predvideti šta će se dogoditi ako dođe do poređenja između pointera koji ne pokazuju na elemente istog polja, ili ako se na njima primenjuje pointerska aritmetika. Ako imate sreće, takav vaš program će krahirati na svim kompjuterima. U suprotnom, može se dogoditi da na jednom kompjuteru radi, a na drugom ne. Ipak, postoji jedan izuzetak: adresa prvog elementa posle kraja nekog polja može se upotrebiti u pointerskoj aritmetici. Drugo, već smo naznačili da se pointer i ceo broj mogu sabrati ili oduzeti. Konstrukcija p + n pokazuje na adresu n-tog objekta iza onog na koji pointer p trenutno pokazuje. Ovo važi bez obzira na to za koji tip objekata je pointer p deklarisan; kompajler će adresu n-tog objekta dobiti množenjem broja bajtova

koje zauzima jedan objekat i broja objekata, u ovom slučaju n. Tako dobijenu adresu kompajler će dodati na adresu na koju pokazuje pointer p, kako bi dobio konačnu adresu n-tog elementa posle onog na koji pokazuje p. Pri tome je tip objekta (a time i njegova veličina u bajtovima) određen deklaracijom pointera p. Oduzimanje dva pointera je takođe dozvoljeno: ako pointeri p i q pokazuju na elemente istog polja, onda p - q predstavlja broj elemenata između elementa na koji pokazuje p i elementa na koji pokazuje q. Ova činjenica može biti iskorišćena da se napiše još jedna verzija funkcije strlen: /* strlen: vraca duzinu niza sa ulaza */ int strlen(char *s) { char *p = s; while (*p != '\0') p++; return (p - s); } U svojoj deklaraciji, pointer p je inicijalizovan na vrednost s, tj. tako da pokazuje na prvi znak niza s. U while petlji, svaki znak se po redu proverava dok se '\0' ne pojavi na kraju. Pošto je sekvenca '\0' u stvari nula, i pošto while petlja testira samo da li je uslov jednak nuli, to je moguće izostaviti taj eksplicitno naznačeni test i pisati jednostavno while (*p) p++; Zbog toga što pointer p pokazuje na neki znak (tj. objekat znakovnog tipa), iskaz p++ će svaki put uvećati pointer i usmeriti ga na sledeći znak, a konstrukcija p - s će dati broj znakova za koji je pointer p odmakao u odnosu na početak niza, a što nije ništa drugo do dužina niza. Pointerska aritmetika je dosledna: da smo kojim slučajem radili sa objektima float tipa, iskaz p++ bi pointer p uvećao za veličinu float objekta, tako da bi on opet pokazivao na sledeći objekat. Otud, da bi napisali drugačiju verziju funkcije alloc (i afree) koja manipuliše, recimo, sa elementima float tipa umesto char, dovoljno bi bilo jednostavno pisati float umesto char na svim mestima u programu. Sve manipulacije pointerima automatski uzimaju u obzir veličinu (u bajtovima) objekta na koji je pointer usmeren, i zato u ovim funkcijama nije potrebno vršiti nikakve druge izmene. Nijedna druga operacija osim onih ovde pomenutih (sabiranje ili oduzimanje pointera i celobrojne vrednosti; oduzmanje ili poređenje dva pointera) nije dozvoljena. Nije dozvoljeno sabirati pointer i pointer, ili množiti, deliti, šiftovati i maskirati ih, ili ih sabirati sa vrednostima float i double tipa.

5.5 ZNAKOVNI POINTERI I FUNKCIJE Konstantni niz, napisan kao „I am a string“ je jedno znakovno polje. U svom internom predstavljanju, kompajler svako polje završava znakom \0 tako da programi mogu da pronađu kraj. Prostor u memoriji koji polje zauzima je otud za jedan bajt veći od onog koji je potreban za samo polje. Možda najčešći vid pojavljivanja konstantnih nizova jeste kao argumenata nekih funkcija, kao u printf(„hello, world\n“); Kada se niz znakova kakav je ovaj pojavi u programu, pristupa mu se preko pointera deklarisanog za znakovne objekte - znakovnog pointera. Ono što se funkciji printf prosleđuje jeste pointer usmeren na polje znakova. Polja znakova, naravno, ne moraju biti argumenti funkcija. Ako je message pointer deklarisan kao char *message; onda iskaz message = „now is the time“; dodeljuje pointeru message vrednost pointera usmerenog na niz znakova sa desne strane izraza. Ovo nije kopiranje nizova; operiše se isklučivo sa pointerima. C ne obezbeđuje nikakve operatore koji bi se primenjivali na čitav niz znakova kao celinu. Postoji značajna razlika između sledećih definicija: char amessage[] = „now is the time“; /* polje */ char *pmessage = „now is the time“; /* pointer */ Polje amessage je dovoljno veliko da se u njemu čuva znak '\0' i svi znakovi niza. Pojedine znakove u okviru polja je moguće promeniti, ali se naziv amessage uvek odnosi na isto polje i istu memoriju. Sa druge strane, pmessage je pointer, inicijalizovan da pokazuje na prvi znak niza; shodno tome, on može biti prilagođen tako da pokazuje na nešto drugo. Ilustrovaćemo još neke primene pointera i polja kroz primere dvaju korisnih funkcija iz standardne biblioteke. Prva funkcija je strcpy(s, t) koja kopira niz t u niz s. Argumenti su pisani tim redom po analogiji sa

izrazom dodeljivanja, u kome bi pisalo s = t da bi se neko t dodelilo nekom s. Napišimo najpre verziju sa poljem: /* strcpy: kopira niz t u niz s; verzija sa indeksima polja */ void strcpy(char s[], char t[]) { int i; i = 0; while ((s[i] = t[i]) != '\0') i++; } Poređenja radi, evo verzije sa pointerima: /* strcpy: kopira niz t u niz s; verzija sa pointerima (1) */ void strcpy(char *s, char *t) { while ((*s = *t) != '\0') { s++; t++; } } Zbog toga što joj se prosleđuju vrednosti argumenata, a ne sami argumenti, funkcija strcpy može upotrebiti promenljive s i t onako kako joj to odgovara. Ovde su oni prikladno inicijalizovani pointeri koji se kroz polje kreću znak po znak, sve dok se znak \0 kojim se završava polje t ne prekopira u polje s. U praksi, funkcija strcpy ne bi bila napisana na gore prikazani način. Druga mogućnost za njenu realizaciju bila bi /* strcpy: verzija sa pointerima (2) */ void strcpy(char *s, char *t) { while ((*s++ = *t++) != '\0') ; } Ova varijanta premešta operatore inkrementiranja u test deo while konstrukcije. Vrednost *t++ je znak na koji je pointer t pokazivao pre nego što je uvećan; sufiks operator ++ neće promeniti pointer t pre nego što se preuzme znak na koji on trenutno pokazuje. Po sličnom principu se i znak prvo smešta u polje s, pa se tek onda pointer s pomera na sledeću poziciju. Vrednost znaka koji se trenutno kopira se sa svakim prolazom kroz petlju poredi sa vrednošću \0, i petlja se okončava kada je jednakost ustanovljena. Poslednji znak koji će se prekopirati u niz s jeste upravo znak \0. Uz konačnu varijantu funkcije strcpy primetimo još jednom da je

poređenje nekog izraza sa nulom u test delu while konstrukcije suvišno, pa stoga funkcija ima oblik /* strcpy: verzija sa pointerima (3) */ void strcpy(char *s, char *t) { while (*s++ = *t++) ; } Iako može izgledati na prvi pogled nečitak, ovakav način pisanja je sasvim razumljiv; na gornju konstrukciju se treba navikavati ako ni zbog čega drugog, ono zbog toga što ćete je često sretati u C programima. Druga rutina koju ćemo analizirati je strcmp(s, t), koja upoređuje znakovne nizove s i t i u program vraća negativnu vrednost, nulu ili pozitivnu vrednost, već u zavisnosti od toga da li je niz s leksički gledano (po abecedi) manji, jednak ili veći od niza t. Vrednost koja se vraća u program se dobija oduzimanjem znakova na prvoj poziciji na kojoj se nizovi s i t razlikuju. /* strcmp: vraca <0 za (s < t), 0 za (s = t),ili >0 za (s > t) */ int strcmp(char s[], char t[]) { int i; i = 0; while (s[i] == t[i]) if (s[i++] == '\0') return (0); return (s[i] - t[i]); } Verzija funkcije strcmp sa pointerima izgleda ovako: /* strcmp: verzija sa pointerima */ int strcmp(char *s, char *t) { for ( ; *s == *t; s++, t++) if (*s == '\0') return 0; return (*s - *t); } Budući da operatori ++ i -- mogu biti napisani i u prefiks varijanti, to se i takve kombinacije operatora *, ++ i -- mogu pojaviti u programima, iako ređe. Na primer, izraz *++p uvećava pointer p pre nego što se pristupi znaku na koji pokazuje; izraz

*--p; najpre umanjuje pointer p. Postoje neke ustaljene konstrukcije koje koriste ovakve kombinacije operatora; jedna od njih je i konstrukcija za smeštanje i uzimanje vrednosti sa steka: *p++ = val; /* stavljanje vrednosti val na stek */ val = *--p; /* uzimanje vrednosti sa steka i smeštanje u val */ Datoteka <string.h> sadrži deklaracije funkcija pomenutih u ovom odeljku, kao i deklaracije mnogih drugih funkcija koje rade sa nizovima. þ Vežba 5 - 2 Napišite verziju sa pointerima funkcije strcat koju smo opisali u Poglavlju 2. þ Vežba 5 - 3 Ponovo napišite funkcije iz ranijih poglavlja, ali ovaj put u verziji sa pointerima umesto indeksa polja. Dobre mogućnosti za to daju funkcije getline (Poglavlja 1 i 4); atoi, itoa i njihove varijante (Poglavlja 2, 3 i 4); reverse (Poglavlje 3) i index i getop (Poglavlje 4). 5.6 POLJE POINTERA; POINTERI NA POINTERE Pošto su pointeri promenljive, mogli ste očekivati da će i polja pointera naći svoju primenu. To je zaista slučaj. Ilustrujmo ga pisanjem funkcije koja sortira skup linija teksta po abecednom redu, a koja je u stvari ogoljena verzija UNIX rutine sort. U Poglavlju 3 smo prikazali funkciju koja po Shell algoritmu sortira neko polje celih brojeva, a u Poglavlju 4 smo je dopunili funkcijom quicksort. Isti algoritam će i ovde funkcionisati, osim što sada radimo sa linijama teksta različitih dužina koje se, za razliku od celih brojeva, ne mogu porediti i premeštati u jednoj operaciji. Potrebna nam je takva organizacija podataka kojom ćemo na pogodan i efikasan način izaći na kraj sa promenljivom dužinom linije teksta. To je mesto na kome uvodimo polje pokazivača. Ako su linije teksta poređane jedna iza druge u nekom velikom polju znakova (koje, recimo, održava funkcija alloc), onda se svakoj liniji može pristupiti pomoću pointera usmerenog na prvi znak te linije. Sami pointeri se mogu čuvati u nekom polju. Dve linije se mogu upoređivati tako što će se jednostavno proslediti funkciji strcmp. Kada nastupi situacija da dve linije moraju zameniti mesta, onda će to umesto njih učiniti samo pointeri koji pokazuju na njih, a one će ostati na svojim mestima. Ovakav način rada eliminiše problem komplikovanog

rukovanja memorijom i problem preopterećenja koje bi nastalo pomeranjem linija. ÚÄÄÄ¿ ÚÄÄÄÄÄÄ¿ ÚÄÄÄ¿ ³ úÄÅÄÄÄÄÄÄÄÄÄÄ´defghi³ ³ úÄÅÄ¿ ÚÄÄÄÄÄÄ¿ ³ ³ ÀÄÄÄÄÄÄÙ ³ ³ ³ ÚÄÄÄÄÄÄ´defghi³ ³ ³ ÚÄÄÄÄÄÄÄÄÄÄÄ¿ ³ ³ ³ ³ ÀÄÄÄÄÄÄÙ ³ úÄÅÄÄÄÄÄÄÄÄÄÄ´jklmnopqrst³ ³ úÄÅÄÅÄÄÄÄÄÄÄÙ ÚÄÄÄÄÄÄÄÄÄÄÄ¿ ³ ³ ÀÄÄÄÄÄÄÄÄÄÄÄÙ ³ ³ ³ ÚÄÄÄ´jklmnopqrst³ ³ ³ ÚÄÄÄ¿ ³ ³ ³ ³ ÀÄÄÄÄÄÄÄÄÄÄÄÙ ³ úÄÅÄÄÄÄÄÄÄÄÄÄ´abc³ ³ úÄÅÄÅÄÄÄÄÄÄÄÄÄÄÙ ÚÄÄÄ¿ ÀÄÄÄÙ ÀÄÄÄÙ ÀÄÄÄÙ ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄ´abc³ ÀÄÄÄÙ Na slici: smer pointera pre i posle sortiranja Proces sortiranja se sastoji iz tri faze: čitanje svih linija sa ulaza sortiranje linija po abecedi štampanje sortiranih linija Kao i obično, najbolje je podeliti program na funkcije od kojih svaka obavlja jedan deo posla, i napisati glavnu funkciju koja kontroliše tok čitavog procesa. Za trenutak ostavimo proces sortiranja po strani, i koncentrišimo se na stvaranje strukture podataka i ulazno - izlaznih funkcija. Funkcija koja obrađuje ulaz mora skupljati i čuvati znake svake linije, i pritom formirati polje pointera koji pokazuju na linije. Ova funkcija takođe mora pamtiti broj ulaznih linija, pošto je taj podatak neophodan za sortiranje i štampanje. Budući da radi samo sa nekim konačnim brojem ulaznih linija, može se dogoditi da funkcija u program vrati neki besmislen broj izbrojanih linija (npr. -1) u slučaju preopterećenja. Funkcija koja formira izlaz mora samo da štampa linije onim redom na koji je ukazano poljem pointera. #include <stdio.h> #include <string.h> #define MAXLINES 100 /* max broj linija koje se mogu sortirati */ #define NULL 0 char *lineptr[MAXLINES]; /* polje pointera na linije teksta */ int readlines(char *lineptr[], int nlines); void writelines(char *lineptr[], int nlines); void qsort(char *lineptr[], int left, int right); int getline(char *, int); char *alloc(int);

main() /* kontrolna rutina */ { int nlines; /* broj ucitanih linija sa ulaza */ if ( (nlines = readlines(lineptr, MAXLINES)) >= 0) { qsort(lineptr, 0, nlines - 1); writelines(lineptr, nlines); return 0; } else { printf(„error: input too big to sort\n“); return 1; } } #define MAXLEN 1000 /* max duzina bilo koje linije sa ulaza */ int readlines(char *lineptr[], int maxlines) { int len, nlines; char *p, line[MAXLEN]; nlines = 0; while ( (len = getline(line, MAXLEN)) > 0) if (nlines >= maxlines || (p = alloc(len)) == NULL) return -1; else { line[len - 1] = '\0'; /* ukloni znak \0 */ strcpy(p, line); lineptr[nlines++] = p; } return (nlines); } Znak \0 sa kraja svake linije je uklonjen da ne bi uticao na redosled sortiranja linija. void writelines(char *lineptr[], int nlines) { int i; for (i = 0; i < nlines; i++) printf(„%s\n“, lineptr[i]); } Funkcija getline je napisana u Poglavlju 1.9. Glavnu novinu predstavlja deklaracija char *lineptr[MAXLINES]; kojom se određuje da je lineptr polje veličine MAXLINES, čiji je svaki

element pointer na objekte tipa char. To znači da je lineptr[i] znakovni pointer, i da se konstrukcijom *lineptr[i] pristupa prvom znaku niza na koji lineptr[i] pokazuje. Kako je lineptr polje, to se i ono može predstaviti pointerom baš kao i polja iz prethodnih primera, pa funkcija writelines može biti napisana kao void writelines(char *lineptr[], int nlines) { while (nlines-- >= 0) printf(„%s\n“, *lineptr++) } Na početku *lineptr pokazuje na prvi pointer; svako uvećavanje ga pomera na sledeći pointer u polju, dok se nlines ne odbroji do nule. Kada su ulaz i izlaz pod kontrolom, možemo pristupiti sortiranju. Algoritam qsort iz Poglavlja 4 zahteva neznatne izmene: deklaracije se moraju preurediti, a operacija poređenja se mora obaviti pozivom funkcije strcmp. Osnovni algoritam ostaje nepromenjen, što je predznak da će rutina i pored navedenih izmena funkcionisati. /* qsort: sortiranje v[left] ... v[right] po rastućem redosledu */ void qsort(char *v[], int left, int right) { int i, last; void swap(char *v[], int i, int j); if (left >= right) /* ne radi ništa ako polje */ return; /* sadrzi vise od dva elementa */ swap(v, left, (left + right) / 2); last = left; for (i = left+1; i <= right; i++) if ( strcmp(v[i], v[left]) < 0) swap(v, ++last, i); swap(v, left, last); qsort(v, left, last-1); qsort(v, last+1, right); } Slično tome, i funkcija zamene swap zahteva samo neznatne izmene: /* swap: medjusobna zamena v[i] i v[j] */ void swap(char *v[], int i, int j) { char *temp; temp = v[i]; v[i] = v[j]; v[j] = temp; }

Polje lineptr se u funkciji qsort zove v. Pošto je svaki njegov element znakovni pointer, onda to mora biti i temp kako bi se omogućilo prebacivanje sadržaja između njih. U Poglavlju 1 smo ukazali na to da se while i for petlje u slučaju neispunjenja uslova okončavaju pre no što se telo izvrši makar i jedanput. Ovde su one dobra garancija da će program funkcionisati i u slučaju da uopšte nema linija na ulazu. Veoma je korisno da analizirate kako se funkcije ponašaju u tom slučaju. þ Vežba 5 - 4 Napišite ponovo funkciju readlines koja linije smešta u polje koje je obezbedila funkcija main, što je bolje nego da se poziva funkcija alloc. Koliko je program dobio na brzini? 5.7 VIŽEDIMENZIONALNA POLJA C obezbeđuje pravougaona višedimenzionalna polja, iako su ona u upotrebi mnogo ređe nego polja pointera. U ovom odeljku ćemo opisati neka njihova svojstva. Analizirajmo problem konverzije dana u mesecu u dan u godini i obrnuto. Na primer, 1. Mart je šezdeseti dan godine koja nije prestupna, a 61. dan prestupne godine. Definišimo dve funkcije koje obavljaju konverziju: funkcija day_of_year konvertuje mesec i dan u dan u godini, a funkcija month_day konvertuje dan u godini u mesec i dan. Pošto funkcija month_day u program vraća dve vrednosti, to će argumenti koji se odnose na mesec i dan biti izvedeni kao pointeri; poziv month_day(1988, 60, &m, &d); će postaviti m na 2 i d na 29 (29. Februar). Obema funkcijama je potrebna ista informacija, tabela broja dana u svakom mesecu (Septembar ima 30 dana...). Pošto se broj dana u mesecu razlikuje za prestupne i neprestupne godine, lakše je odvojiti ih u dva reda dvodimenzionalnog polja nego voditi računa o tome šta se dešava sa Februarom za vreme izračunavanja. Polje i funkcije potrebne za transformacije imaju sledeće oblike: static int day_tab[2][13] = { {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}, {0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31} }; /* day_of_year: nalazenje dana u godini ako su poznati mesec i dan */ int day_of_year(int year, int month, int day) {

int i, leap; leap = year % 4 == 0 && year % 100 != 0 || year % 400 == 0; for (i = 1; i < month; i++) day += day_tab[leap][i]; return (day); } /* month_day: nalazenje meseca i dana znajuci dan u godini */ void month_day(int year, int yearday, int *pmonth, int *pday) { int i, leap; leap = year % 4 == 0 && year % 100 != 0 || year % 400 == 0; for (i = 1; yearday > day_tab[leap][i]; i++) yearday -= day_tab[leap][i]; *pmonth = i; *pday = yearday; } Podsetimo se da je vrednost aritmetičkog izraza, kakav je onaj za leap, ili nula (pogrešno) ili jedinica (tačno). U ovom primeru su te dve vrednosti pogodno upotrebljene za indeks dvodimenzionalnog polja day_tab. Samo polje mora biti spoljašnje za obe funkcije, kako bi mu obe mogle pristupiti. Odabrali smo da polje bude char tipa kako bismo ilustrovali pravilnu upotrebu char tipa za memorisanje malih celih brojeva. Polje day_tab je prvo dvodimenzionalno polje sa kojim smo se sreli. U C- u, dvodimenzionalno polje je u stvari jednodimenzionalno polje čiji je svaki element takođe polje. Otud su indeksi napisani kao day_tab[i][j] /* [red][kolona] */ umesto day_tab[i, j] /* pogresno */ kao kod većine jezika. Osim razlike u notaciji, dvodimenzionalno polje se tretira na isti način kao i u drugim jezicima. Elementi se memorišu po redovima, što znači da će, ako se elementima pristupa onim redom kojim su memorisani, krajnji desni indeks varirati najbrže. Polje se inicijalizuje listom vrednosti navedenih unutar vitičastih zagrada; svaki red se inicijalizuje odgovarajućom pod-listom navedenom takođe u vitičastim zagradama. Polje smo počeli sa kolonom u kojoj su nule zato da bi brojevi meseci išli od 1 do 12, umesto od 0 do 11. Pošto nema dovoljno prostora, ovo je bolje rešenje nego da se usklađuju indeksi. Ako je potrebno dvodimenzionalno polje proslediti funkciji, onda se u deklaraciji argumenata funkcije mora navesti broj kolona; broj redova je nevažan jer se i ovde, kao i ranije, prosleđuje pointer usmeren na početak reda. U ovom konkretnom slučaju, prosleđuje se pointer na objekte tipa int u

vidu polja od 13 elemenata. Stoga, ako polje day_tab treba proslediti nekoj funkciji f, onda će deklaracija argumenata te funkcije biti f(int day_tab[2][13]) { ... } Deklaracija bi mogla, budući da broj redova nije bitan, imati i ovakav oblik f(int day_tab[][13]) { ... } ili čak ovakav: f(int (*day_tab)[13] ) { ... } koji deklariše argument kao pointer na polje od trinaest celobrojnih elemenata. Male zagrade su neophodne pošto srednje zagrade imaju viši prioritet od operatora *. Bez malih zagrada, deklaracija int day_tab[13] predstavlja polje od 13 pointera na objekte tipa int. U opštem slučaju, samo prva dimenzija (indeks) polja može biti izostavljena iz deklaracije: sve ostale moraju biti određene. þ Vežba 5 - 5 U funkcijama day_of_year i month_day se ne vrši provera greške; otklonite taj propust. 5.8 INICIJALIZACIJA POLJA POINTERA Razmotrimo problem pisanja funkcije month_name(n), koja u program vraća pointer usmeren na string u kome je ime n-tog meseca u godini. Ovo je idealna prilika za primenu unutrašnjeg static polja. Dakle, funkcija month_name će u sebi sadržati zasebno polje stringova, i u program vraćati pointer usmeren na odgovarajući string. Tema ovog odeljka je upravo način na koji se polje stringova inicijalizuje. Sintaksa ove je veoma slična sintaksi prethodnih inicijalizacija: /* month_name: vraca ime n-tog meseca u godini */ char *month_name(int n) { static char *name[] = { „illegal month“, „January“, „February“, „March“, „April“,

„May“, „June“, „July“, „August“, „September“, „October“, „November“, „December“ }; return ( (n < 1 || n > 12) ? name[0] : name[n]); } Deklaracija polja name, polja pointera na objekte tipa char, je identična onoj za polje lineptr u programu za sortiranje. Inicijalizator je jednostavno lista znakovnih nizova; svaki je dodeljen određenoj poziciji u polju. Tačnije, znakovi i-tog niza su smešteni na nekom drugom mestu, a na poziciji name[i] se nalazi pointer koji pokazuje na njih. Pošto veličina polja nije navedena, kompajler će je sam odrediti brojeći inicijalizatore. 5.9 POKAZIVAšI I VIŽEDIMENZIONALNA POLJA Početnici u C-u ponekad ne razlikuju dvodimenzionalna polja od polja pointera kakvo je name iz prethodnog primera. Deklaracijama int a[10][10]; int *b[10]; upotreba a i b može biti slična, budući da se i a[5][5] i b[5][5] odnose na neki ceo broj. Ali, a je zaista polje: za svih 100 elemenata je u memoriji odvojen prostor, i pristup svakom od njih se izračunava po formuli 20 * red + kolona. Za b je, međutim, deklaracijom odvojeno svega deset elemenata u koje su smešteni pointeri; svaki od njih može potom biti usmeren tako da pokazuje na neko polje celih brojeva. Pod pretpostavkom da svaki od njih pokazuje na polje od deset elemenata, dolazimo do broja od 100 elemenata raspoređenih naokolo, plus 10 elemenata za pointere. Očito je da polje pointera zauzima neznatno više prostora, i uz to je pointere neophodno eksplicitno inicijalizovati. Međutim, polje pointera ima i dve prednosti: prvo, pristup nekom elementu je, umesto množenjem i sabiranjem, izveden kroz pointer. Drugo, redovi polja mogu biti različite dužine, što znači da svaki element polja b ne mora pokazivati na polje od deset elemenata; neki mogu pokazivati na dva, neki na dvadeset, a neki ni na jedan element. Iako smo celu diskusiju vodili na primeru celih brojeva, polje pointera daleko češću primenu ima kod čuvanja i manipulacije znakovnih nizova različite dužine.

Uporedite deklaraciju i sliku polja pokazivača char *name[] = {„illegal month“, „Jan“, „Feb“, „Mar“}; name: ÚÄÄÄ¿ ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ³ úÄÅÄÄÄÄÄÄÄ´illegal month\0³ ³ ³ ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ ³ ³ ÚÄÄÄÄÄ¿ ³ úÄÅÄÄÄÄÄÄÄ´Jan\0³ ³ ³ ÀÄÄÄÄÄÙ ³ ³ ÚÄÄÄÄÄ¿ ³ úÄÅÄÄÄÄÄÄÄ´Feb\0³ ³ ³ ÀÄÄÄÄÄÙ ³ ³ ÚÄÄÄÄÄ¿ ³ úÄÅÄÄÄÄÄÄÄ´Mar\0³ ÀÄÄÄÙ ÀÄÄÄÄÄÙ sa onom za dvodimenzionalno polje: char name[][15] = {„illegal month“, „Jan“, „Feb“, „Mar“}; ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ³illegal month\0 Jan\0 Feb\0 Mar\0 ³ ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ 0 15 30 45 þ Vežba 5 - 6 Napišite ponovo funkcije day_of_year i month_day tako da koriste pointere umesto indeksa. 5.10 ARGUMENTI KOMANDNE LINIJE U operativnim sistemima koji podržavaju C, postoji način da se iz komandne linije kojom se poziva neki program u taj program prenesu argumenti ili parametri. Komandna linija je linija u kojoj se unose komande operativnog sistema ili pozivaju programi. Kada se poziva funkcija main, njoj se prosleđuju dva argumenta. Prvi, po dogovoru nazvan argc, je broj argumenata navedenih iz komandne linije kojim je program pozvan; drugi je nazvan argv i predstavlja pointer na polje znakovnih nizova. U svakom nizu je smešten jedan argument. Manipulacija ovim nizovima je dobra prilika za upotrebu više nivoa pointera. Najjednostavnija ilustracija je program echo, koji ispisuje sve svoje argumente iz komandne linije u istom redu, odvojene blanko znacima. To znači

da se komandom echo hello, world štampa izlaz hello, world Po dogovoru, argv[0] sadrži ime kojim je program pozvan, pa tako argc mora biti najmanje 1. U gornjem primeru, argc je 3, a argv[0], argv[1] i argv[2] sadrže pointere na nizove „echo“, „hello,“ i „world“ respektivno. Prvi eventualni argument je, dakle, argv[1], a poslednji argv[argc-1]; osim toga, standard zahteva da argv[argc] bude nulti pointer. Ako je argc jednako 1, onda iza imena programa u komandnoj liniji ne slede nikakvi argumenti. Prva verzija programa echo tretira argv kao polje znakovnih pointera: #include <stdio.h> /* echo argumenti; prva verzija */ main(int argc, char *argv[]) { int i; for (i = 1; i < argc; i++) printf(„%s%c“, argv[i], (i < argc-1) ? „ „ : „„); printf(„\n“); return 0; } Pošto je argv isto što i pointer na polje sa tim imenom, to postoji nekoliko načina da se ovaj program izvede korišćenjem pointera umesto indeksiranja polja. Prikažimo dva od njih: #include <stdio.h> /* echo argumenti; druga verzija */ main(int argc, char *argv[]) { while (--argc > 0) printf(„%s%c“, *++argv, (argc > 1) ? „ „ : „„); printf(„\n“); return 0; } Kako je argv pointer usmeren na početak polja znakovnih nizova (dakle na argv[0]), uvećavanjem ++argv on se pomera tako da pokazuje na argv[1], gde i jeste prvi argument (tj. pointer na njega). Svako sledeće uvećavanje pomera ovaj pointer ka sledećem argumentu; *argv postaje tada pointer na taj argument. Istovremeno, argc se umanjuje; kada postane nula, onda nema više argumenata koje treba odštampati. Druga varijanta ovog programa razlikuje se od prethodne samo po načinu

na koji je funkcija printf pozvana: printf(argc > 1) ? „%s „ : „%s“, *++argv); Ovo pokazuje da prvi argument funkcije printf (koji određuje format ostalih) može biti i izraz. To nije čest slučaj, ali je vredan pomena. Za drugi primer izabran je program za traženje nekog niza znakova i štampanje linija koje ga sadrže, napisan u Poglavlju 4.1. Na njemu ćemo napraviti neka poboljšanja, uglavnom vodeći se idejom UNIX programa grep. Ako se sećate, niz koji se traži morao je biti zadat u samom tekstu programa, što očigledno nije zadovoljavajuće rešenje. Program smo izmenili utoliko što se sada niz koji se traži zadaje iz komandne linije, kao prvi i jedini argument. #include <stdio.h> #include <string.h> #define MAXLINE 1000 int getline(char *line, int max); /* find: stampa linije koje sadrže niz zadat prvim argumentom */ main(int argc, char *argv[]) { char line[MAXLINE]; int found = 0; if (argc != 2) printf(„Usage: find pattern.One argument required.\n“); else while (getline(line, MAXLINE) > 0) if ( strstr(line, argv[1]) != NULL ) { printf(„%s“, line); found++; } return found; Funkcija strstr(s, t) standardne biblioteke vraća pointer na mesto prvog pojavljivanja niza t u nizu s, ili NULL ako nema pojavljivanja. Deklaracije funkcije strstr i konstante NULL nalaze se u datoteci string.h koja je uključena u program pretprocesorskom direktivom #include. Stoga se njihove deklaracije ne pojavljuju na početku programa. Ovaj osnovni model može biti dorađen, kako bi se ilustrovale nove konstrukcije pointera. Pretpostavimo da želimo da uvedemo još dva argumenta koji se mogu, ali ne moraju navesti u komandnoj liniji. To su tzv. opcioni argumenti. Neka je značenje prvog „štampaj sve linije osim onih koje sadrže traženi niz“, a značenje drugog „ispred svake odštampane linije stavi njen redni broj“. Uobičajena konvencija u C programima je da se argumenti kojima prethodi znak - (minus) smatraju opcionim. Ako izaberemo oznaku -x (za 'osim') da naznači zahtev za izostavljanjem, i -n (za 'broj') da naznači zahtev za numeraciju linija, onda će se za ulaz

now is the time for all good men to come to the aid of their party komandom find -x -n the odštampati na izlazu 2:for all good men Pošto se program poziva navođenjem njegovog imena, to je potrebno tekst kompajlirati dajući mu ime find. Treba dozvoliti navođenje opcionih parametara bilo kojim redosledom, a program treba da ispravno funkcioniše bez obzira na to koliko je argumenata navedeno. Konkretno, pozivom funkcije index ne treba da se pristupa elementu argv[2] ako je naveden samo jedan argument, ili elementu argv[1] ako argumenata u komandnoj liniji uopšte nema. Pogodno je ako opcioni argumenti mogu da se povezuju, kao u find -nx the Evo programa: #include <stdio.h> #include <string.h> #define MAXLINE 1000 int getline(char *line, int max); /* find: stampa linije koje sadrze niz zadat prvim argumentom */ main(int argc, char *argv[]) { char line[MAXLINE]; long lineno = 0; int c, except = 0, number = 0, found = 0; while (--argc > 0 && (*++argv)[0] == '-') while (c = *++argv[0]) switch (c) { case 'x': except = 1; break; case 'n': number = 1; break; default: printf(„find: illegal option %c\n“, c); argc = 0;

found = -1; break; } if (argc != 1) printf(„Usage: find -x -n pattern\n“); else while ( getline(line, MAXLINE) > 0 ) { lineno++; if ((strstr(line, *argv) != NULL) != except) { if (number) printf(„%ld:“, lineno); printf(„%s“, line); found++; } } } Pre svakog nailaska na opcioni argument, argv se uvećava a argc umanjuje. Ako sve protekne u redu, na kraju te petlje argc će imati vrednost 1, dok će *argv pokazivati na zadati niz. Primetite da je *++argv pointer na niz u kome je argument; (*++argv)[0] je prvi znak tog niza (alternativno, konstrukcija bi se mogla napisati i kao **++argv). Zbog toga što velike zagrade [] povezuju jače nego operatori * i ++, male zagrade su neophodne; bez njih bi izraz bio protumačen kao *++(argv[0]), što je nešto sasvim drugo (i pogrešno). Poslednja konstrukcija je upotrebljena u unutrašnjoj while petlji: tu se izrazom *++argv[0] uvećavao pokazivač argv[0]. Retko se događa da se izrazi sa pointerima koriste na komplikovaniji način od ovog; u takvim slučajevima, treba ih podeliti u dva ili tri nivoa. þ Vežba 5 - 7 Napišite program expr, koji izračunava izraz napisan u obrnutoj poljskoj notaciji i unesen iz komandne linije. Na primer, komandom expr 2 3 4 + * će se izračunavati izraz 2 * (3 + 4). þ Vežba 5 - 8 Napišite program tail, koji štampa poslednjih n linija sa ulaza. Podrazumevana vrednost je n = 10, ali može biti izmenjena navođenjem opcionog argumenta. Tako će tail -n štampati poslednjih n linija. Program treba da se ponaša racionalno bez obzira na to eventualne nerazumno velike vrednosti n. Napišite program tako da na najbolji način iskoristi memoriju: linije treba da se smeste kao u programu sort, a ne u dvodimenzionalno polje fiksne veličine.

5.11 POINTERI NA FUNKCIJE U C-u, sama funkcija nije promenljiva, ali je moguće definisati pointere usmerene na funkcije koji se mogu dodeljivati, prosleđivati funkcijama, vraćati od strane funkcija ili smeštati u polja. Pristupajući pointerima moguće je pozvati funkciju na koju pokazuju. Ilustrovaćemo to modifikovanjem postupka sortiranja napisanog ranije u ovom poglavlju tako da, ako je naveden opcioni argument -n, bude sprovedeno sortiranje linija numerički a ne po abecedi. Sortiranje se obično sastoji iz tri etape: poređenja, koje ustanovljava poredak bilo kog para objekata; izmene, kojom se taj redosled eventualno obrće; i, najzad, algoritma sortiranja, zakona po kome se poređenja i izmene vrše sve dok se elementi ne poređaju kako je zadato. Algoritam je nezavisan od operacija poređenja i izmene, tako da se uvođenjem drugačijih funkcija poređenja i izmene može ostvariti sortiranje po drugačijem kriterijumu. To je ideja koju smo sledili u novom programu za sortiranje. Poređenje dva linije po abecedi se obavlja kroz funkciju strcmp, kao i do sada; ali, biće nam potrebna i rutina numcmp koja će porediti dve linije na osnovu njihove numeričke vrednosti, i koja će u program vraćati iste pokazatelje rezultata poređenja koje je vraća i funkcija strcmp. Pomenute dve funkcije su deklarisane ispred funkcije main, a pointeri usmereni na njih su prosleđeni funkciji sort. Sa svakim prolaskom kroz petlju funkcija sort poziva funkcije pomoću tih pointera. Preskočili smo deo za obradu slučaja kada su prosleđeni pogrešni argumenti zato da bi se koncentrisali na glavne karakteristike. #include <stdio.h> #include <string.h> #define MAXLINES 100 /* max broj linija koje treba sortirati */ char *lineptr[MAXLINES]; /* polje pointera na linije teksta */ int readlines(char *lineptr[], int nlines); void writelines(char *lineptr[], int nlines); void qsort(void *lineptr,int left,int right,int (*comp)(void *,void*)) int numcmp(char *, char *); main(int argc, char *argv[]) /* sortira linije sa ulaza */ { int nlines; /* broj ucitanih linija */ int numeric = 0; /* bice 1 ako je numericko sortiranje */ if (argc > 1 && strcmp(argv[1], „-n“) == 0) numeric = 1; if ( (nlines = readlines(lineptr, MAXLINES)) >= 0 ) { qsort( (void **) lineptr, 0, nlines - 1, (int (*)(void *,void *)) (numeric ? numcmp : strcmp) ); writelines(lineptr, nlines); return 0;

} else { printf(„input too big to sort\n“); return 1; } } U konstrukciji kojom se funkcija qsort poziva, strcmp i numcmp su u stvari adrese tih funkcija. Pošto je poznato da su to funkcije, operator & nije neophodan, baš kao što nije neophodan ispred imena polja. Kompajler preuzima na sebe da funkciji qsort prosledi adrese funkcija strcmp i numcmp. Sledeći korak je modifikacija funkcije qsort. Napisali smo je tako da može da radi sa bilo kojim tipom podataka, a ne samo sa nizovima znakova. Prototipom funkcije je određeno da ona očekuje polje pointera(char *lineptr[]), dva cela broja (left i right) i pointer (*comp) na funkciju koja očekuje dva argumenta (void *, void *) i koja vraća vrednost int tipa. Univerzalnost tipova podataka postignuta je prosleđivanjem argumenata void *. Ovakva konstrukcija zamenjuje bilo koji tip pointera, tako da se funkciji qsort može proslediti pointer jednog tipa a iz nje vratiti pointer drugog tipa bez gubitka informacija. /* qsort: sortira v[left] ... v[right] po rastućem redosledu */ void qsort(void *v[],int left,int right, int (*comp)(void *, void *)) { int i, last; void swap(void *v[], int, int); if (left = right) /* ne radi nista ako polje */ return; /* vise od dva argumenta */ swap(v, left, (left + right) / 2); last = left; for (i = left+1; i <= right; i++) if ( (*comp)(v[i], v[left]) < 0 ) swap(v, ++last, i); swap(v, left, last); qsort(v, left, last-1, comp); qsort(v, last+1, right, comp); } Deklaracije treba pažljivo proučiti. int (*comp) () određuje da je comp pointer usmeren na funkciju koja u program vraća vrednost tipa int. Prvi par zagrada je neophodan; bez njih int *comp () bi deklarisala comp kao funkciju koja u program vraća pointer na objekte int

tipa, što je sasvim drugačije od onog što smo hteli. Upotreba comp u liniji if ( (*comp)(v[j], v[left]) < 0 ) je slična deklaraciji: comp je pointer na funkciju, *comp je funkcija, pa je (*comp)(v[i], v[left]) u stvari njen poziv. Zagrade su neophodne kako bi se komponente pravilno povezale. Mi smo već prikazali funkciju strcmp, koja poredi dva niza. Evo i funkcije numcmp, koja poredi dva niza prema numeričkoj vrednosti izračunatoj pozivanjem funkcije atof. #include <math.h> /* numcmp: poredi nizove s1 i s2 numerički */ int numcmp(char *s1, char *s2) { double v1, v2; v1 = atof(s1); v2 = atof(s2); if (v1 < v2) return -1; else if (v1 > v2) return 1; else return 0; } Poslednji korak je pisanje funkcije swap koja zamenjuje mesta dva pointera. Ova funkcija je identična onoj koju smo predstavili ranije u ovom poglavlju, osim što su deklaracije promenjene u void *. void swap(void *v[], int i, int j) { void *temp; temp = v[i]; v[i] = v[j]; v[j] = temp; } Postoji mnoštvo drugih opcija koje se mogu dodati programu za sortiranje; neke od njih su privlačne za vežbu. þ Vežba 5 - 9 Izmenite program sort tako da rukuje sa opcionim argumentom -r kojim se zahteva sortiranje u obrnutom redosledu. Obezbedite da -r funkcioniše zajedno sa -n. þ Vežba 5 - 10 Dodajte opciju -f kojom se ignoriše razlika između malih i

velikih slova; tako će se, na primer, a i A smatrati istim slovom i porediti kao jednaka slova. þ Vežba 5 - 11 Dodajte opciju -d („rečnik“), koja vrši poređenje samo između slova, brojeva i blanko znakova. Obezbedite da funkcioniše u sprezi sa opcijom -f. 5.12 PRIMERI SLO�ENIH DEKLARACIJA Deklaracija: Objašnjenje: type fn(); funkcija fn koja u program vraća vrednost tipa type type *fn(); funkcija fn koja u program vraća pointer na objekat tipa type type (*pf)(); pointer pf na funkciju koja u program vraća vrednost tipa type type *(*pf)(); pointer pf na funkciju koja u program vraća pointer na objekat tipa type type *arr[N]; polje arr koje čini N pointera na objekte tipa type type (*parr)[N]; pointer parr na polje od N elemenata tipa type type (*(*x[N]))()[M]; polje x koje čini N pointera na funkcije koje u program vraćaju pointer na polje od M elemenata tipa type type (*(*fn())[])(); funkcija fn koja u program vraća pointer na polje koje čine pointeri na funkcije koje u program vraćaju objekte tipa typeP o g l a v l j e 6 : STRUKTURE Struktura je skup od jedne ili više promenljivih koje mogu biti različitih tipova i koje su, pošto opisuju isti objekat, grupisane pod jednim imenom radi lakšeg rukovanja (strukture se zovu 'zapisi' u nekim jezicima - primer za to je Paskal). Uobičajeni primer strukture je primer platnog spiska: svaki službenik je opisan skupom atributa kao što su ime, adresa, broj socijalnog osiguranja, plata itd. Neki od pomenutih atributa bi opet sami za sebe mogli biti strukture: ime čini par elemenata, adresu takođe, pa čak i platu. Drugi primer, još tipičniji za C, vidimo iz grafikona: tačku predstavlja par koordinata, pravougaonik predstavlja par tačaka, i tako dalje. Strukture su način da se organizuju složeni podaci, posebno kod dugačkih programa, zbog toga što u mnogim situacijama omogućavaju grupi promenljivih da budu tretirane kao jedna, umesto svaka za sebe. U ovom poglavlju ćemo

ilustrovati upotrebu struktura. Programi koje ćemo prikazati su duži od mnogih u ovoj knjizi, ali još uvek prihvatljive dužine. Glavna promena, koju je uveo ANSI standard je u definisanju dodeljivanja strukture - strukture mogu biti kopirane a zatim dodeljene, prosleđene funkcijama i vraćene u program od strane funkcija. Ovo su kompajleri podržavali dugi niz godina, ali su te osobine sada precizno definisane. Automatske strukture i polja se sada takođe mogu inicijalizovati. 6.1 OSNOVNE NAPOMENE Vratimo se rutinama za konverziju datuma napisanih u Poglavlju 5. Datum se sastoji iz nekoliko elemenata: dana, meseca i godine, i eventualno dana u godini i imena meseca. Ovih pet promenljivih mogu se smestiti u jednu strukturu na sledeći način: struct date { int day; int month; int year; int yearday; char mon_name[4]; }; Ključna reč struct uvodi listu deklaracija navedenih u vitičastim zagradama, pa lista na taj način predstavlja deklaraciju strukture. Iza ključne reči struct može, ali ne mora slediti oznaka strukture, koja nije ništa drugo do ime te strukture (u ovom primeru, ime strukture je date). Oznaka, dakle, imenuje ovu vrstu strukture, i može se potom upotrebiti kao skraćenica za neki deo deklaracije u vitičastim zagradama. Elementi ili promenljive navedeni u strukturi se nazivaju članovima. šlan strukture ili oznaka strukture i neka obična (koja nije član strukture) promenljiva mogu nositi isto ime bez konflikta, budući da se u svako doba mogu razlikovati po kontekstu u kome su upotrebljene. Naravno, bilo bi dobro uzimati ista imena samo za usko povezane objekte, ponajviše zbog stila. Deklaracija struct predstavlja tip objekta. Iza desne vitičaste zagrade, koja zaklučuje listu članova, može se navesti lista promenljivih, baš kao kod deklaracija promenljivih za int, char, itd. To znači da je konstrukcija struct { ... } x, y, z; sintaksno slična konstrukciji int x, y, z;

u smislu da se u obe konstrukcije promenljive x, y, i z deklarišu za neki tip, i u oba slučaja se za njih odvaja neki prostor. Deklaracija strukture koja nije praćena listom promenljivih neće rezervisati mesto u memoriji; njome se jednostavno opisuje oblik ili izgled strukture. Ako je deklaracija označena (tj. ima ime), onda se oznaka može kasnije upotrebiti prilikom definicija stvarnih struktura. Na primer, deklaracijom date će izraz struct date d; definisati promenljivu d kao strukturu oblika date. Struktura se može inicijalizovati tako što će njenu definiciju pratiti lista inicijalizatora, od kojih je svaki konstantan izraz: struct date d = { 4, 7, 1776, 186, „Jul“ }; šlanu strukture se pristupa izrazom sledeće konstrukcije: ime_strukture.član Operator . pristupanja članu strukture povezuje ime strukture i ime člana. Da bismo, na primer, promenljivu leap (koja određuje da li je godina prestupna ili ne), podesili u zavisnosti od datuma koji se čuva u strukturi d, pisaćemo leap = d.year % 4 == 0 && d.year % 100 != 0 || d.year % 400 == 0; Ili, da bismo proverili ime meseca, pisaćemo if ( strcmp(d.mon_name, „Aug“) == 0 ) ... ili, opet, da bismo veliko slovo u imenu meseca promenili u malo, pisaćemo d.mon_name[0] = lower(d.mon_name[0]); Strukture se mogu umetati jedna u drugu: struktura za platni spisak mogla bi izgledati ovako: struct person { char name[NAMESIZE]; char address[ADDRSIZE]; long zipcode; long ss_number; double salary; struct date birthdate; struct date hiredate; }; Struktura person sadrži, između ostalog, i dve strukture oblika date.

Ako sada neku strukturu emp deklarišemo kao tip person, pišući struct person emp; onda će se emp.birthdate.month odnositi na član month strukture birthdate (oblika date), koja je, opet, član strukture emp (oblika person). Poslednja konstrukcija se, dakle, odnosi na mesec rođenja. Operator . pristupanja članu strukture vrši pridruživanje sa leva na desno. 6.2 STRUKTURE I FUNKCIJE Postoji određen broj pravila vezanih za korišćenje struktura u C-u. Osnovna pravila su da operacije koje se mogu izvoditi na strukturama jesu dodeljivanje strukturi kao celini, uzimanje njene adrese operatorom & i pristupanje njenim članovima. Uz to, strukture mogu biti dodeljene ili kopirane kao celine, i mogu biti prosleđene funkcijama ili iz njih vraćene u program. Bilo kakva struktura (čak i automatska) se može inicijalizovati listom članova konstantnih vrednosti. Proučimo upotrebu struktura na primerima funkcija koje operišu sa tačkama i trouglovima. Postoje bar tri moguća pristupa: prosleđivanje komponenata strukture posebno, prosleđivanje čitave strukture i prosleđivanje pointera na strukturu. Svaki od njih ima svoje dobre i loše strane. Neka je neka struktura point deklarisana kao struct point { int x; int y; }; i neka je ona umetnuta u drugu strukturu rect: struct rect { struct point pt1; struct point pt2; }; Posmatrajmo funkciju makepoint kojoj se prosleđuju dve celobrojne vrednosti i koja u program vraća strukturu oblika point: /* makepoint: pravi tacku od vrednosti x i y */ struct point makepoint(int x, int y)

{ struct point temp; temp.x = x; temp.y = y; return temp; } Primetite da nema prepreke da element strukture i argument imaju isto ime - naprotiv: korišćenje istog imena ističe njihovu međusobnu vezu. Funkcija makepoint se sada može upotrebiti za inicijalizaciju struktura: struct point makepoint(int, int); struct rect screen; screen.pt1 = makepoint(0, 0); screen.pt2 = makepoint(XMAX, YMAX); struct point middle; middle = makepoint((screen.pt1.x + screen.pt2.x) / 2, (screen.pt1.y + screen.pt2.y) / 2); Funkcija addpoint je primer funkcije čiji su argumenti strukture, i koja u program vraća takođe strukturu: /* addpoint: sabiranje koordinata dve tačke */ struct point addpoint(struct point p1, struct point p2) { p1.x += p2.x; p1.y += p2.y; return p1; } Funkciji addpoint su prosleđene vrednosti elemenata struktura p1 i p2, a ne sami elementi: stoga, izračunavanja unutar funkcije addpoint neće imati uticaja na strukturu p1, bez obzira na to što je upotrebljeno isto ime. Sledeći primer je funkcija ptinrect koja ispituje da li se neka tačka nalazi unutar pravougaonika, uz usvojeni dogovor da je pravougaonik određen donjom levom tačkom pt1 (prvi element) i gornjom desnom tačkom pt2 (drugi element): /* ptinrect: vraca 1 ako je p unutar r, ili 0 ako nije */ int ptinrect(struct point p, struct rect r) { return p.x >= r.pt1.x && p.x < r.pt2.x && p.y >= r.pt1.y && p.y < r.pt2.y; } Ovo podrazumeva da su koordinate tačke pt1 manje od pt2 koordinata. Ako je pravougaonik određen drugačijim parom koordinata, sledeća funkcija,

canonrect, će ga prevesti u gore pomenuti oblik: #define min(a, b) ((a) < (b) ? (a) : (b)) #define max(a, b) ((a) > (b) ? (a) : (b)) /* canonrect: prevođenje koordinata */ struct rect canonrect(struct rect r) { struct rect temp; temp.pt1.x = min(r.pt1.x, r.pt2.x); temp.pt1.y = min(r.pt1.y, r.pt2.y); temp.pt2.x = max(r.pt1.x, r.pt2.x); temp.pt2.y = max(r.pt1.y, r.pt2.y); return temp; } Ako je potrebno proslediti funkciji neku veliku strukturu, onda je najbolje umesto nje proslediti pointer na nju. Pointeri na strukture su identični pointerima na obične promenljive. Deklaracija struct point *pp; deklariše pp kao pointer na strukturu oblika point. Na taj način će *pp predstavljati samu strukturu, a (*pp).x i (*pp).y će predstavljati elemente strukture. Sintaksa za upotrebu pointera je sledeća: struct point origin, *pp; pp = &origin; printf(„origin is (%d, %d) \n“, (*pp).x, (*pp).y); Mala zagrada je neophodna u konstrukciji (*pp).x, jer je prioritet operatora . veći od prioriteta operatora *. Izraz *pp.x znači *(pp.x), što je ovde nedozvoljeno jer x nije pointer. Pointeri na strukture se tako često koriste da je uveden alternativni način njihovog obeležavanja kako bi se skratilo pisanje. Ako je p pointer na neku strukturu, onda će izrazom p -> element strukture pointer p pokazivati na određeni element strukture. Operator -> je znak - iza koga odmah sledi znak >. Tako smo poslednji primer mogli pisati i kao struct point origin, *pp; pp = &origin; printf(„origin is (%d, %d) \n“, pp -> x, pp -> y); I operator . i operator -> se pridružuju sa leva na desno, pa su otud sledeći izrazi ekvivalentni: r.pt1.x (r.pt1).x pp -> pt1.x

(pp -> pt1).x Operatori struktura . i ->, zajedno sa zagradama () za pozive funkcija i zagradama [] za indekse nalaze se na vrhu liste prioriteta. Na primer, ako je data deklaracija struct { int len; char *str; } *p; onda izraz ++p -> len; povećava element len, a ne pointer p, jer se zbog većeg prioriteta operatora -> podrazumeva zagrada ++(p -> len). Zagrade se mogu primeniti da promene izraz: (++p) -> len će uvećati pointer p za jedan pre nego što će ga usmeriti da pokazuje na element len; (p++) -> len će pointer p uvećati nakon usmeravanja na element len. U poslednjem slučaju su zagrade nepotrebne. Na isti način, izraz *p -> str++ povećava pointer str nakon što se preko njega pristupi objektu na koji pokazuje, dok će se izrazom (*p -> str)++ povećavati objekat na koji str pokazuje. Konstrukcijom *p++ -> str se pointer p uvećava nakon što se preko pointera str pristupi objektu na koji pointer str pokazuje. 6.3 POLJA STRUKTURA Strukture su posebno pogodne za rukovanje poljima koja su međusobno povezana. Na primer, razmotrimo program koji broji pojavljivanja svake C ključne reči (neka ključnih reči ima NKEYS). Potrebno nam je polje znakova u kojem će biti nazivi ključnih reči, i polje celobrojnih elemenata za čuvanje njima odgovarajućih brojača: char *keyword[NKEYS]; int keycount[NKEYS]; Međutim, sama činjenica da su polja paralelna (tj. da je i-ti element polja keyword povezan sa i-tim elementom polja keycount), ukazuje na mogućnost drugačije organizacije. Svakoj ključnoj reči odgovara par char *word; int count; pa se stoga može uvesti polje čiji su elementi strukture, pri čemu će svaka struktura biti sačinjena od pomenutog para. Deklaracija

struct key { char *word; int count; } keytab[NKEYS]; deklariše strukturu oblika key i nakon toga definiše polje keytab od NKEYS elemenata, gde je svaki element struktura oblika key. Pri tome se za polje odvaja mesto u memoriji. Poslednja deklaracija je mogla biti napisana i drugačije: struct key { char *word; int count; }; struct key keytab[NKEYS]; Kako polje keytab sadrži konstantan skup naziva ključnih reči, to je najbolje inicijalizovati ga jedanput zauvek prilikom definisanja. Inicijalizacija polja je identična ranijim - iza definicije sledi lista vrednosti odvojenih zarezom i navedenih u vitičastim zagradama: struct key { char *word; int count; } keytab[] = { „break“, 0, „case“, 0, „char“, 0, „continue“, 0, „default“, 0, /* ... */ „unsigned“, 0, „while“, 0 }; Inicijalizatori su grupisani u parove kako bi se naglasilo koje članove kog elementa polja inicijalizuju. Bilo bi još preciznije da se za svaki element polja (strukturu) inicijalizatori navedu u zasebnim zagradama: struct key { char *word; int count; } keytab[] = { { „break“, 0 }, { „case“, 0 }, ... };

Parovi unutrašnjih zagrada nisu neophodni kada su inicijalizatori obične promenljive ili nizovi znakova, i kada su svi prisutni. Kao i obično kompajler će, u slučaju da nije navedena, dimenziju polja keytab odrediti prebrojavanjem inicijalizatora. Program za brojanje ključnih reči počinje definicijom polja keytab. Glavna rutina periodično vrši očitavanje ulaza pozivima funkcije getword koja svaki put preuzme jednu reč sa ulaza. Svaka reč sa ulaza se upoređuje sa elementima polja keytab pomoću funkcije binsearch napisane u Poglavlju 3. Da bi funkcija binsearch ispravno radila neophodno je da se ključne reči zadaju u listi rastućim redosledom. #include <stdio.h> #include <ctype.h> #include <string.h> #define MAXWORD 20 int getword(char *, int); int binsearch(char *, struct key *, int); main() /* broji kljucne reci */ { int n; char word[MAXWORD]; while ( getword(word, MAXWORD) != EOF ) if ( isalpha(word[0]) ) if ( (n = binsearch(word, keytab, NKEYS)) >= 0 ) keytab[n].count++; for (n = 0; n < NKEYS; n++) if (keytab[n].count > 0) printf(„%4d %s\n“, keytab[n].count, keytab[n].word); return 0; } /* binsearch: nalazi rec u tab[0] ... tab[n-1] */ int binsearch(char *word, struct key tab, int n) { int cond; int low, high, mid; low = 0; high = n - 1; while (low <= high) { mid = (low + high) / 2; if ( (cond = strcmp(word, tab[mid].word)) < 0) high = mid - 1; else if (cond > 0) low = mid + 1; else return mid; } return -1;

} Uskoro ćemo prikazati i funkciju getword; za sada je dovoljno reći da ona svaki put ključnu reč (ako je pronađe na ulazu) kopira u svoj prvi argument kojim je pozvana: u polje word. Vrednost NKEYS je, kako je rečeno, broj ključnih reči u polju keytab. Iako bi se samo brojanje moglo obaviti i napamet, daleko je lakše i sigurnije prepustiti to mašini, posebno ako se lista menja. Jedna od mogućnosti za ustanovljavanje veličine NKEYS je da se lista inicijalizatora završava nultim pointerom (tj. elementom „„), pa da se u nekoj petlji prolazi kroz polje keytab sve dok se ne naiđe na njega. Međutim, ovo je nepotrebno, jer je veličina polja keytab određena već u procesu kompajliranja. Broj elemenata polja keytab (broj klučnih reči) je jednostavno određen izrazom veličina polja keytab / veličina strukture key U C-u je za određivanje veličine nekog objekta obezbeđen i odgovarajući unarni operator sizeof koji se izvršava još za vreme procesa kompajliranja. Izraz sizeof (objekt) biće u toku procesa kompajliranja zamenjen celim brojem koji predstavlja veličinu objekta objekt izraženu u bajtovima. Pri tome, objekt može biti bilo koja (prethodno definisana) promenljiva, polje, struktura, ili bilo koji od osnovnih (int, char, double ...) ili programski uvedenih tipova (setite se raznih definisanih oblika struktura). Na primer, na 16-bitnim mašinama je najčešće sizeof (int) jednako 2 (bajta), sizeof (char) jednako 1, sizeof (double) jednako 4, itd. U našem slučaju, broj ključnih reči je količnik veličine polja i veličine jednog njegovog elementa. Ta činjenica je upotrebljena u pretprocesorskoj direktivi #define za dodeljivanje vrednosti simbolu NKEYS: #define NKEYS (sizeof(keytab) / sizeof(struct key)) koju treba staviti na početak programa. Vrednost NKEYS je mogla biti dobijena i na nešto drugačiji način: #define NKEYS (sizeof(keytab) / sizeof(keytab[0]) I ovde je NKEYS količnik veličine polja i veličine jednog njegovog elementa s tim što nije navedeno kako on izgleda, pa kasnije, u slučaju izmene elementa, nije potrebno vršiti nikakve izmene u daljem toku programa.

Operator sizeof ne može da se upotrebi u #if direktivi, jer ga pretprocesor ne poznaje. Sa druge strane, izraz koji stoji iza #define direktive ne izračunava pretprocesor, već se kao takav kopira na sva mesta u programu gde se NKEYS pojavljuje, i tek u procesu kompajliranja se izračunava. Stoga su gornje dve konstrukcije dozvoljene. Vratimo se na funkciju getword. Napisali smo mnogo opštiju varijantu nego što je zaista bilo potrebno za naš program, ali ona nije mnogo komplikovanija. Funkcija getword učitava sledeću „reč“ sa ulaza, gde je „reč“ ili niz slova i cifara koji počinje slovom, ili pojedinačni znak koji nije blanko znak. Funkcija u program vraća tip učitanog ulaza: ili prvi znak reči, ili EOF za kraj datoteke, ili sam znak ukoliko nije abecedni. /* getword: uzima sledecu rec ili znak sa ulaza */ int getword( char *word, int lim) { int c, getch(void); void ungetch(int); char *w = word; while (isspace(c = getch())) ; if (c != EOF) *w++ = c; if (!isalpha(c)) { *w = '\0'; return c; } for ( ; --lim > 0; w++) if (!isalnum(*w = getch())) { ungetch(*w); break; } *w = '\0'; return word[0]; } Funkcija getword koristi funkcije getch i ungetch koje smo opisali u Poglavlju 4. Kada se nailaskom na neodgovarajući znak učitavanje alfanumeričkih znakova završi, funkcija getword je učitala jedan znak previše - upravo taj poslednji. Pozivom funkcije ungetch se taj znak vraća na ulaz gde se očekuje sledeći poziv. Upotrebljeni su i makroi isspace za identifikaciju blanko znakova, isalpha za identifikaciju slova i isalnum za identifikaciju slova i cifara; svi ovi makroi su kreirani pretprocesorskom direktivom #define i nalaze se u standardnom zaglavlju <ctype.h>. 6.4 POINTERI NA STRUKTURE

Da bismo ilustrovali neke pretpostavke u vezi sa pointerima na strukture i poljima struktura, napišimo program za brojanje ključnih reči još jedanput, ovog puta koristeći pointere umesto indeksa polja. Spoljnu deklaraciju polja keytab ne treba menjati, ali se funkcije main i binsearch moraju modifikovati. #include <stdio.h> #include <ctype.h> #include <string.h> #define MAXWORD 100 int getword(char *, int); struct key *binsearch(char *, struct key *, int); /* brojanje kljucnih reci; verzija sa pointerima */ void main() { char word[MAXWORD]; struct key *p; while (getword(word, MAXWORD) != EOF) if (isalpha(word[0])) if ((p = binsearch(word, keytab, NKEYS)) != NULL) p -> count++; for (p = keytab; p < keytab + NKEYS; p++) if (p -> count > 0) printf(„%4d %s\n“, p -> count, p -> word); return; } /* binsearch: pronalazi rec u tab[0] ... tab[n-1] */ struct key *binsearch(char *word, struct key *tab, int n) { int cond; struct key *low = &tab[0]; struct key *high = &tab[n-1]; struct key *mid; while (low < high) { mid = low + (high - low) / 2; if ((cond = strcmp(word, mid -> word)) < 0) high = mid; else if (cond > 0) low = mid + 1; else return mid; } return NULL;

} Ovde postoji nekoliko stvari vrednih pažnje. Najpre, deklaracija funkcije binsearch mora naglasiti da se u program vraća pointer na strukturu oblika key, umesto celobrojna vrednost; to se navodi kako u prototipu ispred ispred funkcije main, tako i na samom početku funkcije binsearch. Ako funkcija binsearch pronađe reč, onda u program vraća pointer na nju; ako ne uspe, vraća NULL. Drugo, pristup elementima polja keytab je izveden pomoću pointera. Ovo izaziva značajnu promenu u funkciji binsearch: izračunavanje središnjeg elementa više ne može biti jednostavno mid = (low + high) / 2; /* POGREŽNO */ zbog toga što sabiranje dva pointera neće proizvesti nikakav koristan rezultat (čak ni posle deljenja sa 2), i u suštini je nedozvoljeno. Oduzimanje je dozvoljeno pa je high - low broj elemenata, i stoga mid = low + (high - low) / 2; postavlja pointer mid da pokazuje na element koji je na sredini između low i high. Trebalo bi obratiti pažnju i na inicijalizatore za pointere low i high. Moguće je pointeru dodeliti adresu prethodno definisanog objekta; upravo to je učinjeno u našem primeru. Takođe je bitno da program ne proizvodi nedozvoljene pokazivače ili da ne pokuša da pristupi elementu izvan granica polja. Definicija jezika garantuje da će pokazivačka aritmetika koja uključuje prvi element posle kraja polja (tj. &tab[n]) tačno funkcionisati. U funkciji main smo napisali for (p = keytab; p < keytab + NKEYS; p++) Ako je p pointer na strukturu, bilo kakva aritmetika izvedena na njemu uzima u obzir i stvarnu veličinu strukture, tako da p++ uvećava pointer p tačno za veličinu jedne strukture i usmerava ga da pokazuje na sledeću strukturu u polju. Ali, nemojte misliti da je veličina strukture prost zbir veličina njenih članova. Zbog potrebe raspoređivanja različitih objekata mogu se pojaviti neimenovane „rupe“ u strukturi. Na primer, ako se char tip predstavlja jednim bajtom, a int tip pomoću četiri, onda bi struktura struct { char c; int i; };

mogla zahtevati šest bajtova, a ne pet (zbog težnje kompajlera da podatke slaže od parnih adresa i time dobije na brzini izvršavanja). Zato u svakom programu koji se oslanja na veličinu nekog objekta treba koristiti sizeof operator; on će dati tačnu vrednost. Konačno, evo primedbe na izgled programa: kada funkcija vraća komplikovan tip podatka, kao u struct key *binsearch(char *word, struct key *tab, int n) ime funkcije je teško uočiti i pronaći pomoću editora teksta. Zbog toga se ponekad koristi alternativni stil: struct key * binsearch(char *word, struct key *tab, int n) Ovo je uglavnom stvar ličnog ukusa; odaberite formu koja vam odgovara i pridržavajte je se. 6.5 SAMOREFERENTNE STRUKTURE Pretpostavimo da želimo da rešimo uopšteniji problem - brojanje pojavljivanja svih reči koje se pojavljuju na nekom ulazu. Pošto lista reči nije unapred poznata, ne možemo je na pogodan način sortirati niti koristiti funkciju kakva je binsearch. Takođe, ne možemo da vršimo linearno pretraživanje svake reči koja se pojavi na ulazu da bismo videli da li se ona već pojavljivala; izvršavanje takvog programa bi bilo presporo. (Preciznije, vreme njegovog izvršavanja raste po kvadratnoj zavisnosti od broja unetih reči). Kako onda organizovati podatke da uspešno izađemo na kraj sa proizvoljno dugom listom reči? Jedno rešenje je da čuvamo skup svih reči koje su se već pojavile, i da taj skup bude sortiran u svakom trenutku. To ćemo izvesti tako što ćemo svaku reč postaviti na odgovarajuće mesto prema redosledu pojavljivanja. Sortiranje ne bi trebalo izvoditi pomeranjem reči unutar nekog jednodimenzionalnog polja jer bi i to trajalo predugo. Umesto toga ćemo upotrebiti strukturu podataka koja se zove binarno stablo. Drvo se sastoji od tzv. čvorova; svakoj reči odgovara jedan čvor. Svaki čvor sadrži: pointer na tekst reči brojač pojavljivanja te reči pointer na levi ogranak (pod-čvor) pointer na desni ogranak (pod-čvor) Nijedan čvor ne može imati više od dva ogranka; takođe može imati jedan ili nemati nijedan ogranak.

švorovi su izvedeni tako da u bilo kom čvoru levi ogranak sadrži samo one reči koje su leksikografski manje od reči vezane za taj čvor, a desni ogranak samo one reči koje su leksikografski veće. Da bi se saznalo da li se nova reč već nalazi u binarnom stablu, polazi se od početnog čvora i vrši poređenje nove reči sa onom u početnom čvoru. Ako se poklapaju, znači da je reč već prisutna, i uvećava se brojač pojavljivanja te reči. Ako je nova reč leksički manja od reči sa kojom se poredi, poređenje se nastavlja na levom pod-čvoru. U suprotnom, poređenje se nastavlja na desnom pod-čvoru. Ako ne postoji pod-čvor sa kojim bi se dalje nastavilo poređenje, to znači da reč nije već prisutna u stablu i da je njeno mesto upravo taj pod-čvor. Ovaj proces je nasledno rekurzivan, jer se iz pretraživanja jednog čvora poziva pretraživanje jednog njegovog pod-čvora i tako redom. U skladu sa tim, najprirodnije je upotrebiti rekurzivne rutine za umetanje i štampanje reči. Prikazaćemo stablo za rečenicu „now is the time for all good men to come to the aid of their party“, koje je nastalo slaganjem reči po redosledu nailaska: now / \ is the / \ / \ for men of time / \ \ / \ all good party their to / \ aid come Vraćajući se na opis čvora, jasno je da će biti predstavljen kao struktura sa četiri komponente: struct tnode { /* cvor stabla */ char *word; /* pointer na tekst */ int count; /* br. pojavljivanja */ struct tnode *left; /* levi pod-cvor */ struct tnode *right; /* desni pod-cvor */ }; Ova 'rekurzivna' deklaracija čvora može izgledati problematično, ali je sasvim korektna. Nije dozvoljeno da struktura sadrži samu sebe kao element, ali struct tnode *left; deklariše left kao pointer na čvor tnode, a ne kao strukturu oblika tnode. Povremeno, biće nam potrebne samoreferentne strukture; dve strukture koje se odnose jedna na drugu. Način na koji ćemo to ostvariti je

struct t { . . . struct s *p; /* p pokazuje na strukturu s */ }; struct s { . . . struct t *q; /* q pokazuje na strukturu t /* }; Dužina celog programa je iznenađujuće mala, zahvaljujući podršci rutina koje smo već napisali. Glavna rutina čita reči pomoću funkcije getword i postavlja ih na stablo pomoću funkcije addtree. #include <stdio.h> #include <ctype.h> #include <string.h> #define MAXWORD 20 struct tnode *addtree(struct tnode *, char *); void treeprint(struct tnode *); int getword(char *, int); /* brojanje učestanosti pojavljivanja reči */ void main() { struct tnode *root; char word[MAXWORD]; root = NULL; while (getword(word, MAXWORD) != EOF) if ( isalpha(word[0]) ) root = addtree(root, word); treeprint(root); return; } Funkcija addtree je rekurzivna. Reč se, pomoću funkcije main, dovodi do najvišeg nivoa (korena) stabla. Na svakom nivou, reč se upoređuje sa sa onom reči koja se već nalazi u čvoru, i prosleđuje se naniže u levi ili desni pod- čvor pomoću funkcije addtree. Reč može eventualno da se poklopi sa nekom od već postojećih reči (u kom slučaju se vrši uvećavanje brojača), ili da se naiđe na nulti pointer koji naznačava da mora da se napravi novi čvor i doda stablu. Ako se formira novi čvor, funkcija addtree vraća pointer na njega, koji je postavljen u matičnom čvoru. struct tnode *talloc(void); char *strdup(char *); /* addtree: postavi w na ili ispod p */

struct tnode *addtree(struct tnode *p, char *w) { int cond; if (p == NULL) { /* naišla je nova reč */ p = talloc(); /* pravi novi cvor */ p -> word = strdup(w); p -> count = 1; p -> left = p -> right = NULL; } else if ((cond = strcmp(w, p -> word)) == 0) p -> count ++; /* rec se ponovila */ else if (cond <0) /* manja rec ide u levi pod-cvor */ p -> left = addtree(p -> left, w); else /* veca rec ide u desni pod-cvor */ p -> right = addtree(p -> right, w); return p; } Memoriju za novi čvor obezbeđuje funkcija talloc, koja predstavlja modifikaciju funkcije alloc koju smo napisali ranije. Ova funkcija vraća u program pointer na slobodan prostor pogodan za smeštanje čvora stabla. Funkcija strdup kopira novu reč na skriveno mesto, brojač se inicijalizuje, i dva pod-čvora se postavljaju na nulu. Ovaj deo programa se izvršava samo na granicama stabla, kada treba dodati novi čvor. Nije uvedena (nepreporučljivo za komercijalni program) provera vrednosti koje vraćaju funkcije strdup i talloc. Funkcija treeprint štampa drvo po redosledu slaganja; u svakom čvoru ona štampa levi ogranak (tj. sve reči manje od reči u tom čvoru), zatim samu reč u tom čvoru i na kraju desni ogranak (sve reči veće od reči u tom čvoru). Ako niste sigurni kako rekurzija funkcioniše, nacrtajte sami stablo a zatim ga odštampajte pomoću funkcije treeprint; ovo je jedna od najrazumljivijih rekurzivnih rutina sa kojima se možete sresti. /* treeprint: stampanje stabla p po redosledu */ void treeprint(struct tnode *p) { if (p != NULL) { treeprint(p -> left); printf(„%4d %s\n“, p -> count, p -> word); treeprint(p -> right); } } Praktična napomena: ako stablo postane 'neuravnoteženo' zato što reči ne pristižu u slučajnom poretku, vreme izvršavanja programa može brzo narasti. Žto je još gore, ako su reči već uređene, ovaj program obavlja skupo plaćenu simulaciju linearnog pretraživanja. Postoje uopštenja binarnog stabla, na primer 2-3 stabla i AVL stabla koja ne pate od takvog ponašanja, ali ih ovde nećemo opisivati.

Pre nego što napustimo ovaj problem, na kratko se osvrnimo na problem vezan za alokatore memorije. Očigledno je da je poželjno da u programu postoji samo jedan alokator memorije, čak i ako treba praviti prostor za različite tipove objekata. Međutim, ako jedan alokator treba da obradi zahteve za, recimo, pointere na char objekte i pointere na strukture oblika tnode, proizlaze dva pitanja. Prvo, kako odgovoriti zahtevima većine računara da objekti izvesnih tipova moraju da zadovolje ograničenja koja nameće smeštanje (na primer, celi brojevi moraju da budu smešteni počev od parnih adresa)? Drugo, kakvom deklaracijom se obezbediti da alokator u program vraća različite vrste pointera? Ograničenja vezana za smeštanje podataka u memoriji mogu se lako zadovoljiti po cenu izvesnog neupotrebljenog prostora, ali uz garanciju da će alokator uvek vratiti pointer koji zadovoljava sva ograničenja. Na nekim mašinama, kod kojih se podaci bilo kog tipa moraju smestiti od parne adrese, veoma je bitno da alokator vrati pointer na parnu adresu. Kod zahteva za prostorom čija je veličina neparan broj, jedini gubitak je neiskorišćeni bajt. Budući da funkcija alloc iz Poglavlja 5 ne garantuje bilo kakvo poravnavanje, biće upotrebljena funkcija malloc iz standardne biblioteke koja to garantuje. Pitanje deklaracije funkcije malloc je neugodno za svaki jezik u kome se vrši ozbiljna provera tipa vrednosti koja se vraća u program. U C-u, dobar metod je da se malloc deklariše tako da vraća pointer na objekte tipa void (dakle pointer na bilo koji tip objekta), a da se zatim tamo gde je potrebno izvrši prilagođavanje pointera za željeni tip pomoću cast operatora. To znači da, ako je neki pointer p deklarisan kao void *p; onda će ga (struct tnode *) p konvertovati pomoću cast operatora u pointer na strukturu oblika tnode, i kao takvog upotrebiti u nekom izrazu. Funkcija malloc i odgovarajuće rutine su deklarisane u standardnom zaglavlju <stdlib.h>. Stoga se funkcija talloc može napisati kao #include <stdlib.h> /* talloc: pravi prostor za cvor */ struct tnode *talloc(void) { return (struct tnode *) malloc( sizeof(struct tnode) ); } Funkcija strdup samo kopira svoj argument (string) na sigurno mesto, koje obezbeđuje poziv funkcije malloc: char *strdup(char *s) /* pravi duplikat s-a */

{ char *p; p = (char *) malloc(strlen(s) + 1); /* +1 za '\0' */ if (p != NULL) strcpy(p, s); return p; } Funkcija malloc vraća NULL ako nema mesta; funkcija strdup prenosi tu vrednost dalje, prepuštajući obradu greške svom pozivaocu. Memorija odvojena pozivom funkcije malloc može kasnije da se pozivom funkcije free oslobodi i koristi za nešto drugo. þ Vežba 6 - 1 Napišite program koji čita C program i po abecednom redu štampa svaku grupu imena promenljivih koja su identična u prvih 6 znakova, a različita nadalje. Nemojte brojati reči koje su deo nizova i komentara. Napravite da 6 bude parametar koji može da se podešava sa ulazne linije. þ Vežba 6 - 2 Napišite program koji ispisuje kratak sadržaj nekog dokumenta: listu svih reči u programu i, za svaku reč, listu brojeva linija u kojima se ta reč pojavljuje. Izbacite česte reči kao što su 'and', 'the' itd. þ Vežba 6 - 3 Napišite program koji štampa reči sa ulaza sortirane po opadajućem redosledu u zavisnosti od učestanosti pojavljivanja. Neka svakoj reči prethodi broj njenih pojavljivanja. 6.6 PRETRA�IVANJE TABELE U ovom odeljku napisaćemo sadržaj paketa za pretraživanje tabele kako bismo ilustrovali još neke aspekte struktura. Ovakav kđd se najčešće može pronaći kod kompajlera ili pretprocesora, u njihovim rutinama za upravljanje tabelama simbola. Na primer, razmotrimo pretprocesorsku direktivu #define. Kada se naiđe na liniju kakva je #define YES 1 ime YES i tekst zamene 1 se smeštaju u tabelu. Kasnije, kada se ime YES pojavi u iskazu kakav je inword = YES; ono mora biti zamenjeno tekstom 1. Postoje dve glavne rutine koje manipulišu simboličkim imenima i tekstovima zamene. Funkcija install(s, t) zapisuje ime s i tekst zamene t u tabelu; s i t su samo nizovi znakova. Funkcija lookup(s) traži ime s u tabeli

i u program vraća pointer na mesto na kom ga je našla, odnosno NULL ako ga u tabeli nema. Upotrebljeni algoritam predstavlja zbirno pretraživanje - ime koje ulazi u tabelu pretvara se u mali ne-negativni ceo broj. Taj broj se zatim koristi za indeksiranje polja pointera. Svaki element polja pokazuje na početak lanca blokova u kojima su imena koja su konvertovanjem dala isti indeks, indeks tog elementa. Element polja može biti i NULL, ako nijedno ime konvertovanjem nije dalo njegov indeks. Svaki blok u lancu je struktura koja sadrži pointer na ime, pointer na tekst zamene i pointer na sledeći blok u lancu. Kraj lanca označava se tako što se pointeru na sledeći blok u lancu dodeljuje NULL. Izgled polja *hashtab i lanaca dat je sledećom slikom: ÚÄÄÄÄ¿ ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ ³ ÃÄÄ>struct nlist { ³ ³struct nlist { ³ * ÃÄÄÄÄ´ ³ struct nlist *next;ÃÄÄ> struct nlist *next;ÃÄ>... h ³NULL³ ³ char *name; ³ ³ char *name; ³ a ÃÄÄÄÄ´ ³ char *defn; ³ ³ char *defn; ³ s ³NULL³ ³}; ³ ³}; ³ h ÃÄÄÄÄ´ ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ t ³NULL³ a ÃÄÄÄÄ´ ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ b ³ ÃÄÄ>struct nlist { ³ [ ]ÃÄÄÄÄ´ ³ NULL ³ ³NULL³ ³ char *name; ³ ÃÄÄÄÄ´ ³ char *defn; ³ ³... ³ ³}; ³ ³ ³ ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ Kako se sa slike može videti, blok u lancu je struct nlist { /* pocetak lanca */ struct nlist *next; /* next: pokazuje na sledeci blok */ char *name; /* ime */ char *defn; /* tekst zamene */ }; Polje pointera je #define HASHSIZE 100 static struct nlist *hashtab[HASHSIZE]; /* polje pointera na */ /* strukture oblika nlist */ Zbirna funkcija hash(), koju koriste i funkcija lookup() i funkcija install(), vrši konvertovanje imena u odgovarajuću vrednost. Ona dodaje vrednost svakog znaka imena proizvoljnoj kombinaciji prethodnih. Tako

dobijena vrednost deli se sa veličinom polja *hashtab (sa HASHSIZE), a ostatak takvog deljenja predstavlja indeks nekog elementa polja. To znači da će ime biti smešteno u lanac na koji pokazuje tako dobijeni element polja. U tom lancu se mogu već nalaziti neka druga imena koja su konvertovanjem dala isti indeks. Ovo nije najbolji mogući algoritam, ali je kratak i efikasan. /* hash: formira zbirnu vrednost stringa s */ unsigned hash(char *s) { unsigned hashval; for (hashval = 0; *s != '\0'; s++) hashval = *s + 31 * hashval; /* proizvoljna formula */ return hashval % HASHSIZE; } Deklaracija unsigned obezbeđuje da rezultat funkcije ne bude negativan broj. Gornja funkcija, dakle, proizvodi polaznu tačku u polju hashtab; ime koje bi moglo biti bilo gde, biće u lancu blokova koji počinje odatle. Pretraživanje tabele u potrazi za imenom vrši funkcija lookup. Ako ustanovi da je ime prisutno, u program vraća pointer na njega; ako ne pronađe ime, vraća NULL. /* lookup: trazi s u polju hashtab */ struct nlist *lookup(char *s) { struct nlist *np; for (np = hashtab[hash(s)]; np != NULL; np = np -> next) if (strcmp(s, np -> name) == 0) return np; /* pronađeno */ return NULL; /* nije pronađeno */ } Petlja for u funkciji lookup je standardna konstrukcija za pretraživanje lanca blokova: for (pointer = head; pointer != NULL; pointer = pointer -> next) Funkcija install koristi funkciju lookup da bi ustanovila da li ime koje se smešta u tabelu već tamo postoji; ako postoji, onda umesto starog teksta zamene treba staviti novi. U suprotnom, treba kreirati potpuno novi blok u lancu. Funkcija install će u program vratiti NULL, ako iz bilo kojeg razloga nema prostora za novi ulaz. struct nlist *lookup(char *); char *strdup(char *); /* install: smesta ime i definiciju u polje hashtab */ struct nlist *install(char *name, char *defn) { struct nlist *np; unsigned hashval;

if ((np = lookup(name)) == NULL) { /* nije vec u tabeli */ np = (struct nlist *) malloc(sizeof(*np)); if (np == NULL) /* greska ili nema mesta */ return NULL; if ((np -> name = strdup(name)) == NULL) return NULL; hashval = hash(np -> name); np -> next = hashtab[hashval]; hashtab[hashval] = np; } else /* ime vec postoji */ free((void *) np -> defn); /* uklanja prethodnu def. */ if ((np -> def = strdup(defn)) == NULL) /* nova def. */ return NULL; /* ako postoji greska */ return np; /* uspesna instalacija imena u tabelu */ } þ Vežba 6 - 4 Napišite rutinu koja uklanja ime i definiciju (tekst zamene) iz tabele koju održavaju funkcije install i lookup. þ Vežba 6 - 5 Kreirajte jednostavnu varijantu pretprocesora koji bi prepoznavao samo direktivu #define, i koji bi mogao da se (uz pomoć rutina iz ovog odeljka) koristi u C programima. Možda će vam funkcije getch i ungetch biti od koristi. 6.7 DEKLARACIJA TYPEDEF C obezbeđuje olakšicu koja se zove typedef za kreiranje novih imena za tipove podataka. Na primer, deklaracija typedef int Length; uvodi ime Length kao sinonim za tip int. 'Tip' Length može se upotrebiti u deklaracijama, kod cast operatora, itd. isto onako kako bi se upotrebio i int: Length len, maxlen; Length *lengths[]; Na sličan način, deklaracija typedef char *String; uvodi ime String kao sinonim za char *, tj. za pointer na objekte tipa char. Taj sinonim može kasnije biti upotrebljen u deklaracijama kao što je String p, lineptr[MAXLINES], alloc(int);

int strcmp(String, String); p = (String) malloc(100); Primetite da se sinonim deklarisan pomoću typedef pojavljuje na mestu na kom se pojavljuju promenljive, a ne odmah iza reči typedef. Sa stanovišta sintakse, typedef je sličan deklaracijama extern, static itd. Početno slovo sinonima je veliko, da bi se istaklo ime. Kao složeniji primer, mogli bismo uvesti typedef deklaracije za čvorove stabla ranije opisane u ovom poglavlju: typedef struct tnode *Treeptr; struct tnode { /* cvor stabla */ char *word; /* pointer na tekst */ int count; /* br. pojavljivanja */ struct tnode *left; /* levi pod-cvor */ struct tnode *right; /* desni pod-cvor */ } Treenode; Ovim se stvaraju dva nova tipa ključnih reči nazvanih Treenode (struktura) i Treeptr (pointer na strukturu). Tako rutina talloc može postati Treeptr talloc(void) { return (Treeptr) malloc(sizeof(Treenode)); } Mora se naglasiti da deklaracija typedef ne uvodi novi tip podataka; ona samo dodaje novo ime nekom od već postojećih tipova. Nema ni novog značenja promenljivih: promenljive deklarisane na ovaj način imaju iste osobine kao i one koje su deklarisane na uobičajen način. U suštini, deklaracija typedef slična je pretprocesorskoj direktivi #define, ali sa tom razlikom što je obrađuje kompajler umesto pretprocesora. Otud se ovom deklaracijom mogu izvoditi tekstualne zamene koje prevazilaze mogućnosti pretprocesora. Na primer, deklaracija typedef int (*PFI) (char *, char *); kreira tip PFI umesto 'pointera na funkciju (sa dva char * argumenta) koja u program vraća vrednost tipa int', i koji se potom može koristiti u kontekstu kakav je PFI strcmp, numcmp, swap; iz programa za sortiranje napisanog u Poglavlju 5. Pored čisto estetskih razloga, postoje dva glavna razloga za upotrebu typedef deklaracije. Prvi je da se odrede parametri programa zbog problema prenosivosti. Ako se typedef deklaracije koriste za tipove podataka koji

zavise od računarskog sistema, onda kada se program premesti treba promeniti samo typedef deklaraciju (a ne sva mesta na kojima se pojavljuju ti tipovi). šesta je situacija da se typedef deklaracije koriste za uvođenje sinonima za različite celobrojne veličine (short, int ili long), pa da se zatim napravi odgovarajući izbor od short, int i long za svaki pojedinačni računar. Primer za to su tipovi size_t i ptrdiff_t iz standardne biblioteke. Druga namena typedef deklaracije je da obezbedi bolju preglednost programa - tip nazvan Treeptr je jednostavniji za razumevanje od onog koji je deklarisan samo kao pointer na složenu strukturu. 6.8 UNIJE Unija je promenljiva koja može čuvati (u različitim trenucima) objekte različitih tipova i veličina, ostavljajući kompajleru da vodi računa o zahtevima za veličinom i rasporedom. Unije predstavljaju način za manipulaciju različitim tipovima podataka u okviru jednog te istog memorijskog prostora, bez ubacivanja u program bilo kakve informacije koja je zavisna od tipa mašine. Kao primer, opet preuzet iz tabele simbola koju koristi kompajler, pretpostavimo da konstante u njoj mogu biti int tipa, float tipa ili biti pointeri na objekte tipa char. Vrednost svake konstante mora biti smeštena u promenljivu odgovarajućeg tipa, ali je za manipulaciju tabelama najpogodnije da neka vrednost zauzima istu veličinu memorije i da je smeštena na istom mestu bez obzira na njen tip. To je svrha unije - da obezbedi jednu jedinu promenljivu koja može čuvati podatke bilo kog od više različitih tipova. Sintaksa je preuzeta od struktura: union u_tag { int ival; float fval; char *pval; } u; Promenljiva u će biti dovoljno velika da čuva objekte onog od ova tri tipa, koji zahteva najviše prostora. To važi bez obzira na kompjuter na kojem je program kompajliran - kompajler proizvodi kod nezavisan od hardverskih karakteristika. Bilo koji od ovih tipova može da se pridruži promenljivoj i da se potom dosledno koristi u izrazima; važeći tip mora biti onaj koji je najskorije memorisan u uniji. Na programeru je odgovornost da vodi računa o tome koji je tip trenutno smešten u uniji. Ako je vrednost jednog tipa smeštena u uniju, a odatle potom upotrebljena kao vrednost drugog tipa, posledice će zavisiti od konkretne mašine. Sintaksno gledano, članovima unije se pristupa

ime-unije.član ili pointer na uniju -> član baš kao i kod struktura. Ako se neka promenljiva utype iskoristi za ispitivanje tipa koji je trenutno smešten u uniji u, onda se može pisati if (utype == INT) printf(„%d\n“, u.ival); else if (utype == FLOAT) printf(„%f\n“, u.fval); else if (utype == STRING) printf(„%s\n“, u.pval); else printf(„bad type %d in utype\n“, utype); Unije se mogu pojaviti unutar struktura i polja, i obrnuto. Sintaksa za pristup članu unije koja je umetnuta u strukturu (ili obrnuto) je identična onoj za umetnute strukture. Na primer, za polje struktura symtab[NSYM] definisano kao struct { char *name; int flags; int utype; union { int ival; float fval; char *pval; } u; } symtab[NSYM]; članu ival se pristupa pomoću symtab[i].u.ival a prvom znaku niza na koji pokazuje pointer pval pomoću *symtab[i].u.pval ili symtab[i].u.pval[0] U suštini, unija je struktura čiji se svi članovi smeštaju od iste, za tu uniju početne adrese. To je, praktično, struktura dovoljno velika da u nju stane njen 'najveći' član, a raspored je odgovarajući za sve tipove u uniji. Iste operacije koje su dozvoljene na strukturama dozvoljene su i na unijama: kopiranje unije kao celine, dodeljivanje adrese i pristupanje njenim

članovima. Unija se može inicijalizovati samo vrednošću tipa njenog prvog člana; otud prethodno opisana unija može da se inicijalizuje samo pomoću vrednosti int tipa. 6.9 BIT - POLJA Kada slobodan memorijski prostor postane kritičan, može se ukazati potreba za smeštanjem nekoliko objekata u jednu jedinu mašinsku reč; uobičajeni slučaj primene tako pakovanih podataka jesu zastavice, jednobitni indikatori stanja koji se upotrebljavaju u aplikacijama kakva je tabela simbola kompajlera. Formati podataka diktirani od strane spoljnih uređaja, kao što su interfejsi za hardverske uređaje, često nameću potrebu za pristupom delovima mašinske reči. Zamislite deo kompajlera koji manipuliše tabelom simbola. Svaki identifikator (ime promenljive, ime funkcije i dr.) u programu ima sebi pridruženu određenu informaciju. Na primer, da li se ili ne radi o ključnoj reči, da li je ili nije u pitanju spoljni (extern) ili statički (static) simbol, i tako redom. Najsažetiji način da se zapiše takva informacija jesu jednobitni indikatori unutar jednog char ili int objekta. Ovo se obično postiže definisanjem skupa tzv. maski koje odgovaraju pojedinim bit-pozicijama, i koje u logičkim operacijama setuju ili resetuju te bitove. Na primer: #define KEYWORD 01 /* maska za nulti bit */ #define EXTERNAL 02 /* maska za prvi bit */ #define STATIC 04 /* maska za drugi bit */ ili enum { KEYWORD = 01, EXTERNAL = 02, STATIC = 04 }; Brojevi moraju biti stepeni od broja dva. Na taj način pristupanje bitovima postaje stvar pomeranja bitova pomoću operatora za šiftovanje, maskiranje i komplementiranje opisanih u Poglavlju 2. Određene konstrukcije se često pojavljuju: flags |= EXTERNAL | STATIC; setuje (postavlja na 1) EXTERNAL i STATIC bitove u flags, dok ih flags &= ~(EXTERNAL | STATIC); resetuje (postavlja na nulu), a if ((flags & (EXTERNAL | STATIC)) == 0) ...

je tačno ako su oba pomenuta bita resetovana. Iako je ovim konstrukcijama lako ovladati, C kao alternativu nudi mogućnost direktnog definisanja i pristupa poljima unutar jedne reči, umesto upotrebe bit-operacija. Bit-polje, skraćeno polje, predstavlja skup susednih bitova unutar jedne memorijske jedinice koju ćemo zvati 'reč', a čija će veličina zavisiti od konkretne primene. Sintaksa definisanja i pristupa poljima je zasnovana na strukturama. Na primer, gornja #define tabela simbola je mogla biti zamenjena definicijom tri polja: struct { unsigned int is_keyword : 1; unsigned int is_extern : 1; unsigned int is_static : 1; } flags; Ova konstrukcija definiše promenljivu zvanu flags koja sadrži tri 1-bitna polja. Broj koji sledi iza znaka dvotačke označava veličinu polja u bitovima. Polja su deklarisana kao unsigned int kako bi se naglasilo da je zaista reč o nenegativnim veličinama. Pojedinačnim poljima se pristupa pomoću flags.is_keyword, flags.is_extern itd., baš kao i drugim članovima strukture. Polja se ponašaju kao mali celi nenegativni brojevi, i mogu učestvovati u aritmetičkim izrazima jednako kao i drugi celi brojevi. Na taj način bi prethodni primeri mogli biti mnogo prirodnije napisani kao flags.is_extern = flags.is_static = 1; za setovanje bitova; flags.is_extern = flags.is_static = 0; za resetovanje bitova; i if (flags.is_extern == 0 && flags.is_static == 0) . . . za testiranje tih bitova. Skoro sve što je u vezi sa poljima zavisi od implementacije. Da li polje sme da pređe granicu reči takođe je definisano implementacijom. Polja ne moraju biti imenovana; neimenovana polja (samo dvotačka i veličina polja) koriste se za popunu. Specijalna veličina 0 može poslužiti da se sledeća polja smeštaju u novu reč. Polja se dodeljuju sa leva na desno na jednim računarima, a sa desna na levo na drugim, odražavajući tako različitost hardvera. To znači da iako su bit-polja veoma korisna za manipulaciju interno definisanih struktura

podataka, pitanje sa koga kraja početi treba pažljivo razmotriti kada se pristupa podacima koji su definisani spolja. Programi koji zavise od takvih stvari nisu prenosivi. Ostala ograničenja koja treba imati na umu: bit-polja su nenegativna; mogu se smeštati samo u objekte int tipa (ili, ekvivalentno, unsigned); ona nisu isto što i obična polja; ona nemaju adrese, pa se na njih operator & ne može primeniti.�