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
20
40
60
...
260
280
300
-17
-6.7
4.4
15.6
...
126.7
137.8
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, 16bitni 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 Ca. 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
int
jedan bajt, može čuvati jedan znak iz lokalnog skupa znakova
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
\b
\f
\n
\r
\t
\v
znak za zvučni signal
\\ obrnuta kosa crta
povratnik
\? znak pitanja
form feed
\' jednostruki navodnik
novi red
\“ dvostruki navodnik
carriage return
\ooo oktalni broj
horizontalni tabulator
\xhh heksadecimalni broj
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
AND
1100010110011010
0000000000011111
----------------------------------0000000000011010
(neko proizvoljno n)
(broj 31)
(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
OR
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 (x = 1)
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
0000000000001000
(x = 8)
0000000000100000
(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:
1111111111111111
(~0)
2. Potom je izvršeno šiftovanje ulevo za n = 3 pozicije kako bi se na
krajnja desna tri mesta pojavile nule:
1111111111111000
(~0 << n)
3. Ovo je upravo komplement maske koja nam je potrebna, pa se stoga
jednostavno ponovo primeni unarni operator ~:
0000000000000111
~(~0 << n)
Posle primene operatora & na vrednost x i upravo kreiranu masku, dobiće se
željeni efekat:
0 0 0 . . . . . . . . . . bit4 bit3 bit2
&
0000000000000 1 1
1
----------------------------------------------0 0 0 0 0 0 0 0 0 0 0 0 0 bit4 bit3 bit2
(x)
~(~0 << n)
þ 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 -.
OPERATOR
() {} -> .
! ~ ++ -- + - * & tip sizeof
* / %
+ < <=
> >=
== !=
&
^
|
&&
||
?:
= += -= *= /= &= ^=
,
| = <<= >>=
asocijativnost
sleva nadesno
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
12-45+*=
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
#include <stdio.h>
#include <math.h>
#include „calc.h“
#define MAXOP 100
main() {
...
}
getop.c
#include <stdio.h>
#include <ctype.h>
#include „calc.h“
getop() {
...
}
...
stack.c
#include <stdio.h>
#include „calc.h“
#define MAXVAL 100
int sp = 0;
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)
napisao kao
#define square(x)
(x) * (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)
{
int temp;
temp = x;
x = y;
/* pogresno */
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 Cu, 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();
type *fn();
type
type
type
type
type
type
funkcija fn koja u program vraća vrednost tipa type
funkcija fn koja u program vraća pointer na objekat
tipa type
(*pf)();
pointer pf na funkciju koja u program vraća vrednost
tipa type
*(*pf)();
pointer pf na funkciju koja u program vraća pointer
na objekat tipa type
*arr[N];
polje arr koje čini N pointera na objekte tipa type
(*parr)[N];
pointer parr na polje od N elemenata tipa 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
(*(*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.
Download

P o g l a v l j e 1 : OSNOVNE NAPOMENE