Računari 27, jun 1987

Sa bejzika na C

Dejan Ristanović (dejanr@sezam.com)

[Dvadeset i kusur godina kasnije, 1. februara 2008: Tekst koji sledi objavljen je juna 1987. godine kao specijalni dodatak časopisa "Računari", tri meseca posle dodatka "Sa bejzika na paskal". Pokazaće se da su ta dva umetka moji tekstovi koji "najduže traju" - i dan-danas mi ih povemeno traže. "Sa bejzika na paskal" sam pre 10 godina stavio na svoju Web stranu, a ovaj tekst je do nedavno čamio u nekom folderu sa materijalima konvertovanim sa BBC-ja, da bih ga sada najzad pripremio za Web. Zasluge za to ima Vladimir Stefanović, koji je umetak toliko ishvalio da sam jednostavno morao da potrošim nekoliko sati na ubacivanje listinga i malo HTML formatiranja. I tako, evo teksta, ili barem njegovog većeg dela - izbacio sam uvodna poglavlja o Lattice C-u i Mark Williams C-u, koji su danas zaboravljeni, pa počinjemo od samog C-a. A ako baš ne možete da izdržite bez tih uvodnih poglavlja, stavio sam ih na posebnu stranu.]

O C-u se mnogo govori i piše ali ga, bar na našim meridijanima, malo ko zaista i koristi. Razlog za ovo zapostavljanje svakako nisu karakteristike C-a koje, uostalom, retko ko poznaje do detalja - domaće hakere mnogo više brine nedostatak literature i kvalitetnih kompajlera. Verujući da dobar deo čitalaca "Računara" s vremene na vreme piše sistemski softver i da je dobrom delu ovakvih programera asembler dosadio, drugi nastavak naše serije umetaka o programskim jezicima posvećujemo baš C-u.

Za razliku od mnogobrojnih modernih programskih jezika koji se liferuju gotovo konfekcijski i čija je osnovna filozofija da budu različiti od drugih, C je jezik sa vrlo jasnom koncepcijom usmeren na vrlo određeno tržište. Obzirom da je poznavanje koncepcije nekog programskog jezika neophodan preduslov za njegovu uspešnu upotrebu, pokušaćemo da opišemo ideje kojima se rukovodio Denis Ritchie pišući prvi C kompajler.

Svima nam je dobro poznata razlika između interpretatora i kompajlera - znamo da se interpretator koristi za interaktivno pisanje programa čija brzina izvršavanja nije kritična dok se kompajleri "potežu" kada je svaki sekund rada dragocen. Postoje, međutim, kompajleri i kompajleri - brzina izvršavanja istog programa na istom računaru može da se promeni gotovo za red veličine u zavisnosti od primenjenog prevodioca! Dobri kompajleri, naime, vrše takozvanu optimizaciju koda.

Optimizaciju ćemo najbolje razumeti na primeru: pretpostavimo da kompajler prevodi izraz I=J+K na mašinac. Ukoliko bi procesor bio izrazito registarski orijentisan, prevod ove naredbe bi mogao da bude:

slika 1.1:
MOV  j,  R0   ;   i:=j+k
MOV  k,  R1
ADD  R1, R0
MOV  R0, i

Sledeća naredba izvornog programa je, recimo, J=J+1 što bi moglo da se prevede kao na slici 1.2 - dajemo dve varijante pri čemu dobro znamo da većina kompajlera usvaja prvu i lošiju.

slika 1.2:
MOV  j,  R0   ;    j:=j+1 - loša varijanta
MOV  #1, R1
ADD  R1, R0
MOV  R0, j
MOV  j,  R0   ;    j:=j+1 - dobra varijanta
INC  R0
MOV  R0, j

Reklo bi se da je prevod na slikama 1.1 i 1.2 savršen. On to jeste ako se svaka od naredbi I=J+K i J=J+1 uzima sama za sebe; ove dve naredbe, međutim, slede jedna iza druge što znači da bismo mogli da ih prevedemo kao na slici 1.3.

slika 1.3:
MOV  k,  R0   ;   i:=j+k; j:=j+1
MOV  j,  R1
ADD  R1, R0
MOV  R0, i
INC  R1
MOV  R1, j

Program sa slika 1.3 očito ima jednu instrukciju manje od programa sa slika 1.1 i 1.2. Ušteda je postignuta zahvaljujući činjenici da se vrednost promenljive J u toku izvršenja naredbe I=J+K već dovodi u registar R1 što znači da je možemo zgodno inkrementirati i smestiti u memoriju. Možda vam se čini da ušteda jedne instrukcije nije vredna pažnje ali znate kako se kaže: zrno po zrno, pogača. Dopustite, dakle, da vam navedemo jedan primer drastično uspešne optimizacije.

Autor ovog umetka je pre dosta godina želeo da "oseti" brzinu nekog velikog sistema (radilo se, koliko se sećamo, o IBM 370) pa je otkucao fortran program koji bi, preveden na bejzik, mogao da izgleda otprilike ovako:

10 FOR I=1 TO 10000
20 A=SIN (0.5)
30 NEXT I
40 END

IBM 370 je ovaj program izvršio trenutno. Zamenjivao sam konstantu 10,000 sve većim brojevima i tako stigao do milijarde a program se i dalje izvršavao trenutno; računar, ipak, nije mogao da bude baš toliko brz! Pokazalo se da je optimizacioni kompajler primetio da se u petlji neprekidno računa sinus istog broja pa je ovo računanje "izvadio iz petlje" što bi se otprilike svelo na zamenu mesta naredbi 10 i 20. Čak i pošto smo to shvatili, program je bio prebrz: prazna petlja sa stotinak miliona operacija bi itekako morala da se oseti. Kompajler je, međutim, bio daleko pametniji nego što smo očekivali: "shvatio" je da je višestruko ponavljanje petlje (sa njegovog stanovišta) besmisleno pa je čitavu strukturu jednostavno ignorisao. Pokušali smo da petlju učinimo smislenom zamenjujući naredbu 20 sa 20 A=SIN(A*I) ali je kompajler i dalje bio pametniji. U toj smo fazi istakli belu zastavu premda smo od "pravog" rešenja bili udaljeni svega jedan korak: iako je petlja bila smislena, kompajler je zaključio da se njeni rezultati u daljem toku programa ne koriste pa ju je ignorisao; trebalo je dodati naredbu 35 PRINT A. Postojalo je, jasno, i drugo rešenje: prevesti program sa FORTRAN /NOOPTIM.

Optimizacija sa slike 1.3 je po svojoj prirodi sasvim različita od optimizacije koju smo upravo opisivali: dok je u našem slučaju kompajler ispravljao (uslovno rečeno) gluposti koje je programer napravio, optimizacija naredbi I=J+K: J=J+1 je ispravila jednu manu bejzika, fortrana i paskala koji ne omogućavaju izrazu da da nekoliko rezultata.

Dobri optimizacioni kompajleri su mahom rezervisani za veće kompjuterske sisteme: potrebna je bar snaga 32-bitnog procesora da bi se izvela brza i kvalitetna optimizacija mašinskog programa! Ovakvi su kompajleri, osim toga, vrlo glomazni (neprimerni čak i hard disku od dvadesetak megabajta), komplikovani i, zbog svega toga, skupi. Pisanje sistemskog softvera za personalne računare, sa druge strane, nameće racionalnost dobijenog koda kao jedan od osnovnih kriterijuma kvaliteta programa - racionalnost ovde obuhvata kako minimalno zauzeće memorije tako i brzinu rada.

Teorijski posmatrano, programi će biti najkraći i najbrži ako ih pišemo na asembleru. Praksa se, međutim, ne poklapa uvek sa teorijom: ako softver ne piše savršeno utreniran programer, posao će trajati predugo dok će rezultat biti filovan bagovima koji se teško nalaze u zapetljanom kilometarskom listingu. Mašinski programi su, osim toga, primenjivi samo na jednoj jedinoj mašini što je vrlo neprijatno za softverske kuće koje se bore za zaradu. Trebalo je, dakle, izmisliti jezik koji će zadovoljiti nekoliko teških uslova:

1. Kompajler treba da bude mali i brz tj. primenjiv na šesnaestobitnim a možda i na osmobitnim mašinama.

2. Jezik treba da omogući direktan pristup memoriji i hardveru tj. da bude pogodan za sistemsko programiranje i razvoj operativnih sistema.

3. Jezik treba da omogući programeru da piše racionalan kod koji će, uz veoma umerene optimizacije, biti maksimalno brz.

4. Programi pisani na jednom računaru treba da budu primenjivi i na drugim mašinama koje su snabdevene sličnim kompajlerom.

C u velikoj meri zadovoljava sva ova četiri uslova što je, po nama, sasvim dovoljan motiv da nastavite sa čitanjem ovog umetka! Ništa, međutim, nije savršeno - na sledećim ćemo se stranicama upoznati i sa manama C-a a sami ćete svakako primetiti da je on prilično složen. Vatrene pristalice C-a će, doduše, reći da se radi o jeziku koji je izuzetno jednostavan za učenje - jedva desetak naredbi. Čitaoci ovoga umetka su, međutim, dobro upoznati sa bejzikom i (možda) ponekim drugim "fortranoidnim" jezikom pa će im programiranje na C-u ponekad izgledati čudno i strano. Nemojte se, međutim, predavati: čak i ukoliko u početku budete pisali bejzik programe na C-u (što je prilično lako), postepeno ćete se sve bolje upoznavati sa filozofijom novog jezika i početi da pišete sve racionalniji softver.

Upotreba kompajlera

Novi programski jezik teško može da se nauči na papiru - potreban je računar na kome mogu da se isprobaju razne naredbe i njihove kombinacije. Važnost ove teorijske konstatacija varira od jezika do jezika: poznavaoci bejzika će, na primer, "progovoriti" fortran čitajući par stranica uputstva (važi i obratno) dok će, sa druge strane, bejzik programer lišen računara imati mnogo muke sa paskalom ili algolom. C je jedan od "drugačijih" jezika što znači da ga nema smisla učiti bez kompjutera. Računar, naravno, nije dovoljan - treba vam i C kompajler. Pripremajući ovaj umetak koristili smo IBM PC AT kompatibilnu mašinu i dva kompajlera: Lattice C (u daljem tekstu LC) i Mark Williams C (MWC). Sve programe koje budemo napisali ćemo, po potrebi, lako prevesti uz pomoć nekog drugog kompajlera kao što je Microsoft C. C kompajleri, jasno, nisu privilegija vlasnika PC-ja: programiranje na C-u možete da naučite i ako ste vlasnik Atarija 520, Amige, QL-a, BBC-ja... Opisivanje kompajlera za razne kompjutere, na žalost, izlazi iz domena ovoga umetka, ali će primeri koje dajemo raditi na svakom iole kompletnom C komajleru - C je portabilan jezik. Doduše, portabilnost nije stoprocentna: neki programi koji rade sa pointerima (sa pointerima ćete se, kada dođe vreme, mučiti barem onoliko koliko smo se i mi mučili) savršeno rade na LC-u dok na MWC-u daju besmislene rezultate. Tih smo se programa ovde odrekli, ali postoji mogućnost da neki od primera ne radi na vašoj konfiguraciji. U tom slučaju morate sami da se snađete koristeći dokumentaciju koja vam je na raspolaganju.

Ostalo je još da opišemo kako se C kompajleri ponašaju kada naiđu na nešto što im se ne dopada - u početku ćete, sasvim prirodno, proizvoditi gomile sintaksnih greški. Svaka greška izaziva trenutni prekid kompilacije i ispisivanje poruku čiji je sadržaj ponekad gotovo šifrovan - greška čak ne mora ni da se nađe u liniji na koju poruka ukazuje već u nekoj od prethodnih! Sledeća generacija greški se javlja u toku povezivanja: linker možda ne može da pronađe neku pozvanu proceduru. Kada prebrodimo i ovu prepreku, startovaćemo program i nadati se da neće nastupiti ni jedna takozvana run time greška.

Većina bejzik interpretatora (izuzetak su Sinklerove mašine) ne pravi veliku razliku između sintaksnih grešaka i problema sa kojima se računar sreće izvršavajući program - i jedne i druge greške se prijavljuju tek posle kucanja komande RUN. U C-u se, kao što smo videli, sintaksne greške prijavljuju u toku prevođenja i linkovanja programa dok se u toku izvršavanja prijavljuju samo greške koje nisu mogle da se zapaze ranije: deljenje sa nulom, prekoračenje dimenzija matrica, izračunavanje matematički nedefinisane funkcije i tome slično. Za razliku od paskala koji uredno prijavljuje sve moguće greške, većina C kompajlera je prilično liberalna: ponekad će se čak dogoditi da 1/0 bude nula! Razlog za ovu nemarnost je želja da se rezultujući kod ne opterećuje i ne usporava raznoraznim mašinskim IF-ovima i porukama. Bolji C kompajleri omogućavaju da se, navođenjem odgovarajućih opcija pri prevođenju, provocira generisanje koda koji prijavljuje run time greške; ovome, jasno, pribegavamo samo u toku razvoja i testiranja softvera dok ćemo u konačnoj verziji umanjiti strogost nadajući se da problema neće biti.

Da vidimo... (Ili Let's C)

Kažu da je najteže početi što znači da tekstovi poput ovoga treba da obezbede što bezbolniji start. Zato ćemo ovo poglavlje posvetiti jednom sasvim neobaveznom pogledu na C - upoznaćemo osnovnu strukturu programa i nekoliko opštih pravila koja je C preuzeo iz algola i paskala. Nadamo se da će vas sledeći redovi podstaći da otkucate program koji dajemo i da se malo zabavljate menjajući ga - to je izvanredan uvod u napore koji nas očekuju.

Pogledajmo, dakle, jedan sasvim jednostavan bejzik program i njegov C ekvivalent. Na slici 3.1 vidimo bejzik program koji sortira N stringova po abecedi dok je na slici 3.2 ovaj program preveden na C.

slika 3.1:
10 REM
20 REM  Sortiranje stringova
30 REM
40
50 maxn=100
60 DIM niz$(maxn)
70 INPUT "Koliko stringova"; n
80 GOSUB 160
90 PRINT "Nesortirani stringovi:"
100 GOSUB 210
110 GOSUB 260
120 PRINT "Sortirani stringovi:"
130 GOSUB 210
140 END
150
160 FOR i=1 TO n
170     INPUT niz$(i)
180 NEXT i
190 RETURN
200
210 FOR i=1 TO n
220     PRINT niz$(i)
230 NEXT i
240 RETURN
250
260 FOR i=1 TO n-1
270     FOR j=i+1 TO n
280         IF niz$(i)<niz$(j) THEN 320
290            temp$=niz$(i)
300            niz$(i)=niz$(j)
310            niz$(j)=temp$
320     NEXT j
330 NEXT i
340 RETURN
slika 3.2:
#define maxn 100 #define maxs 256
main ()
/* Sortiranje N stringova */
{
   char niz [maxn] [maxs];
   int  n;
   printf ("%s\n","Koliko stringova?");
   scanf ("%d", &n);
   ulaz (niz,n);
   printf ("Nesortirani stringovi:\n");
   ispis (niz, n);
   sortirka (niz, n);
   printf ("Sortirani stringovi:\n");
   ispis (niz, n);
}
ulaz (niz, nclan)
char niz [maxn] [maxs]; int  nclan;
{
   int i;
   for (i=1; i<=nclan; ++i)
   scanf ("%s", niz[i]);
}
ispis (niz,nclan) char niz [maxn] [maxs]; int  nclan;
{
   int i;
   for (i=1; i<=nclan; ++i)
   printf ("%s\n", niz[i]);
}
sortirka (niz, nclan) char niz [maxn] [maxs]; int  nclan;
{
int  i, j;
char temp[maxs];
   for (i = 1; i < nclan; ++i)
   for (j = i + 1; j <= nclan; ++j)
   if (strcmp (niz[i], niz[j]) > 0)
   {
      strcpy (temp,   niz[i]);
      strcpy (niz[i], niz[j]);
      strcpy (niz[j], temp  );
   };
}

Ako je ovo prvi ne-bejzik program koji gledate, primetićete brojne razlike. Prva od njih je, pomalo paradoksalno, najmanje važna: bejzik programi se pišu velikim a C programi malim slovima. Mnogi moderni računari omogućavaju ravnopravno korišćenje velikih i malih slova u bejzik programima ali većina korisnika nastavlja da VIČE NA RAČUNAR VELIKIM SLOVIMA. C kompajleri, sa druge strane, podržavaju velika i mala slova pri čemu se ova slova međusobno razlikuju: BROJ, broj i BrOj su tri različite promenljive. Uobičajeno je, međutim, da se programi na C-u pišu malim slovima - sa pravom se smatra da je ovakav tekst prijatniji za čitanje. Iako neki autori predlažu da se definicije konstanti pišu velikim a ostatak programa malim slovima, u ovom ćemo umetku sve bejzik programe pisati velikim a sve C programe malim slovima - tako je verovatnoća zabune svedena na najmanju moguću meru.

Mnogo bitnija razlika je nedostatak linijskih brojeva koji su postali sasvim nepotrebni čim su programi prestali da se "saopštavaju" direktno kompajleru: editor teksta omogućava jednostavno umetanje i premeštanje linija što znači da bi njihovo dodatno numerisanje predstavljalo nepotreban balast.

Svakako ste primetili da sve naredbe sa slike 3.2 ne počinju od početka reda: kažemo da je program "nazubljen". Nazubljivanje, u stvari, nije neophodno: sve bi bilo u redu da smo naredbe pisali od prve pozicije u redu. Ipak, uvlačenjem pojedinih redova vizuelno izdvajamo celine u programu što doprinosi njegovoj čitljivosti. Uobičajeno je da se u okviru svake celine naredbe uvlače bar za tri blanko simbola.

Znamo da se dve bejzik naredbe u istom redu razdvajaju dvotačkom (ređe obrnutom kosom crtom ili "majmunskim znakom"); u C-u se svaka naredba (čak i poslednja u redu) završava tačkom i zarezom. U prvim danima navikavanja na C ćete redovno izostavljati neophodni terminator što će rezultirati najčudnijim porukama o greškama koje će kompajler ispisivati. Za utehu može da vam posluži činjenica da je na ovaj način omogućeno da se jedna naredba prostire u više redova što u "Računarima" ponekad koristimo u cilju skraćivanja predugih linija.

Neophodne deklaracije...

Sledeći novitet su deklaracije: na C-u morate da ispišete gomilu redova pre nego što nešto stvarno počne da se događa. Prva linija zapravo ne pripada programu: radi se o takozvanoj makrodefiniciji koja predstavlja uputstvo kompajleru; naredili smo, konkretno, da reč MAXN ubuduće bude zamena za broj 100. Sledeća linija označava da je ono što pišemo glavni program (main) a ne neki od potprograma; zagrade iza main govore da glavni program nema nikakve argumente (uskoro ćemo naučiti da pišemo programe koji primaju argumente iz komandne linije što znači da možemo da ih startujemo sa ime a1 a2 ... aN).

Sledeća linija našeg programa je komentar - dok se u bejziku komentari pišu iza REM-ova, u C-u se oni započinju sa /* a završavaju sa */. Komentar, kao što vidimo, može da bude dugačak nekoliko linija pri čemu svaku od njih ne moramo posebno da uokvirimo sa /*...*/.

Ostatak programa sa slike 3.2 je uokviren velikim zagradama koje, za one koji znaju paskal, predstavljaju zamenu za službene reči begin i end; ukoliko se nikada niste bavili paskalom, jednostavno zanemarite ove zagrade - njihov će vam smisao uskoro biti sasvim očigledan. Prvi redovi iza otvorene velike zagrade predstavljaju deklaracije ili, preciznije rečeno, definicije promenljivih (razliku između deklaracija i definicija ćete razumeti tek u sedmom poglavlju). U bejziku, naime, možemo slobodno da uvodimo promenljive kako nam koja od njih zatreba. U C-u, sa druge strane, svaka promenljiva mora da se definiše na samom početku programa: treba navesti njeno ime i tip. Kada malo bolje razmislite, i bejzik zahteva neke definicije: vektor ili matricu pre korišćenja treba pomenuti iza DIM dok neki interpretatori poznaju i deklaracije DEFINT, DEFDBL i slične.

Šta je dobijeno a šta izgubljeno uvođenjem deklaracija? Pre svega, C nas nateruje da pre kucanja program skiciramo na papiru što se obično smatra dobrom praksom koja bi trebala da proredi bagove. Drugi dobitak je pojačano obezbeđenje od "tipfelera": ako ime promenljive granica pogrešno otkucamo kao gramica, kompajler će se pobuniti pa će nas jedan ulazak u editor osloboditi greške koja bi na bejziku često rezultirala programom koji bi radio bez greške ali davao pogrešne rezultate.

Ostalo je da pomenemo činjenicu da ime promenljive više nije povezano sa njenim tipom: dok u bejziku ime alfanumeričke promenljive mora da se završava znakom dolar a ime celobrojne procentom, na C-u tip promenljive zavisi samo od njene deklaracije!

Većina modernih bejzika omogućava da imena promenljivih budu proizvoljno dugačka pa se programerima često savetuje da biraju slikovita višeslovna imena kako bi se program docnije lakše razumevao i modifikovao. Programeri koji su na ratnoj nozi sa brzim kucanjeme imaju dobar argument protiv toga: duža imena promenljivih i brojni komentari usporavaju izvršavanje programa i povećavaju utrošak memorije. Iako bi bolji bejzik interpretator trebao da minimizira usporenje, ovakav argumenat je u suštini dobro smišljen. Na C-u nije tako!. Pri prevođenju programa komentari se ignorišu dok se promenljive zamenjuju ukazateljima konstantne dužine - broj slova u imenu ili komentar nemaju baš nikakvog uticaja na dužinu ili vreme izvršavanja prevedenog programa! Svi udžbenici i kursevi programiranja preporučuje korišćenje ilustrativnih imena i dodavanje komentara što znači da bi naš program sa slike 3.2 bio prilično loše ocenjen kao prepisivanje bejzika na C.

... i izvršni deo

Prvi novitet koji ćete primetiti u izvršnom delu programa je da naredbe praktično ne postoje: dok većina programskih jezika pravi oštru razliku između instrukcija i poziva potprograma, C program se, sa izuzetkom aritmetičkih izraza, sastoji od samih poziva procedura. Na nekom bismo "normalnom" jeziku, dakle, napisali for i:=1 to n, na C-u pišemo for (i=1; i<n; i=i+1) - kao da je for neka procedura sa tri argumenta! Konstruktori C-a su verovatno želeli da, unificirajući kontrolne strukture, učine kompajler jednostavnijim i bržim.

Ukoliko se do sada niste bavili paskalom i sličnim jezicima, C će vas iznenaditi još jednom konvencijom: bilo koja procedura, funkcija, petlja ili neka druga struktura uključujući i sam glavni program se sastoji od jedne jedine naredbe. Jedna naredba je, jasno, sasvim nedovoljna da bi se obavilo bilo šta ozbiljno pa se umesto neke izvršne rečenice koristi par velikih zagrada, potpuni ekvivalent paskalove begin ... end konstrukcije. Kada C kompajler analizira neku strukturu i očekuje izvršnu naredbu, pronađena otvorena velika zagrada će izazvati izvršavanje svih naredbi do odgovarajuće zatvorene zagrade. Glavni program sa slike 3.2 se, na primer, sastoji od nekoliko deklaracija i nekoliko izvršnih naredbi koje smo uokvirili zagradama. Pogledajmo, dalje, THEN deo IF naredbe iz procedure sortirka: tri izvršne naredbe su okružene zagradama dok čitava IF-THEN konstrukcija nije - naredba IF je ugnježdena u for petlju koja ima samo jednu izvršnu naredbu; zagrade su nepotrebne ali ne bi ni smetale. FOR petlja je, jasno, deo potprograma sortirka koji je, poput svake strukture, uramljen zagradama.

Što se potprograma tiče, program sa slike 3.2 predstavlja dobar primer njihove fleksibilnosti: potprogram pozivamo prostim navođenjem imena i spiska argumenata. C, za razliku od paskala, ne pravi naročitu razliku između funkcija i procedura - svaka struktura po potrebi može da se upotrebi na oba načina. Detaljniju diskusiju procedura i funkcija ostavljamo za sedmo poglavlje; sada ćemo se pozabaviti samo neophodnim ugrađenim procedurama printf i scanf.

Ne treba biti veliki poznavalac kompjutera da bi se zaključilo da printf nalaže ispisivanje na ekran (ono 'f' naglašava da je ispisivanje formatirano) a scanf očitavanje tastature. Argumente funkcija scanf i printf ćemo najbolje razumeti na primeru sa slike 3.3.

slika 3.3:
main ()
{
int i;
float a,b;
char  tekst [80];
   i = 703;
   a = 203.78902;
   b = a;
   strcpy (tekst, "Niz znakova");
   printf ("%d  %f %8.3f\nString podatak: %s\n", i, a, b, tekst);
   scanf ("%d", &i);
   printf ("Kontrolno ispisivanje: i=%d\n", i);
}

Prvi argument funkcije printf je format koji opisuje način na koji će vrednosti ostalih promenljivih biti ispisane. Sledi, kao i u bejziku, lista promenljivih razdvojenih zarezima. Svaka promenljiva iz ovog spiska mora da bude opisana tačno jednom stavkom formata; svaka stavka započinje znakom za procenat.

Program sa slike 3.3 najpre ispisuje vrednost celobrojne promenljive I koja je u formatu opisana sa %d pri čemu slovo D označava ceo broj. Vrednost racionalne promenljive A je opisana kao %f dok se B ispisuje sa %8.3f. Oznaka 8.3 će u trenutku biti jasna svima koji poznaju fortran premda bi i ostali trebali da naslute njen smisao: vrednost racionalnog broja će zauzeti osam mesta na ekranu pri čemu se ispisuju tačno tri decimala.

Posle opisa promenljivih I i A format sadrži \n što označava prelazak u novi red. Sledi nekoliko znakova koji se direktno prepisuju na ekran i opis poslednje, alfanumeričke promenljive tekst; stringovi se, kao što i priliči, opisuju sa %s.

Veoma je važno da zapamtite da elementi formata moraju da odgovaraju listi promenljivih po broju i redosledu. C kompajleri, naime, u suštini ne podržavaju procedure koje imaju promenljiv broj argumenata pa je kod printf i scanf pribegnuto malom triku: kompajler na osnovu prvog elementa liste (formata) "zna" koliko će elemenata lista imati. Pogrešan broj argumenata može da izazove vrlo neprijatne posledice koje, na nekim kompajlerima, mogu da izazovu i potpun pad sistema. Pogrešan redosled će, sa druge strane, izazvati samo neželjen ispis; ponekad ćete čak namerno ispisivati neke vrednosti u neodgovarajućim formatima da biste postigli razne "specijalne efekte".

Što se procedure scanf tiče, slika 3.3 će vam biti sasvim dovoljna: format se priprema kao za printf. Treba, međutim, da primetimo da imenima promenljivih prethodi znak & (and). Razloge za ovu anomaliju ćete shvatiti tek kroz izvesno vreme; ako ste baš mnogo nestrpljivi, reći ćemo da & označava prenos parametra po imenu.

slika 3.4:
/* Ne radi na Lattice C kompajleru! */
main ()
{
int a;
   input (&a, "Unesi a = ");
   printf ("Kontrolno ispisivanje: a=%d\n", a);
}
input (ime, prompt) int  ime; char prompt [256];
{
   printf ("%s", prompt);
   scanf ("%d", ime);
}

Ostalo je još da pogledamo sliku 3.4 koja simulira staru, dobru INPUT naredbu: na ekranu se ispisuje prompt i računar čeka da korisnik otkuca neku vrednost. Treba da kažemo da program 3.4 ne radi korektno na Lattice C kompajleru koji smo koristili - format printf funkcije mora da se dopuni jednim prelaskom u sledeći red. Nije nam poznato da li ovu anomaliju treba pripisati našoj AT konfiguraciji ili kompajlerskom bagu.

Tipovi i deklaracije

Videli smo da sve promenljive koje ćemo koristiti u nekom C programu moraju da se definišu: definicija se sastoji od imena promenljive, njenog tipa i (po potrebi) početne vrednosti. Što se imena tiče, stvar je sasvim jednostavna: poslužiće bilo koja reč od najviše osam slova, cifara i znakova za podvlačenje čiji je prvi znak slovo. Imena promenljivih, da budemo precizniji, mogu da budu praktično proizvoljno dugačka pri čemu većina kompajlera uzima u obzir samo prvih osam znakova: promenljive računari_1 i računari_10 su potpuno identične. Ne verujemo, međutim, da će vam ovo ograničenje smetati: osam slova je sasvim dovoljno da vam kucanje presedne! Slika 4.1 prikazuje nekoliko korektno napisanih imena promenljivih kao i nekoliko pogrešnih - verujemo da će ovi primeri, dopunjeni vašim poznavanjem bejzika, biti sasvim dovoljni da bez problema imenujete konstante, promenljive, matrice i delove programa.

slika 4.1:
Korektna imena Nekorektna imena
a 7dana
a650161 a650-161
učitavanjevrednosti učitavanje vrednosti
dve_reči for
sumaA0Z9 int
LujXIV Luj XIV

Poput bejzika, C ne dopušta da imena promenljivih počinju rezervisanim rečima: pokušajte da nazovete neku bejzik promenljivu STOP! Slika 4.2 prikazuje službene (bolje reći rezervisane) reči C-a - ukoliko neku od njih iskoristite kao ime promenljive, sva je prilika da će vas kompajler pozdraviti nekom čudnom porukom!

slika 4.2:
Rezervisane reči C-a
auto		do		for		return		typedef
break	  	double		goto		short		union
case		else		if		sizeof		unsigned
char		entry		int		static		while
continue	extern		long		struct
default		float		register	switch

Pošto smo videli da je sintaksa imena promenljivih sasvim uobičajena i jednostavna, pozabavimo se tipovima. Videćemo da se C veoma razlikuje od paskala, jezika koji je stekao popularnost zahvaljujući ponajviše moćnim strukturama podataka - C poznaje samo cele i racionlane brojeve ali se njihovim kombinovanjem može postići mnogo što-šta. Pođimo redom i upoznajmo najjednostavnije, skalarne tipove.

Skalarni tipovi

Skalarne promenljive su celobrojne, racionalne i alfanumeričke, baš kao u modernijim bejzicima. Celobrojna i racionalne promenljive su, međutim, podeljene na kratke i duge pa ćemo ih na taj način i razmatrati.

"Obična" celobrojna promenljiva se deklariše kao int ili, ređe, short int i "pamti" isključivo ceo šesnaestobitni broj - raspon vrednosti celobrojnih promenljivih je, dakle, (-32768, 32767). Long int je, pogađate, deklaracija trideset dvobitnih celih brojeva čije se vrednosti nalaze između -2147483648 i 2147483647. Unsigned int, najzad, označava ceo pozitivan broj između 0 i 65535 (vrednosti ovih granica mogu da variraju od kompajlera do kompajlera). Ukoliko ste ortodoksni bejzik programer, celobrojne promenljive mogu da vam donesu dosta problema: ako su I i J celobrojne promenljive, I/J će takođe biti ceo broj što znači da je 7/3=2. U programiranju je, međutim, mnogo brojača i sličnih kontrolnih veličina koje su po prirodi celobrojne što znači da ćete se postepeno navići da ubrzavate svoje programe koristeći intidžere.

Racionalne promenljive su najbliže onome na šta nas je bejzik navikao: u standardnom su bejziku sve promenljive racionalne. Racionalne promenljive se deklarišu korišćenjem službenih reči float i double - float najčešće označava 7 a double četrnaest uslovno rečeno tačnih cifara. Opseg racionalnih brojeva zavisi od implementacije C-a; obično se apsolutne vrednosti brojeva različitih od nule nalaze približno u intervalu (1E-38, 1E+38).

Logičke promenljive oficijelno ne postoje ali se lako zamenjuju celobrojnima - uobičajeno je da se true označava jedinicom a false nulom. Čim upoznamo makro naredbe, ove ćemo konstante i sami simbolički definisati pa će reči true i false postati ekvivalentne zamene za brojeve 1 i 0.

Alfanumerička promenljive se deklariše sa char i "pamti" samo jedan ASCII znak. Često će nam, jasno, biti potrebno da nekoj alfanumeričkoj promenljivoj dodelimo čitavu rečenicu; u takvom ćemo slučaju koristiti pointere ili matrice: tekst je, zapravo, niz slova pa ćemo ga kao takvog i memorisati.

slika 4.3:
#define formf '\014'
main ()
{
short int  i, j, brojac;
long  int  ulaz;
float      duzina, sirina;
double     pi;
char       zn1, zn2, tekst [80];
   i = 0;
   j = 12;
   brojac = 77;
   ulaz   = 100000L;
   duzina = 20.0;
   sirina = 15.6e3;
   pi     = 3.1415926543;
   zn1    = 'A';
   zn2    = formf;
/* Sledeće naredbe treba da provere vaš kompajler:
da li su zaista podrzani "long" i "double"? */
   printf ("ulaz     = %d\n",      ulaz);
   printf ("pi       = %12.10f\n", pi  );
   printf ("ASC(zn2) = %d\n",      zn2 );
}

Program 4.3 je dobra prilika da utvrdimo ono što smo naučili: njegove prve linije sumiraju čitav prvi deo ovoga poglavlja. Službena reč main, kao što znamo, označava početak programa koji nema nikakve argumente. Main-u, međutim, prethode nekakve define linije koje opisuju konstante. Šta li su to konstante? Pišete, na primer, program koji sortira brojeve i odlučite da će tih brojeva biti najviše 1000. Posle izvesnog vremena, zaključujete da brojeva ponekad ima i više pa odlučite da povećate maksimalnu dimenziju na 2000. Nema druge nego da pretražite ceo program i zamenite 1000 sa 2000; pazite, ipak, da vam ne promakne neka naredba tipa IF (i=999) .... Alternativa je uvođenje promenljive koju ćete, na primer, nazvati MAXN i kroz čitav program koristiti kao zamenu za broj 1000; docnije povećanje niza se svodi na promenu naredbe MAXN=1000. Još su tvorci paskala zaključili da MAXN u ovom slučaju nije promenljiva pošto se njena vrednost ne menja u toku izvršavanja programa: MAXN je zapravo konstanta! Zato je prvi deo svakog paskal programa sekcija konstanti. Konstruktori C-a su otišli korak dalje: možemo da definišemo ne samo zamenu za broj nego i zamenu za bilo koji drugi tekst; ako nas funkcija scanf nervira, program ćemo započeti sa #define input scanf pa ćemo ubuduće umesto scanf pisati input. Makro naredbe mogu i da se proprate parametrima ali se sličnim pikanterijama ovoga puta nećemo baviti. Kada, međutim, počnete ozbiljno da programirate na C-u, svaki ćete program započinjati uvođenjem konstanti tj. parametara programa koje ima smisla menjati u raznim situacijama; posle svake promene neke od konstanti program se, jasno, mora nanovo kompajlirati. Možda ćete s vremenom formirati i svoja specijalizovana zaglavlja programa koja ćete držati na disku; uobičajeno je da se ova zaglavlja čuvaju u datotekama tipa .H pa je i vaš C kompajler dopunjen sa nekoliko standardnih zaglavlja koja ćemo u budućnosti često koristiti.

Komunikacija sa periferijom a posebno štampačem zahteva slanje pojedinih escape sekvenci tj. znakova čiji su ASCII kodovi manji od 32. C u takvim prilikama koristi obrnutu kosu crtu iza koje sledi bilo koji trocifren oktalni broj čija se vrednost umeće direktno u string. Na slici 4.3 smo definisali sekvencu za pomeranje papira na početak nove strane.

Prvi pravi odeljak programa sa slike 4.3 predstavlja sekciju promenljivih. Primećujemo da su imena svih definisanih promenljivih ravnopravna: nema nikakve potrebe (a ni mogućnosti) da imena celobrojnih završavamo znakom za procenat a alfanumerike dolarom. Ispred svake liste imena je smeštena jedna od službenih reči int, float, double ili char čime je promenljiva detaljno opisana dok se svaki red završava tačkom i zarezom.

Pre nego što počnemo sa opisom ostalih tipova podataka, izvucimo dva zaključka iz programa 4.3. Prvi (i najvažniji) zaključak je da sve korišćene promenljive moraju da se deklarišu: čak i najnevažniji brojač mora da bude opisan. Deklarisanje promenljivih nas praktično tera da program najpre koncipiramo na papiru: kucanje "direktno u računar" jednostavno ne odgovara filozofiji C-a. Često će vam se, naravno, događati da u toku ispravljanja programa uvedete neku novu promenljivu i zaboravite da označite njen tip; kompajler vam to neće oprostiti!

Program 4.3, sa druge strane, pokazuje da nas kompajler ne nateruje da iskoristimo sve promenljive koje smo deklarisali: alfanumerička promenljiva se ne javlja nigde u programu! Jasno je, sa druge strane, da ovakva praksa nije naročito dobra jer prostor predviđen za nekorišćenu promenljivu jednostavno propada!

Nizovi i matrice

Skalarne promenljive nam, jasno, nisu dovoljne - treba da naučimo da dimenzionišemo nizove i matrice. Iako C dosledno podržava ove neophodne strukture, videćemo da se rad sa nizovima zapravo svodi na operacije sa pointerima.

slika 4.4:
#define maxn 1000 main () {
int niz [maxn];
int i, j, max;
   i=0; niz [i] = 777;
   while (niz [i] > 0)
   {
      i=i+1;
      scanf ("%d", &niz [i]);
   }
   max = -1;
   for (j=1; j < i; j=j+1)
   {
      if (niz [j] > max)  max = niz [j];
   }
   printf ("Najveći broj je %d.", max);
}

Program sa slike 4.4 treba da učita sa tastature nekoliko pozitivnih brojeva, smesti ih u niz i odredi najveći od njih. Iako ćemo se kontrolnim strukturama baviti tek u sledećim poglavljima, verujemo da ćete začas zaključiti da je primenjeni algoritam trivijalan - for petlja je, na kraju krajeva, vaš stari poznanik!

U ovom nas trenutku posebno interesuje deklaracija int podatak[maxn] - ona opisuje niz podatak koji ima MAXN elemenata (indeksi, kao i u bejziku, počinju od nule). Primetimo da se indeksi stavljaju između srednjih a ne, kao u bejziku, malih zagrada; razlog za ovaj novitet je verovatno činjenica da se procedure pozivaju prostim navođenjem imena (bez nekog PROC ili CALL) pa treba razlikovati argumente funkcije od indeksa niza. C, očito, koristi male, srednje i velike zagrade što znači da programiranje na njemu ume da bude veoma neprijatno ukoliko vaš računar nema profesionalnu i potpunu tastaturu.

O referenciranju elemenata nizova jedva da vredi govoriti: navodimo ime, otvaramo zagradu i pišemo izraz koji predstavlja indeks. Ne smemo, jasno, da zaboravimo da se koriste srednje a ne male zagrade; previd će dovesti do neke čudne kompajlerske poruke.

slika 4.5:
/* Problematično uz Lattice C kompajler -
koristite cele a ne float brojeve! */
#define maxx 2 #define maxy 2
main ()
{
float a [maxx] [maxy],x;
int i,j;
   printf ("Unesi elemente matrice %d*%d\n", maxx, maxy);
   for (i=1; i<=maxx; i=i+1)
      for (j=1; j<=maxy; j=j+1)
         scanf ("%f", &a [i] [j]);
   printf ("Unesi skalar X = ");
   scanf ("%f", &x);
   for (i=1; i<=maxx; i=i+1)
      for (j=1; j<=maxy; j=j+1)
         a [i] [j] = a [i] [j] * x;
   for (i=1; i<=maxx; i=i+1)
      for (j=1; j<=maxy; j=j+1)
         printf ("A [%d,%d]=%f\n", i, j, a [i] [j]);
   exit(0);
}

Program 4.5 po prvi put uvodi višedimenzione nizove - matrice. Rešili smo sasvim jednostavan problem: unošenje matrice M*N, njeno množenje skalarom X i (ne baš doterano) ispisivanje rezultata. Rad sa matricama je sasvim sličan onome na šta su nas bejzik i paskal navikli ali uz jednu veliku razliku: umesto da pišemo a[i,j] moramo da napišemo a[i][j]. Ovakvo referenciranje je verovatno pojednostavilo kompajler koji matricu ne "zamišlja" kao pravougaonu strukturu od m*n ćelija već kao niz od M kolona pri čemu se svaka kolona sastoji od N brojeva. Tu je baš i smisao definicije: A je niz čiji se indeksi kreću od 0 do MAXX. Šta su elementi toga niza? To nisu brojevi nego novi nizovi; indeksi svakog od ovih nizova idu od 0 do MAXY a elementi niza su racionalni brojevi. Na sličan bismo način mogli da uvedemo i trodimenzione, četvorodimenzione i ostale nizove ali bi nas prečesto kucanje zagrada svakako iznerviralo - šteta što C ne može bar za toliko da imitira bejzik, fortran ili paskal!

slika 4.6:
10 REM Sortiranje podataka
20 INPUT "Koliko podataka"; N
30 DIM A(N), POMOCNI(2*N)
40 FOR I=1 TO N
50 ...

Pažljiva analiza programa sa slika 4.4 i 4.5 treba da istakne još jednu prednost bejzika: zar program sa slike 4.6 nije sasvim prirodna pojava? Računar, vidimo, pita korisnika koliki je niz i onda izvršava DIM A(N) ili DIM A(N+1)? Nešto slično je na C-u apsolutno nemoguće: maksimalna dimenzija svih statičnih nizova mora da se zada pre prevođenja i izvršavanja programa! Ovakvo ograničenje važi za sve jezike koji se prevode (kompajliraju) i na njega morate da se naviknete iako vam to neće biti jednostavno - mnogi će početnik izgubiti silno vreme razmišljajući o tome koliko elemenata neki niz može da ima i ne izvodeći nikakav pametan zaključak! Predlažemo vam da definišete konstantu MAXN i napišete, bez mnogo razmišljanja, #define maxn 100 ili #define maxn 1000. Ukoliko se docnije bude pokazalo da je ovo malo, lako ćete prevesti program još jednom; i tako ga bezbroj puta prevodite ispravljajući greške!

C i alfanumerici

Upoznali smo promenljive tipa char koje, rekosmo, "pamte" samo jedan ASCII znak koji se, da dodatmo, piše između apostrofa: ako je promenljiva A definisana sa char a, vrednost možemo da joj dodelimo sa a='P'. C, međutim, jasno razlikuje alfanumeričke promenljive i konstante od stringova: string može da bude proizvoljno dugačak niz slova koji se obeležava navodnicima, na primer "Ovo je string". Pošto promenljivoj tipa char možemo da dodelimo samo jedno slovo, za memorisanje stringa ćemo koristiti niz koga ćemo u početku deklarisati kao na slici 4.7.

slika 4.7:
main ()
{
char tekst [80];
int  i;
   strcpy (tekst, "Ovo je primer");
   for (i=0; i<=13; i=i+1)
      printf ("%x  %c\n", tekst [i], tekst [i]);
}

String tekst je deklarisan kao char tekst[80] što znači da može da memoriše najviše 80 znakova. Nekoliko redova docnije ovom je stringu dodeljena vrednost (za to je morala da bude pozvana jedna funkcija; videćemo i zašto) a zatim je njegov sadržaj, slovo po slovo, ispisan na ekranu: svaki element niza sadrži po jedan znak dok je kraj označen sa NUL ili, u bejzik terminologiji, CHR$(0). Program 4.7 je dobra prilika da upoznamo još par svojstava funkcije printf: alfanumerike možemo da ispisujemo i kao cele brojeve (u %d formatu) pa čak i da, kao u našem primeru, sa %x zahtevamo da taj ceo broj bude predstavljen heksadekadno. Svaki je karakter docnije ispisan i u %c formatu tako da jasno zapažamo koja heksadekadna vrednost odgovara kom slovu.

Slika 4.8:
main () {
char *tekst;
int  i;
   tekst = "Ovo je primer";
   for (i=0; i<=13; i=i+1)
      printf ("%x  %c\n", tekst [i], tekst [i]);
}

Program sa slike 4.8 je veoma sličan prethodnom ali se od njega razlikuje u jednom suštinskom detalju: umesto da string deklarišemo sa char tekst[80], napisali smo char *tekst. Zvezdica označava da tekst nije promenljiva u klasičnom smislu nego ukazatelj na neku slobodnu memorijsku zonu. U tu smo zonu, bez potrebe da koristimo funkciju strcpy (koja, uzgred budi rečeno, ne bi ni smetala), upisali neki tekst a zatim ga, kao i u prethodnom primeru, čitali slovo po slovo i ispisivali rezultate. Zanimljivo je da smo, ispisujući znakove, postupali kao da je tekst niz tj. referencirali njegove članove tekst[1], tekst[2] i tako dalje. C zapravo ne pravi gotovo nikakvu razliku između nizova i memorijskih zona na koje ukazuje pointer - videćemo čak da u pozivu procedure možemo da navedemo jednu a u njenoj definiciji drugu strukturu. Savetujemo vam da u početku koristite isključivo nizove na koje ste se svakako navikli - na pointere ćete preći tek kada (i ako) vam C "uđe u krv".

Možda je ovo pravi trenutak da se vratite na sliku 3.2 i pokušate da analizirate program za sortiranje stringova - bio nam je potreban niz alfanumerika koga bismo na Microsoft bejziku deklarisali sa DIM A$(MAXN). Na C-u smo iskoristili dvodimenzioni niz: prvi indeks označava rečenice koje treba sortirati a drugi slova u okviru jedne rečenice. Mogli smo, jasno, da definišemo i niz ukazatelja (sa char *niz[maxn]) ali ovu praksu, kako rekosmo, ostavljamo za neka buduća vremena.

Slogovi

Poput svih modernih jezika, C omogućava programeru da definiše specijalizovane strukture podataka koje su potrebne za rešavanje nekog problema. Iako se C u ovom aspektu ne može meriti sa paskalom, deklaracije struktura zaslužuju određenu pažnju.

Pošto svi dobro znamo da se u našim uslovima veliki i skupi računski centri koriste uglavnom za vođenje finansija i obračun plata, pokušaćemo da prodiskutujemo strukturu podataka neophodnu za ovakve primene.

Datoteka zaposlenih se sastoji od slogova; svaki slog sakuplja podatke o jednom zaposlenom: ime i prezime, pol, radni staž i mnoge druge stvari. Pošto tek učimo C, te "druge stvari" ćemo sasvim pojednostaviti: broj radnih sati i ukupna plata. Slikovito prikazani, podaci o svakom radniku mogu da se prikažu otprilike ovako:

slika 4.9:
 ___________________________________________________________________
|        |           |     |      |               |                 |
|   Ime  |  Prezime  | Pol | Staž | Radnih časova |  Lični dohodak  |
|________|___________|_____|______|_______________|_________________|

Kako bismo slogove sa slike 4.9 obrađivali na bejziku? Nema druge nego dimenzionisati matricu imena, prezimena, polova, staževa, sati i plata pa svakom zaposlenom dodeliti po jedan element svake od matrica. Iako bi poslužio, ovaj način nije naročito pregledan: podaci koji po prirodi predstavljaju celinu (vezani su za jednog radnika) su rascepkani na više mesta što svakako neće pomoći nekome ko docnije bude modifikovao naš bejzik program.

Dobro, bejzik se i onako ne koristi za rad sa datotekama. Da vidimo kako bismo opis sa slike 4.9 "prepisali" na C.

slika 4.10:
#define duz_im 30 #define broj_radnika 100
main()
{
struct radnik {
   char  ime [duz_im];
   char  prezime [duz_im];
   int   pol;
   int   staz;
   int   sati;
   float plata;
} ;
struct radnik zaposleni [broj_radnika];
   zaposleni [3].staz = 30;
   printf ("Kontrolni ispis: %d", zaposleni [3].staz);
}

Definisali smo tip radnik kao strukturu (struct) koja se sastoji od polja prezime, ime, pol, staž, sati i plata; svako od ovih polja je deklarisano na odgovarajući način što znači da u svakom zapisu imamo dve alfanumeričke matrice, tri cela i jedan racionalan broj. Definicija sloga se, kao i sve druge strukture u C-u, uokviruje velikim zagradama dok se njeni elementi završavaju terminatorom ';'.

Šta da radimo sa zapisom koji smo definisali? Odgovor ponovo pruža slika 4.10: uveli smo niz zaposleni koji se sastoji od najviše 100 slogova radnik - koristimo, dakle, činjenicu da elementi matrice mogu da budu ne samo brojevi već i komplikovane strukture.

C nije neko svetsko čudo kada se radi o operacijama sa slogovima: operacije se svode na pristup pojedinačnim poljima. Kao što za pristupanje elementima matrice koristimo srednje zagrade tako za pristup pojedinim poljima koristimo tačku: ako treći zaposleni ima 30 godina staža, napisaćemo nešto poput zaposleni[3].staž = 30. Prenošenje slogova u potprograme se svodi na (neprijatne) operacije sa pointerima što znači da ove komplikovane strukture nećete baš previše koristiti.

DATA liste u C-u

Deklaracije su vam sigurno već dosadile - krajnje je vreme da počnemo da pišemo izvršne programe. Dopustite nam, međutim, da još nekoliko redova potrošimo na predstavljanje starog poznanika - DATA liste.

Za razliku od paskala, C retko prijavljuje run time greške kada se iščitava korektno deklarisana promenljiva kojoj u ranijem toku programa nije dodeljena vrednost. Ponekad ćete poželeti da nekim promenljivima dodelite početne vrednosti i tako izbegnete dug niz aritmetičkih izraza na početku programa; princip tog dodeljivanja daje slika 4.11.

slika 4.11:
main () {
struct datum {
   int dan;
   int mes;
   int god;
};
static struct datum kalendar = { 24, 3, 1987 };
static struct datum nova_god = {  1, 1, 1988 };
int    praz_dana = 7;
float  godina = 365.23;
   printf ("%d.%d.%d.\n", kalendar.dan, kalendar.mes, kalendar.god);
   printf ("%d\n", praz_dana);
   printf ("%9.3f\n", godina);
}

Program 4.11 je gotovo trivijalan - početne vrednosti definišemo navodeći znak jednakosti i vrednost iza imena promenljive. Obratite pažnju da vrednosti elemenata strukture stavljamo između velikih zagrada i razdvajamo zarezima. Što se deklaracije static tiče, razumećemo je kada budemo govorili o potprogramima.

Aritmetički izrazi

Obično se u Školama poput ove lako izlazi na kraj sa aritmetičkim izrazima - kažemo da se, uz par sitnih izuzetaka, pišu kao i na bejziku. U dosadašnjim primerima smo, zaista, koristili određenu količinu aritmetičkih izraza i svi su oni odgovarali sintaksi našeg dragog bejzika. Korišćenje bejzik izraza, međutim, predstavlja strahovitu degradaciju za C - ako ste pažljivo pročitali uvodno poglavlje znate da C kompajler proizvodi kratak i brz kod zato što je optimizator zamenjen savršenijom spravom - čovekom. Ukoliko, dakle, želite da vaši programi budu po karakteristikama slični mašincu, moraćete da naučite da pišete sasvim drugačije aritmetičke izraze!

Počnimo od najjednostavnijeg izraza A=B kojim se vrednost B prenosi u A. Dok na bejziku jedna naredba izaziva jednu dodelu vrednosti, na C-u možemo da napišemo nešto poput A=B=C=0. Ovakav se izraz analizira s desna pa se promenljivoj C dodeljuje vrednost nula. Kompajler zatim ovu vrednost dodeljuje redom promenljivoj B i najzad promenljivoj A. Zvuči jednostavno ali...

Kako biste opisali dejstvo naredbe X=Y-Z=0? Najpre promenljiva Z dobija vrednost nula (čime se njen raniji sadržaj gubi) a zatim se ta vrednost oduzima od Y da bi se dobila vrednost X; naredba X=Y-Z=0 je, dakle, uglavnom ekvivalentna sa Z=0: X=Y. U okviru bilo kog aritmetičkog izraza na C-u možete proizvoljan broj puta da dodeljujete vrednosti što će vas u početku veoma zbunjivati: koji broj gde figuriše i koja se vrednost kada gubi? Mehanizam zadugo nećete razumeti do kraja; to, međutim, ne treba da vas spreči da eksperimentišete!

slika 5.1:
Dodela vrednosti
=
a = b
Sabiranje
+
a = b + c
Oduzimanje
-
a = b - c
Množenje
*
a = b * c
Deljenje
/
a = b / c
Ostatak pri deljenju
%
a = b % c
Inkrementiranje
++
a++, ++a
Dekrementiranje
--
a--, --a
Konjukcija (AND)
&
a = b & 0xFF00
Disjunkcija (OR)
|
a = b | 0xFFFF
Negacija (NOT)
~
a = ~b
Ekskluzivna disjunkcija (XOR)
^
a = b^c
Shift udesno
>>
a = b >> 3
Shift ulevo
<<
a = b << 3
Adresa promenljive (VARPTR)
&
a = &b
Sadrzaj memorije (PEEK, POKE)
*
*c= "Tekst"
Uvećavanje sadržaja
+=
a += 3
Umanjivanje sadržaja
-=
a -= 3
Množenje sadržaja
*=
a *= 3
Deljenje sadrzaja
/=
a /= 2
Ostatak pri deljenju
%=
a %= 3
Konjukcija
&=
a &= 0xFF00
Disjunkcija
|=
a |= 0x00FF
Ekskluzivna disjunkcija
^=
a ^= 0xFF00
Shift sadržaja udesno
>>=
a >>=3
Shift sadržaja ulevo
<<=
a <<=3

Tabela 5.1 opisuje sve operatore pomoću kojih delujemo na cele brojeve. Četiri osnovne računske radnje se, kao što vidimo, malo razlikuje od bejzika i paskala: paskal je čak precizniji od C-a jer poseduje različite operatore za deljenje celih i racionalnih brojeva dok se na C-u u oba slučaja koristi kosa crta. C je, najzad, sličan paskalu utoliko što ne poseduje operator za stepenovanje celih niti racionalnih brojeva.

Slika 5.1, međutim, uvodi i mnoštvo novih operatora koje ćemo postepeno objašnjavati. Tu su, pre svega, unarni operatori za povećavanje i smanjivanje sadržaja promenljivih. Koliko ste samo puta napisali i=i+1 što je najverovatnije postajalo MOV i,R0: ADD R0,#1: MOV R0,i ili, u gorem slučaju, prevođeno na RPN a zatim izračunavano uz simulaciju steka. C naredba i++ će, sa druge strane, biti prevedena u prosto INC i dok će i-- postati DEC i. Da bi stvar bila posebno zgodna, možete da inkrementujete (dekrementujete) brojač istovremeno radeći nešto drugo, kao na slici 5.2.

slika 5.2:
#define elem 5
main () {
int niz1 [elem], niz2 [elem], i;
   for (i=0; i<elem; i++)
   scanf ("%d", &niz1 [i]);
   i = 0;
   while (i<elem)
   niz2 [i] = niz1 [i++];
   printf ("Prepisani podaci:\n");
   for (i=0; i<elem; i++)
   printf ("%d\n", niz2[i]);
}

Program 5.2 prepisuje pet elemenata niza1 u niz2 koristeći sasvim uobičajenu WHILE petlju. Sve je logično ali se brojač i nigde eksplicitno ne povećava. Pogledajmo, međutim, naredbu niz2[i]=niz1[i++] koja prenosi i-ti elemenat niza1 u niz2 i istovremeno povećava i za jedan. Šta bi se dogodilo da smo napisali niz2[i]=[niz1[++i]? Brojač bi se povećao pre nego što bi bio referenciran što znači da bi prvi element niza1 bio prepisan u nulti element niza2 (pokušajte!). Ostavljamo da sami pogodite kako bi delovale naredbe niz2[i++]=niz1[i], niz2[++i]=niz1[i] ili niz2[i++]=niz1[i++]. Bolji poznavaoci šesnaestobitnih mikroprocesora će razumeti zašto su operatori ++ i -- značajni: oni se najčešće prevode kao autoinkrementno odnosno autodekrementno adresiranje što znači da povećanje brojača ne zauzima nikakvu memoriju niti troši dodatno vreme (još je PDP 11 mogao da izvrši MOV (R0), (R1)+).

Šta da radimo ako nam nekada zatreba da uvećamo (umanjimo) brojač za dva? Ništa lakše: umesto i++ (i--) pisaćemo i+=2 (i-=2). Uz pomoć tzv. rekurzivnih operatora sa slike 5.2 možemo, pored ostalog, da uvećavamo, umanjujemo, množimo, delimo i šiftujemo podatke bez potrebe da ih premeštamo što znači da naredbe tipa I=I+1 pripradaju prošlosti.

Jezik koji pretenduje da zameni mašinac ne može da se zadovolji sa četiri osnovne računske radnje: za sistemsko programiranje su neophodne logičke operacije i šiftovanja. Slika 5.2 pokazuje da C podržava konjukciju (&), disjunkciju (|), ekskluzivnu disjunkciju (^), negaciju (~), oba šiftovanja za N mesta (>> odnosno <<) i ekvivalente bejzik funkcija VARPTR (& - iako je simbol jednak simbolu za konjukciju, zabuna je sintaksno nemoguća) i PEEK (*). Sve se logičke operacije obavljaju bit po bit što znači da je 123 & 712 = 72 (odakle ovo? 123(10) = 1111011(2); 712(10) = 1011001000(2); 1111011 & 1011001000 = 1001000; 1001000(2) = 72 (10)). Ako radite sa maskama, poželećete da kucate heksadekadne konstante što C potpuno podržava: I=0x1ABC se potpuni ekvivalent bejzik naredbe I=&1ABC. Osim heksadekadnih, C podržava i oktalne konstante kojima prethodi jedna nula: 0127 je oktalni broj (ova je konvencija nedoglupa jer se na svim drugim jezicima vodeće nule ignoriše ali - šta da se radi!). Kada smo već kod oktalnih brojeva, iskoristićemo priliku da odgovorimo na pitanje koje nam čitaoci često postavljaju: ko to još koristi oktalne konstante? Oktalni brojevi se na personalcima zaista koriste veoma retko (heksadekadni su mnogo pogodniji) ali su na prvoj popularnoj šesnaestobitnoj mašini zvanoj PDP 11 obilno korišćeni da razdvoje operand od oznake adresiranja. Obzirom da su mnogi danas slavni programeri počinjali baš na PDP-ju, nije im lako da zaborave oktalni sistem!

Relacioni operatori

Već znamo da C ne omogućava deklarisanje logičkih (Bulovih) vrednosti - flegove zamenjujemo celim brojevima pri čemu istini odgovara vrednost 1 (ili, da budemo precizniji, bilo koja vrednost različita od nule) a neistini vrednost 0. Ove vrednosti, jasno, nastaju raznim poređenjima koja prikazuje slika 5.3.

slika 5.3:
Jednako
==
a == b
Različito
!=
a != b
Veće
>
a >  b
Veće ili jednako
>=
a >= b
Manje
<
a <  b
Manje ili jednako
<=
a <= b
Bulovo I (and)
&&
a<b && c<d
Bulovo ILI (or)
||
a<b || c<d
Bulovo NE (not)
!
! a<b

Tabela 5.3 ukazuje na neke stvari koje će namučiti bejzik, fortran i paskal programere. Prva od tih nevolja je toliko važna da smo joj posvetili sliku 5.4.

Slika 5.4:
/* Program sa GADNOM greškom */
main ()
{
int a;
   scanf ("%d", &a);
   if (a=0)
      printf ("Broj je nula");
   else printf ("Nije nula");
}

Šta radi program sa slike 5.4? Ništa lakše - sa tastature se učitava neka vrednost i ispisuje odgovarajuća poruka u zavisnosti od toga da li je uneti podatak jednak nuli ili nije. Program 5.4 je, međutim, sasvim pogrešan a krivicu za tu grešku snosi nedopustiva nemarnost konstruktora C-a: na bezmalo svim ranijim jezicima znak jednakosti označava poređenje podataka ili, u odgovarajućim uslovima, dodeljivanje vrednosti. Tvorcima paskala se ova dvoznačnost nije svidela pa su za dodeljivanje vrednosti koristili znakove ':=' što je sasvim u redu - ukoliko zaboravimo dvotačku, kompajler će nas upozoriti. Na C-u, međutim, znak jednakosti predstavlja dodeljivanje dok se poređenje zahteva sa dva znaka jednakosti (==). Program sa slike 5.4 je, dakle, sintaksno sasvim ispravan ali, umesto da poredi N sa nulom, dodeljuje nulu promenljivoj N. Ovakva promena jedne sasvim uobičajene konvencije je jedna od mana C-a koja će vas bezbroj puta zaviti u crno u periodu navikavanja na ovaj jezik - tražićete grešku a da nikada ne posumnjate u naoko savršen IF. Pokušajte, dakle, da vam slika 5.4 bude pred očima kada god programirate u C-u.

Ostale razliku su manje važne: umesto <> pišemo != (not jednako - baš logično zvuči!) dok veće ili jednako mora da se označava sa >= a ne, kao u bejziku, =>. Kako se pišu složeni uslovi poput IF A>3 AND B<5? Slika 5.3 prikazuje i relacione operatore and, or i not koji se zamenjuju simbolima &&, ||, odnosno !. Vidimo da se "bit po bit" konjukcija zamenjuje simbolom & a logička konjukcija simbolom &&. U čemu je razlika? Logička konjukcija (isto važi i za disjunkciju) uvek daje rezultat 0 ili 1 što znači da je 123 & 712 = 72 dok je 123 && 712 = 1. Ukoliko u nekoj IF naredbi zaboravite da duplirate znakove, verovatno se neće dogoditi ništa strašno - program neće raditi baš onako kako ste mislili ali će davati tačne rezultate. Pokušajte sami da uporedite naredbe IF ((i=3) & (j=2)) printf ("komentar"); i IF ((i=3) && (j=2)) printf ("komentar"); i utvrdite da li one uvek proizvode isto dejstvo.

Poslednji "C specijaliteti" su skrivene IF naredbe i grupna dodeljivanja; upoznaćemo ih na slici 5.5.

slika 5.5:
main ()
{
int x,y,z,i,j,k;
   scanf ("%d %d", &x, &y);
   (x==3) ? (y=1) : (x++);
   z = (i=4, j=14, k=77);
   printf ("x=%d\ny=%d\nz=%d\n", x, y, z);
   printf ("i=%d\nj=%d\nk=%d\n", i, j, k);
}

Kakav je, pre svega, smisao linije x == 3 ? y=1 : x++;? X==3, kao što znamo, ispituje da li je sadržaj promenljive X jednak 3. Ako jeste, izvršava se deo izraza iza upitnika što znači da će ipsilonu biti dodeljena vrednost 1. Ukoliko je, sa druge strane, X bilo različito od tri, biće izvršen izraz iza dvotačke što znači da će vrednost X biti povećana za jedan (u ovom smo slučaju mogli da napišemo x++ ili ++x - dejstvo je sasvim isto).

Sledeća linija primera 5.5 ilustruje višestruko dodeljivanje: u zagradu smo, ukratko rečeno, smestili nekoliko aritmetičkih izraza razdvojenih zarezima. Računar će izračunati sve ove izraze ali će promenljivoj X dodeliti samo vrednost poslednjeg (tj. prvog s desna). Sve vam ovo izgleda komplikovano i nepotrebno. Nemate, ipak, previše razloga za žalbu: u početku možete da pišete najobičnije bejzik izraze i C će ih (sa izuzetkom primera 5.4) savršeno interpretirati. S vremenom ćete, međutim, početi da koristite sve više "specijaliteta kuće" pa će vam, ako ostanete pri C-u, jednoga dana izrazi u paskalu i bejziku izgledati grozno ograničeni. Tada će vam biti značajna tabela 5.6 koja opisuje prioritete pojedinih operatora i njihovu eventualnu neasocijativnost: ponekad se deo izraza računa s leva na desno a ponekad i sa desna na levo. Prioritet, jasno, možete da promenite navođenjem zagrada - baš kao na bejziku, fortranu ili paskalu.

slika 5.6: Prioriteti operatora
1.
f() [] -> .
L --> D
2.
! ~ ++ -- -x (dodele) *p &a sizeof()
D --> L
3.
* / %
L --> D
4.
+ -
L --> D
5.
<< >>
L --> D
6.
< <= > >=
L --> D
7.
== !=
L --> D
8.
&
L --> D
9.
^
L --> D
10.
|
L --> D
11.
&&
L --> D
12.
||
L --> D
13.
?:
D --> L
14.
= += -= *= /= %= >>= <<= &= |= ^=
D --> L
15.
,
L --> D

Racionalni brojevi

Što se racionalnih brojeva tiče, na raspolaganju je sabiranje, oduzimanje, množenje i deljenje ali ne i stepenovanje (treba znati da je A^B = exp(b*log(a))). Broj raspoloživih elementarnih funkcija zavisi od biblioteke koju koristite (sećate se one LC.LIB (ili kako se već zove) sa kojom neprekidno linkujete vaše programe) što znači da ćete morati da konsultujete uputstvo za upotrebu vašeg kompajlera. Ukratko rečeno, C poseduje većinu funkcija koje su vam poznate iz bejzika pa je čak i sintaksa pozivanja identična.

U istom izrazu mogu da se kombinuju celi i racionalni brojevi ali uz određenu dozu opraza. Svakome je jasno da I=A (gde je I ceo a A racionalan broj) izaziva gubitak decimala ali mana naredbe A=2/3 ne mora da bude očigledna: 2 i 3 su celi brojevi što znači da se njihovim deljenjem dobija nula koja se zatim dodeljuje racionalnoj promenljivoj A - decimali su izgubljeni još u toku izračunavanja vrednosti izraza! Zato je najbolje da racionalne konstante uvek pišete sa decimalnim delom: izraz A=2.0/3.0 ili bar A=2.0/3 bi rezultirao racionalnim brojem.

Diskusiju o aritmetičkim izrazima završavamo programom 5.7 koji je prepun komplikovanih aritmetičkih i logičkih izraza. Ako umete da objasnite dobijene rezultate, sjajno ste napredovali. Ako umete da pogodite rezultate bez startovanja programa, ovo ste poglavlje magistrirali!

slika 5.7:
main ()
{
int x,y,z,i,j,k,m;
   m = 5;
   k = (x=17, z=--m);
   i = ( (k==4) ? (j=((x==17) ? (z=3) : (z=2))) : (z=1, j=45));
   i <<= 3;
   y = ++i & (k==4);
   printf ("x=%d\ny=%d\nz=%d\n", x, y, z);
   printf ("i=%d\nj=%d\nk=%d\n", i, j, k);
}

Kontrolne strukture

Ko čeka taj dočeka pa su tako i strpljivi čitaoci ovoga umetka dočekali programe koji se neće sastojati samo od deklaracija i izraza. Počećemo, jasno, od glavnog programa koji se obeležava sa main.

Već smo upoznali za bejzik programere čudnu konvenciju da se bilo koja procedura, funkcija, petlja ili neka druga struktura uključujući i sam glavni program sastoji od jedne jedine naredbe. Tu naredbu, jasno, najčešće zamenjujemo otvorenom velikom zagradom iza koje sledi niz izvršnih naredbi od kojih se svaka (pa i poslednja!) završava tačkom i zarezom. Ovu takozvanu sekvencu završavamo zatvorenom velikom zagradom.

Osim sekvence, strukturirano programiranje zahteva ispitivanje i petlju. Ispitivanje se realizuje primenom konstrukcije if ... then ... else koju znamo iz bejzika. Razlika je zaista mala: C ne poznaje reč then pa će, ukoliko logički izraz iza IF ima vrednost različitu od nule (true) izvršiti jednu sledeću naredbu dok će u slučaju da izraz ima vrednost 0 (false) izvršiti jednu naredbu iza ELSE (else sekcija, jasno, može i da se izostavi). Umesto jedne naredbe možete da otvorite veliku zagradu i napišete čitavu sekvencu naredbi (u kojoj smeju da se nalaze i nova ispitivanja) koja se završava zatvorenom zagradom. Slika 6.1 prikazuje nekoliko if naredbi u praksi.

slika 6.1:
main ()
{
int a=10, b=2, c=5, d=0, e, i, p, x=5, y;
   if (a>3) printf ("A je veće od 3\n");
   if (b>3)
   {
      printf ("B je veće od 3\n");
   }
   if (c>3)
   {
      d = c*c; e = d-3*a;
   }
   else
   {
      d = c+3; e = d+3*a;
   };
   if (c>17)
   {
      printf ("C veće od 17\n");
      if (x>3)
      {
         p = x*x + c*c;
         printf ("p=%d\n", p);
      } ;
      i = i+1;
   }
   else
   {
      y=0;
      if (x>3) {
         p=x*x-c*c;
         printf ("p=%d\n", p);
      }
      else {
         p=0;
         printf ("Neocekivana situacija!\n");
      }
   }
}

Prva IF naredba je sasvim jednostavna: ako je vrednost promenljive A veća od tri, ispisuje se odgovarajući tekst i prelazi na sledeću naredbu. Obzirom da smo ispisivanje obavili jednim jedinim printf, par zagrada nam nije bio potreban mada ne bi smetao da smo ga, kao u drugoj IF naredbi, napisali. Treća IF naredba je nešto komplikovanija: sastoji se iz then i else sekcija pri čemu svaka od ovih sekcija ima po nekoliko naredbi "uokvirenih" zagradama. Četvrti IF, najzad, u svojim sekcijama ima još po jednu IF naredbu šta nas dovodi do veoma važnog problema: gde sve treba staviti terminator ';'? Smemo li da ga stavljamo iza svake naredbe nadajući se da će C ignorisati terminatore koji mu nisu potrebni? Nikako: svaki udžbenik paskala i C-a opisuje jednu fatalnu početničku grešku koja može da donese mnogo glavobolje. Pogledajmo naredbu:

if (a==b) then; printf ("Jednaki su\n");

Simbol ';' koji smo istakli u prethodnoj naredbi je, kao što smo rekli, terminator instrukcije. On je stavljen iza then i samim tim predstavlja kraj čitave if...then...else konstrukcije. Ukoliko je A jednako B, biće izvršen deo te konstrukcije koji se nalazi iza then što znači da će biti izvršeno ništa; računar će preći na sledeću naredbu (printf) pa će se na ekranu pojaviti željeni tekst 'Jednaki su'; reklo bi se da je sve u redu. Pretpostavimo, međutim, da je A različito od B; pošto else sekcija ne postoji, računar će izvršiti prvu sledeću naredbu tj. opet printf; na ekranu će se pojaviti tekst 'Jednaki su' iako su brojevi različiti! Terminator, dakle, treba staviti tek na kraj čitave strukture.

Petlje

C podržava tri vrste petlji: while, do ... while i for. Prva petlja se smatra najuniverzalnijom: iza while se (u zagradi) piše neki logički uslov a iza njega jedna naredba (ili čitava sekvenca) koja se izvršava sve dok je uslov ispunjen. Pogledajmo kako ova petlja, na slici 6.2, izračunava faktorijel celog broja.

slika 6.2:
main ()
{
   int i, n;
   double y;
   printf ("Unesi broj čiji se faktorijel računa\n");
   scanf ("%d", &n);
   y = 1.0;
   i = 1;
   while (i<=n)
   y=y*i++;
   printf ("%d!=%.0f\n", n, y);
}

Posle učitavanja broja N promenljivoj Y dodeljujemo vrednost 1 i postavljamo brojač I na istu vrednost; treba da računamo 1*2*3*...*N. Petlja se sastoji od jedne jedine naredbe y=y*i++ koja zapravo u sebi skriva i povećanje brojača (mogli smo da napišemo i y=y*i; i=i+1; ali bi nam tada trebale i velike zagrade). Ovu naredbu treba ponavljati sve dok je I manje ili jednako N pa otuda i while (i<=n). Sve je, dakle, jasno ali ipak verujemo da primer 6.2 ne može da prođe bez par reči komentara.

Šta se događa ako je N=0? Obzirom da je I=1, neće biti I<=N pa računar uopšte ne ulazi u petlju - ona će biti preskočena pa će biti ispisano da je vrednost faktorijela 1 što je sasvim ispravno. Sami zaključite da li smo mogli da započnemo sa I=2 i tako mikroskopski ubrzamo program.

slika 6.3:
main ()
{
   int i, n;
   double y;
   printf ("Unesi broj čiji se faktorijel računa\n");
   scanf ("%d", &n);
   y = 1.0;
   i = 1;
   do
      y=y*i++;
   while (i<=n);
   printf ("%d!=%.0f\n", n, y);
}

Slika 6.3 prikazuje do ... while petlju: računar će izvršavati naredbe između do i while sve dok je uslov iza while ispunjen. Obzirom da službene reči do i while na neki način uokviruju sekvencu, ne moramo da koristimo velike zagrade premda one, jasno, neće biti na smetnji.

Da li možemo da (mikroskopski) ubrzamo program 6.3 tako što ćemo početi od I=2? Na žalost, ne možemo: za N=0 računar će ući u petlju, izračunati Y=Y*2, uvećati I i, pošto primeti da je I veće od N, izaći iz petlje što znači da će 0! biti dva a ne jedan. Sadržaj do ... while petlje se, dakle, izvršava bar jednom dok se sadržaj while petlje ne mora uopšte izvršiti; zato do ... while zovemo "petljom sa izlazom na dnu" a while "petljom sa izlazom na početku". Za računanje faktorijela je while petlja nešto pogodnija ali to ne znači da je ona "bolja": mogli smo lako da izmislimo primer u kome bi do ... while bolje poslužilo. Da je jedna od petlji uvek bolja, druga ne bi ni postojala!

Došli je vreme da susretnemo starog poznanika, for petlju. Razlike između bejzika i C-a su, bar kada se radi o ovoj petlji, mnogobrojne ali uglavnom formalne - jedina suštinska razlika je što brojač u petlji mora da bude ceo broj što znači da naredbe poput FOR X=1.5 TO 7.5 STEP 0.1 možemo da zaboravimo. Što se sintakse tiče, iza službene reči for pišemo tri izraza koja se razdvajaju tačkom i zarezom i smeštaju u zajedničku malu zagradu. Prvi izraz predstavlja inicijalizaciju brojača, tipično i=1. Drugi izraz je logički i definiše uslov za ponavljanje petlje - naredba iza for se izvršava sve dok je uslov ispunjen. Treći argument, najzad, povećava ili smanjuje brojač: možemo da pišemo i=i+1 ali je i++ daleko racionalnije.

Poput while, for - do petlja može i da se ne izvrši (ako uslov u startu nije ispunjen) što smo iskoristili na slici 6.4 - program za računanje faktorijela počinje od I=2. Sa slike vidimo da C nema naredbu NEXT - sadržaj petlje je samo jedna naredba koju možemo da zamenimo sekvencom. Obzirom da u našem primeru petlja ima samo jednu naredbu, velike zagrade nisu bile potrebne.

slika 6.4:
main ()
{
int i, n;
double y;
   printf ("Unesi broj čiji se faktorijel računa\n");
   scanf ("%d", &n);
   y = 1.0;
   for (i=1; i<=n; i++)
   y=y*i;
   printf ("%d!=%.0f\n", n, y);
}

Ideal strukturiranog programiranja je da svaka struktura (glavni program, potprogram, petlja i tome slično) ima tačno jedan ulaz i tačno jedan izlaz - jedini legalan način za izlazak iz petlje je, dakle, da uslov iza while odnosno for prestane da bude ispunjen. Zamislimo, međutim, da pišemo kompajler koji, jasno, analizira sintaksu teksta koji je korisnik otkucao. Ukoliko je korisnik pogrešio, treba ispisati odgovarajuće poruke i napustiti sve petlje - njihovo je dalje izvršavanje bespredmetno. Jedan od načina za iskakanje iz petlje je inkriminisana naredba goto. Drugi (i malo lepši) način je break.

slika 6.5:
#define maxn 10
main ()
{
   int niz [maxn], i, j;
   printf ("Unesi %d celih brojeva ili -777 za kraj\n", maxn);
   for (i=0; i<maxn; i++) {
      scanf ("%d", &niz [i]);
      if (niz [i] == -777) break;
   };
   printf ("Uneti elementi niza:\n");
   for (j=0; j<i; j++)
      printf ("niz[%d] = %d\n", j, niz [j]);
   exit (0);
}

Program sa slike 6.5 učitava deset brojeva sa tastature i smešta ih u niz. Ukoliko, međutim, korisnik otkuca broj -777, smatra se da je unošenje podataka završeno pa se izlazi iz petlje. Sličan se efekat, naravno, mogao postići uvođenjem nekog flega i proširivanjem uslova iza for ali su konstruktori C-a (sa pravom) smatrali da bi ovakvo rešenje bilo neracionalnije i teže shvatljivo. Naredba break je, dakle, veoma korisna alatka koju treba upotrebljavati sa merom. Ni ona, međutim, nije savršena: sa break napuštate samo tekuću petlju a ne i eventualne petlje u kojima se ta petlja nalazi. Ako baš želite duže skokove, moraćete da koristite goto.

Naredba SWITCH (CASE)

Koristite li naredbu ON ... GOTO u bejziku? Ako je vaš računar ne poseduje, pročitajte sledećih par rečenica: posle ON X GOTO 100, 200, 300, 400 izvršavanje programa će se nastaviti od naredbe sa obeležjem 100 ako je X=1, od naredbe 200 ako je X=2 i tako dalje. Ukoliko je X manje od 1 ili veće od četiri, (verovatno) će se preći na sledeću instrukciju. Instrukcija ON ... GOTO je dosta zgodna zamena za seriju IF-ova sa slike 6.6 ali C nudi nešto mnogo bolje - strukturu switch sa slike 6.7.

slika 6.6:
10 IF X=1 THEN PRINT "Jedan"
20 IF X=2 THEN PRINT "Dva"
30 IF X=3 THEN PRINT "Tri"
40 IF X=4 THEN PRINT "Četiri"
 
slika 6.7:
#define true 1 #define false 0
main ()
{
int izbor, dalje;
   dalje = true;
   while (dalje) {
      printf ("\n\n");
      printf ("  1. Unos podataka\n");
      printf ("  2. Ispis podataka\n");
      printf ("  3. Brisanje podataka\n");
      printf ("  7. Testiranje modela\n");
      printf ("999. Kraj rada\n\n");
      printf ("     Vaš izbor?\n");
      scanf  ("%d", &izbor);
      printf ("\n\n");
      switch (izbor)
      {
         case   1: printf ("Treba uneti podatke\n"); break;
         case   2: printf ("Podaci se ispisuju\n"); break;
         case   3: printf ("Brisanje dela podataka\n"); break;
         case   7: printf ("Testiranje modela\n"); break;
         case 999: dalje = false; break;
         default:  printf ("Pogrešna komanda!\n"); break;
      }
   }
   printf ("\n\nKraj rada\n");
}

Primer sa slike 6.7 je sasvim jednostavan: svaka od unetih vrednosti rezultuje izvršavanjem nekoliko naredbi. Primetimo, međutim, da nisu zastupljene sve vrednosti: predvideli smo da ulazna promenljive može da bude 1, 2, 3, 7 ili 999 što u bejziku ne bismo mogli racionalno da uradimo sa ON ... GOTO. Čitavu strukturu iza switch treba da smestimo u velike zagrada dok svaku od njenih grana moramo da završimo sa break - tako je omogućeno da se za nekoliko susednih vrednosti izvrše iste naredbe.

Šta se događa ako promenljiva A u programu 6.7 nema ni jednu od predviđenih vrednosti? Nije teško: izvršavaju se naredbe iza klauzule default koju, premda je poslednja, treba završiti sa break.

Naredbe GOTO i EXIT

Dani u kojima je utvrđena štetnost naredbe goto se danas slave kao prekretnica u svetu programera-profesionalaca; ova je instrukcija, naime, okrivljena za nepregledne programe pisane u takozvanom "špageti stilu" tj. programe koje je teško razumeti, testirati i modifikovati. C ipak sadrži goto naredbu ali će autori raznih udžbenika svakako predložiti da je zaboravite.

slika 6.8:
main ()
{
int a;
pocetak:
   scanf ("%d", &a);
   if (a >  10) goto kraj;
   if (a ==  3) goto tri;
   if (a == 10) goto deset;
nepr:
   printf ("Nepredviđena vrednost\n");
   goto dalje;
tri:
   printf ("Tri!\n");
   goto dalje;
deset:
   printf ("Deset!\n");
dalje:
   goto pocetak;
kraj:
   printf ("Uspešan završetak!\n\n");
   exit (0);
}

Program sa slike 6.8 ilustruje primenu naredbe goto: uveli smo nekoliko labela (labela je u C-u obična reč iza koje sledi dvotačka) i "skakali" na neke od njih. Upotreba goto naredbe ipak nije sasvim slobodna: ne možemo da "skačemo" iz jedne procedure u drugu dok je "uletanje" u sred neke strukture prilično opasno. Predlažemo vam da goto naredbu koristite isključivo u situacijama poput:

slika 6.9:
main ()
{
   for (i=1; i<=n; i++) {
      ...
      for (j=1; j<=n; j++) { ...
         if (k<3) { ...
            while (dalje) {
               ...
               if (katastrofa) goto uzbuna;
               ...
            } ...
         } ...
      } ...
   }
   exit (0);
uzbuna:
   /* spasi što se spasti može; */
   exit (1);
}

Naredba exit predstavlja ekvivalent bejzik instrukcije STOP: nailazak na exit prekida izvršavanje programa i predaje kontrolu operativnom sistemu. Računar će, prirodno, ispisati prompt i sačekati da korisnik otkuca novu naredbu ili ponovo startuje program.

Kako smo do sada živeli bez exit-a? Bilo koji C program završava rad kada naiđe na poslednju zatvorenu srednju zagradu. Exit se, dakle, koristi samo u specijalnim situacijama, na primer kada je detektovana greška pa je dalje izvršavanje besmisleno. Neki kompajleri omogućavaju da exit dopunite završnim statusom: exit(0) označava da je program korektno izvršen dok bi, na primer, exit(1) moglo da znači da datoteka nije pronađena. LC i MWC kompajleri dopuštaju da se iza exit napiše statusni kod ali taj kod jednostavno ignorišu.

Procedure i funkcije

Kada malo bolje razmislimo, bejzik je sasvim pristojan programski jezik ali ga upropašćuju nefleksibilni potprogrami - kada ovladate fortranom, paskalom ili C-om, prosto nećete moći da pogledate naredbu GOSUB! Ako su procedure i funkcije za vas apsolutni novitet, neće vam biti baš sasvim lako da ih razumete (svaki je početak težak...) pa ćemo zato napredovati korak po korak; počnimo od "prostih" potprograma sa argumentima.

Procedure i argumenti

slika 7.1:
#define maxn 1000
main () {
int x [maxn], y [maxn];
int poc, kraj, i, j;
   for (j=0; j<maxn; j++)
      (x [j] = j, y [j] = -j);
   printf ("Od kog se elementa ispisuje\n");
   scanf  ("%d", &poc);
   printf ("Do kog se elementa ispisuje\n");
   scanf  ("%d", &kraj);
   printf ("\n Elementi niza X:\n");
   ispis  (x, poc, kraj);
   printf ("\n Elementi niza Y:\n");
   ispis  (y, 5, 10);
}

ispis (a,   prvi, zadnji) int a[], prvi, zadnji; {
int i;
   i=prvi;
   while (i<=zadnji) {
      printf ("Element %d = %d\n", i, a [i]);
      i++;
   }
}

Slika 7.1 prikazuje proceduru koja ispisuje "komadić" niza A. Već znamo da se procedura poziva prostim navođenjem imena i da se definiše posle završetka glavnog programa (možete je, zapravo, pisati i pre glavnog programa pa čak i odvojiti u posebnu datoteku ali se sličnim komplikacijama za sada nećemo baviti). Šta, međutim, označava tekst u zagradama?

Procedura sa slike 7.1 se zove ispis i ima tri takozvana fiktivna argumenta: A, PRVI i ZADNJI; A je niz a PRVI i ZADNJI celobrojne promenljive. Zašto ove promenljive nazivamo fiktivnim argumentima? Podignimo pogled za par redova i pročitajmo naredbu kojom je procedura pozvana: ispis(x, poc, kraj). C kompajler će najpre da pronađe proceduru i onda da uporedi argumente:

slika 7.2:
a	prvi	zadnji	- fiktivni argumenti
x	poc	kraj	- stvarni  argumenti

Argumenti, vidimo, moraju da budu saglasni po vrsti i redosledu: nizu X odgovara niz A, celobrojnoj promenljivoj POC celobrojna promenljiva PRVI a celobrojnoj promenljivoj KRAJ celobrojna promenljiva ZADNJI. Vrednosti stvarnih argumenata se dodeljuju fiktivnima a onda se izvršava procedura. Zašto čitava ova komplikacija? Zato što samo par naredbi docnije ponovo pozivamo proceduru ispis ali sa drugim stvarnim argumentima: ispis(y, 5, 10). Slika 7.3 prikazuje kako su ovoga puta vrednosti stvarnih argumenata dodeljene fiktivnim.

slika 7.3:
a	prvi	zadnji - fiktivni argumenti
y	5	10     - stvarni  argumenti

Da li vam je sada jasno zašto su argumenti izmišljeni? Zahvaljujući njima smo bili u mogućnosti da istu proceduru koristimo za ispisivanje elemenata nekoliko različitih nizova. Proceduru ispis možemo, dakle, da koristimo na mnogo mesta u raznim programima ne razmišljajući više o tome kako ona radi - što je ispis složenija procedura to će nam se više isplatiti da je jednom zauvek detaljno testiramo i docnije koristimo kao "crnu kutiju" menjajući samo ulazne parametre - argumente. U tome je i smisao strukturiranog programiranja.

Pošto smo razumeli smisao, obratimo malo pažnje na sintaksu. Fiktivni argumeti se navode u zagradi i razdvojaju zarezima. Svaki argument mora da se deklariše pre nego što otvorimo veliku zagradu koja započinje samu proceduru; ovo se deklarisanje vrši na standardan način s tim što ne moramo (ali možemo) da navodimo prvu dimenziju niza - on je definisan u glavnom programu (sada, dakle, vidimo da se u glavnom programu niz definiše (rezerviše se memorijski prostor za njega) dok se u procedurama taj niz samo deklariše tj. opisuje). Stvarni argumenti se, kao i u modernijim bejzicima, razdvajaju zarezima.

slika 7.4:
#define maxn 1000
main () {
int x [maxn], y [maxn];
int poc, kraj, i, j;
   for (j=0; j<maxn; j++)
      (x [j] = j, y [j] = -j);
   printf ("Od kog se elementa ispisuje\n");
   scanf  ("%d", &poc);
   printf ("Do kog se elementa ispisuje\n");
   scanf  ("%d", &kraj);
   printf ("\n Elementi niza X:\n");
   ispis  (x, poc, kraj);
   printf ("\n Elementi niza Y:\n");
   ispis  (y, 5, 10);
   printf ("poc = %d", poc);
}
ispis (a,   prvi, zadnji) int a[], prvi, zadnji; {
int i;
   i=prvi;
   while (i<=zadnji) {
      printf ("Element %d = %d\n", i, a [i]);
      i++;
   };
   prvi = 0;
}
sl74
Od kog se elementa ispisuje 7
Do kog se elementa ispisuje 12
Elementi niza X:
Element 7 = 7
Element 8 = 8
Element 9 = 9
Element 10 = 10
Element 11 = 11
Element 12 = 12
Elementi niza Y:
Element 5 = -5
Element 6 = -6
Element 7 = -7
Element 8 = -8
Element 9 = -9
Element 10 = -10
poc = 7

Hajde da malo promenimo proceduru ispis i dodamo joj naredbu prvi=0 koja će se, kao na slici 7.4, naći na njenom fizičkom kraju. Dopunili smo i glavni program naredbom za ispisivanje promenljive poc i startovali program. Iznenađenje - računar je ispisao da je poc jednako 7! Iznenađenje je nastalo zato što...

Samo trenutak! Da li vas iznenađuje ispis poc=7? Trebalo bi da vas iznenadi: iako je promenljivoj POC dodeljena vrednost 7, pozivom procedure ispis je vrednost POC "presuta" u fiktivnu promenljivu PRVI a docnije je vrednost promenljive PRVI promenjena na nulu; trebalo bi da se i POC promeni. Ipak, sadržaj POC se nije promenio: računar je prepisao sadržaje stvarnih argumenata u fiktivne ali je "zaboravio" da, po izlasku iz procedure, prepiše vrednosti fiktivnih promenljivih u stvarne. Zaboravni smo, u stvari, mi - nismo naredili ovo prepisivanje! Pogledajmo sliku 7.5.

slika 7.5:
main () {
int a=7, b=9, c=11, d=13, x=15, y=17;
   printf ("Pre ulaska u  proceduru:\n");
   printf ("a=%d\nb=%d\nc=%d\n", a, b, c);
   printf ("d=%d\nx=%d\ny=%d\n", d, x, y);
   nista (&a, &b, &c, &d, x, y);
   printf ("Po izlasku iz procedure:\n");
   printf ("a=%d\nb=%d\nc=%d\n", a, b, c);
   printf ("d=%d\nx=%d\ny=%d\n", d, x, y);
}
nista (a, b, c, d, x, y) int *a,*b,*c,*d, x, y; {
   (*a=0, *b=1, *c=2, *d=3);
   (x=4, y=5);
}
sl75
Pre ulaska u  proceduru: a=7 b=9 c=11 d=13 x=15 y=17
Po izlasku iz procedure: a=0 b=1 c=2 d=3 x=15 y=17

Procedura ništa sa slike 7.5, kao što joj i ime kaže, ne radi ništa pametno: samo menja vrednosti fiktivnih argumenata. Glavni program, sa druge strane, dodeljuje početne vrednosti i, po izvršenju procedure, ispisuje rezultate: neke su vrednosti promenjene a neke nisu. Kako to? Odgovor pruža pogled na liniju koja poziva potprogram: promenljivima A, B, C i D prethodi and (&) dok je kod X i Y ovaj predznak izostavljen; slično tome, umesto sa imenima promenljivih, potprogram operiše sa odgovarajućim memorijskim ćelijama (primetimo one zvezdice koje, kako rekosmo, označavju PEEK odnosno POKE). Sve u svemu, potprogram može da promeni vrednosti A, B, C i D ali ne i vrednostu X i Y. Ukoliko, dakle, procedura ima neke izlazne veličine (tj. promenljive koje vraćaju rezultate iz procedure u glavni program), imenima odgovarajućih stvarnih argumenata prethodi & i radi se sa pointerima; ukoliko su promenljive isključivo ulazne (tj. služe za prenos podataka iz glavnog programa u proceduru), ovaj specijalni znak izostavljamo. Korišćenje ulazno-izlaznih argumenata (tj. promenljivih koje će donositi podatke i preuzimati rezultate) nije baš u duhu strukturiranog programiranja; ukoliko ipak ne težite za tim da budete "u duhu", i ove ćete promenljive morati da prenosite sa &.

Nema potrebe da mehanizam prenošenja parametara ostane tajna: lakše ćete ga koristiti ako ga razumete. Pri svakom pozivu neke procedure svi argumenti kojima ne prethodi & se prepisuje na stek. Kada se izvršavanje potprograma okonča, vrednosti sa steka se vraćaju u zonu promenljivih čime se anuliraju sve izmene koje je procedura preduzela nad argumentima prenesenim po vrednosti. Što se "onih drugih" promenljivih tiče, njihove se vrednosti ne prepisuju već se proceduri dostavljaju adrese stvarnih promenljivih (sećamo se da je & ekvivalent bejzik funkcije VARPTR) - svaka promena nekog argumenta prenetog po imenu preživljava i kraj izvršenja potprograma! Visokostručne termine "prenošenje po vrednosti" i "prenošenje po imenu" ćete bolje zapamtiti ako se prisetite jedne čuvene esnafske anegdote vezane za ime Niklausa Virta, tvorca paskala. Ako anegdotu još ne znate, upućujemo vas na umetak "Sa bejzika na paskal" iz "Računara 24".

Da li je bolje prenositi argumente po vrednosti ili po imenu? Kao i obično, decidiran odgovor ne postoji: da je jedan od načina uvek bolji, drugi ne bi ni postojao! Treba, ipak, da primetimo da prenošenje po vrednosti zahteva rezervaciju dodatnog memorijskog prostora pa na C-u nije dopušteno prenošenje nizova, matrica i stringova na ovaj način - pošto je samo ovaj način prenošenja nizova moguć, nema potrebe da se ispred njihovih imena stavljaju and-ovi ili zvezdice - oni se podrazumevaju. To nam je i omogućilo da u programu 3.2 prikrijemo zbrku sa pointerima!

Lokalne (automatske) promenljive

Najteže je prošlo - ako ste shvatili kako se potprogrami pozivaju i kako se argumenti prenose po imenu i po vrednosti, razumećete šta su lokalne promenljive i slične sitnice. Ako niste razumeli - probajte da pročitate tekst još par puta a onda menjajte program sa slike 7.5 i posmatrajte rezultate.

Slika 7.6:
int c=3;
main () {
int a=1, b=2;
   printf ("Pre ulaska u  proceduru:\n");
   printf ("a=%d\nb=%d\nc=%d\n", a, b, c);
   promena (&a, &b);
   printf ("Po izlasku iz procedure:\n");
   printf ("a=%d\nb=%d\nc=%d\n", a, b, c);
   exit (0);
}
promena ( x, y) int *x, y; {
   (*x=5, y=5);
   c=303;
}
sl76
Pre ulaska u  proceduru: a=1 b=2 c=3
Po izlasku iz procedure: a=5 b=2 c=303

Na slici 7.6 vidimo kratak program na C-u i rezultate njegovog izvršavanja koje ćemo pokušati da prokomentarišemo. Definisali smo celobrojne promenljive A, B i C a onda im dodelili vrednosti 1, 2 i 3 respektivno. Sledi poziv procedure promena čiji su stvarni argumenti A i B a fiktivni X i Y. U okviru procedure fiktivnim argumentima X i Y dodeljujemo vrednost 5 ali je po povratku u glavni program samo A promenjeno - B je, primećujemo, preneto po vrednosti. Do sada nismo primetili ništa novo. Pogledajmo, međutim, promenljivu C - ona uopšte nije preneta u proceduru promena a opet je procedura promena uzela slobodu da promeni vrednost C; nova je vrednost, štaviše, "preživela" i kraj izvršavanja procedure! Promenljiva C, međutim, nije baš sasvim obična: deklarisali smo je izvan bilo koje procedure ili funkcije. Zato je ta promenljiva globalna tj. dostupna svim daljim procedurama koje su smeštene u tu datoteku. Za trenutak ćemo videti da globalnim promenljivima mogu da pristupaju i linkovane procedure iz drugih datoteka ako je deklarišu kao "spoljnu" (extern od external).

Procedura, dakle, može da analizira i menja vrednost promenljivih koje nisu prenete kao argumenti ukoliko su takve promenljive globalne tj. definisane van tela bilo koje procedure. U bejziku su, uzgred budi rečeno, sve promenljive globalne što znači da GOSUB potprogram može (i mora) da ih menja po volji. Ovakav rad je na C-u moguć ali ne i preporučljiv - ako želite da pišete bejzik programe na C-u, nema nikakve potrebe da se uopšte mučite sa novim jezikom! Ukoliko, dakle, vaš potprogram treba da operiše sa nekom promenljivom, uvek je prenesite kao argument! Zašto su onda uvedene globalne promenljive? Zato što ponekad mnogobrojne procedure dele istu strukturu podataka koju u tom slučaju nema smisla neprekidno provlačiti kroz liste argumenata. Za poznavaoce fortrana globalne promenjive su zapravo COMMON zone.

slika 7.7:
#define brojeva 10
#define true 1
#define false 0
int x,y,i;  /*  Logičnije je da se ove veličine deklarišu iza main () */
main () {
int uspeh;
   i=1;
   while (i<=brojeva) {
      printf ("\nUnesi broj\n");
      scanf ("%d", &x);
      prost (x, &uspeh);
      if (uspeh) printf ("Broj je prost\n");
      else printf ("Broj nije prost\n");
      i++;
   }
   exit (0);
}
prost (x,  uspeh) int x, *uspeh; }
/* program korektno radi tek kada mu se doda:
int i,y;  */
   *uspeh = true;
   y = x/2;
   i = 2;
   while (i<=y)
   if (x % i++ == 0) { *uspeh = false; break; };
}

Globalne promenljive mogu da budu prilično opasne: zašto program sa slike 7.7 ne radi kako bi trebalo? Kako bi program, pre svega, uopšte trebalo da radi? Sa tastature se, jedan po jedan, učitava deset brojeva pa se, uz korišćenje kratkog i lošeg algoritma, proverava da li su neki od otkucanih brojeva možda prosti. Primetimo da se podaci učitavaju u petlji for (i=1; i<=10; i++) a da se u proceduri prost takođe koristi promenljiva I čija će vrednost, dakle, biti uništena. Znači li to da moramo strogo paziti na imena promenljivih koje upotrebljavamo u glavnom programu i potprogramima? Taman posla - C nam omogućava da u programu i potprogramu koristimo ista imena promenljivih ne plašeći se zabune. Da bismo se uverili u to, izbacimo oznake komentara sa slike 7.7 - tako je programu pridodata još jedna punopravna deklaracija. Rezultat? Program se korektno prevodi i korektno radi što je za početnika prilično čudno - kako to da se kompajler ne "buni" što dva puta definišemo promenljivu I?

Procedura je, kao što smo već stidljivo napomenuli, potpuni C program koji može da ima svoje konstante i svoje promenljive. U tom smislu prvi redovi procedure mogu da definišu (definišu a ne deklarišu!) nove promenljive koje važe samo u proceduri koja ih je definisala i, eventualno, u procedurama koje je ta procedura dalje pozivala - to su takozvane lokalne ili, u terminologiji C-a, automatske promenljive. Lokalne promenljive mogu da imaju jedinstvena imena ili da nose imena nekih promenjivih glavnog programa ili drugih potprograma - neće doći ni do kakve zabune. Lokalne nam promenljive omogućavaju da procedure pišemo i testiramo potpuno nezavisno od glavnog programa pa čak i da ih izdvajamo u posebne datoteke koje ćemo na kraju linkovati sa glavnim programom.

Lokalne i globalne promenljive bi trebale da vam budu dovoljne za početak - korisnici paskala, na primer, već godinama ne traže ništa osim njih! Konstruktori C-a su, međutim, dalje iskomplikovali mehanizam prenošenja argumenata i učinili ga daleko moćnijim. Ukoliko vam se, dakle, dosadašnji tekst učinio jednostavnim, pročitajte i sledeće poglavlje. U protivnom pređite na rekurzije - statičkim i eksternim promenljivima ćete se vratiti kada vam one zatrebaju.

Statičke i eksterne promenljive

Vrednosti automatskih promenljivih nestaju (ili, kako se to stručno kaže, "automatske promenljive umiru") kada se izvršavanje procedure završi - ukoliko glavni program ponovo pozove proceduru, sve će promenljive imati slučajne vrednosti (neće nužno biti jednake nuli!). Ponekad je, međutim, korisno da vrednost neke lokalne (automatske) promenljive "preživi" kraj procedure tj. da se pri novom pozivu ranija vrednost sačuva - zamislimo da neka procedura ima lokalni stek koga simulira uz pomoć niza i jednog pointera. Slika 7.8 ilustruje korišćenje ovakvih, takozvanih statičkih (deklarišu se sa static) promenljivih - verujemo da je dalji komentar nepotreban.

slika 7.8:
#define prolaza 5
main () {
int brojac;
   for (brojac = 1; brojac <= prolaza; brojac++)
   ispis ();
}
ispis() {
static int cnt = 1;
   printf ("cnt = %d\n", cnt++);
}
sl78
cnt = 1
cnt = 2
cnt = 3
cnt = 4
cnt = 5

Statičke promenljive mogu da budu globalne ili lokalne što je čudno samo na prvi pogled. Ako je statička promenljiva lokalna, ona preživljava kraj izvršavanje procedure ali to ne znači da glavni program sme da joj pristupa: sačuvana vrednost će "oživeti" tek kada "njena" procedura bude ponovo pozvana. Ovo je, verujemo, sasvim logično. Kakvog, međutim, smisla imaju globalne statičke promenljive kada globalna promenljiva "živi" u toku čitavog izvršavanja programa? Pitanje je dobro i ukazuje da vam nismo baš sve rekli: do sada smo uglavnom zamišljali strukturu jednog glavnog programa i jednog potprograma. Realni paketi se, međutim, sastoje od glavnog programa i stotina potprograma pa definicije globalnih promenljivih možemo da stavimo na mnogo mesta: ispred glavnog programa (pod pretpostavkom da je on na početku datoteke što je uobičajeno ali ne i obavezno), između glavnog programa i prvog potprograma, između prvog i drugog potprograma i tako dalje. Globalna promenljiva važi od momenta u kome je definisana; ako je definisana na samom početku datoteke, "živeće" dok se glavni program ne izvrši ali će promenljiva definisana između glavnog programa i prvog potprograma "živeti" samo dok se neki od potprograma izvršava. Pošto i globalne promenljive mogu da "umru", ima smisla definisati ih kao statičke ili tu definiciju izostaviti.

slika 7.9:
int c;
main () {
int a=3, b=4;
   c=5;
   printf ("Pre poziva procedure:\n");
   printf ("a=%d\nb=%d\nc=%d\n", a, b, c);
   drugadat (&a, &b);
   printf ("Po povratku iz procedure:\n");
   printf ("a=%d\nb=%d\nc=%d\n", a, b, c);
   exit (0);
}
extern int c;
drugadat (x, y) int *x, *y; {
   *x = 77; *y = 88;
   c = 303;
}
sl79
Pre poziva procedure: a=3 b=4 c=5
Po povratku iz procedure: a=77 b=88 c=303

Ostalo je još da razmotrimo eksterne promenljive i da (konačno) napišemo, prevedemo, povežemo i izvršimo program koji se proteže kroz dve datoteke. Program smo, zajedno sa dijalogom koji ga je preveo, dali na slici 7.9 koja sadrži i rezultate izvršavanja - program ne radi ništa pametno ali ilustruje prenos parametera između fizički razdvojenih procedura.

Parametri, pre svega, mogu da se prenose preko liste - na to smo već navikli. Primetimo, međutim, da smo u glavnom programu definisali jednu globalnu promenljivu koju smo u drugoj datoteci deklarisali (opet razlika - promenljiva je u glavnom programu definisana (tj. alociran je memorijski prostor za nju) a u drugoj datoteci deklarisana) kao extern; na taj su način ovu globalnu promenljivu koristile i procedure. Ima li neke koristi od pisanja programa koji se proteže kroz nekoliko datoteka? Naravno da ima - tako izbegavamo prevođenje čitavog paketa samo zbog sitne greške u nekoj proceduri - prevešćemo, dakle, samo ispravljenu proceduru i onda je povezati (link) sa glavnim programom i drugim potprogramima.

Funkcijske naredbe

Funkcije su, pogotovu kada radimo na C-u prilično slične procedurama - može se reći da je funkcija specijalna procedura koja ima nekoliko ulaznih i tačno jednu izlaznu veličinu. Noviteti se, naravno, najbolje razumeju na primeru.

slika 7.10:
main () {
int n;
double fact();
   printf ("Unesi broj čiji se faktorijel računa\n");
   scanf ("%d", &n);
   printf ("%d!=%.0f\n", n, fact (n));
}
double fact (n) int n; {
int i;
double y;
   y = 1.0;
   for (i=1; i<=n; i++)
   y=y*i;
   return (y);
}

Pogled na sliku 7.10 treba da nas podseti na program za računanje faktorijela sa slike 6.4. Koncentrišimo se na prvi novitet, double fakt (n);. Fakt je očito ime funkcije, N je argument (i to celobrojan zbog deklaracije koja sledi) ali šta bi mogla da označava reč double ispred imena funkcije? Svaka funkcija vraća jednu vrednost pa moramo da kažemo kog je ta vrednost tipa - u našem je slučaju faktorijel racionalan broj pa smo napisali da je funkcija fakt racionalna (da smo ovu deklaraciju izostavili, podrazumevalo bi se da je rezultat int što je obično i slučaj). Primetimo da tip funkcije mora da se definiše i u glavnom programu.

Iza imena funkcije i deklaracije fiktivnog argumenta pronalazimo definicije lokalnih (automatskih) promenljivih I i Y (i funkcije mogu da imaju lokalne promenljive i lokalne konstante pri čemu važe svi raniji saveti i sva ranije izložena upozorenja) i, na kraju, niz izvršnih naredbi koje računaju faktorijel. Primetimo, ipak, da se funkcija završava sa return (y); na taj način stavljamo kompajleru na znanje da je vrednost koja treba da se vrati u glavni program Y a ne I ili možda Y-1. Vrednost funkcija se, dakle, na kraju obavezno vraća u glavni program korišćenjem službene reči return.

Iako procedura po definiciji ima jedan izlazni argument, njene naredbe mogu da menjaju i sadržaje nekih globalnih promenljivih pa čak i nekih argumenata prenetih po imenu. Ovo vam, međutim, ne preporučujemo - ako već imate više izlaznih veličina, koristite proceduru!

Rekurzije

Samo je po sebi jasno da nema nikakve prepreke da neka procedura (ili funkcija) poziva neku drugu proceduru ili funkciju i tako do prilično velike dubine koja zavisi isključivo od raspoloživog RAM-a. Moguće je, takođe, da procedura ili funkcija pozove samu sebe čime dolazimo do pojma rekurzije.

Na prethodnim smo stranama napisali bezbroj programa koji računaju faktorijele množeći sve prirodne brojeve manje od zadatog. Matematički nastrojeni čitaoci će primetiti da izraz 1*2*3*...*N nije prava definicija faktorijela; pravilnije je napisati:

slika 7.11:
N! = 1, za N=0
N! = N*(N-1)!, za N>0

Pokušajmo da dokažemo da je 3! isto što i 1*2*3. Po gornjoj definiciji je 3!=3*2!. Dalje je 2!=2*1! a 1!=1*0!. 0! je 1 po definiciji pa je onda 1!=1*1=1; tada je 2!=2*1!=2*1=2 a 3!=3*2!=3*2=6. Slično tome bismo mogli da pokažemo da je 5!=120 (pokušajte!) i da na taj način bar poverujemo u gornju definiciju faktorijela ako ne i da matematički dokažemo njenu ekvivalentnost sa ranije izloženom formulom. Sastavimo sada novu verziju funkcije fakt:

slika 7.12:
main () {
int n;
double fact();
   printf ("Unesi broj čiji se faktorijel računa\n");
   scanf ("%d", &n);
   printf ("%d!=%.0f\n", n, fact (n));
}
double fact (n) int n; {
   if (n==0) return (1.0);
   else return (fact (n-1) * n);
}

Na prvi pogled bi se reklo da je ovako dobijena funkcija mnogo bolja od one koju smo napisali pre nego što smo znali za rekurzije: manje linija i ni jedna jedina pomoćna promenljiva (ranije smo koristili dve, Y i I). Moramo, međutim, da kažemo da je funkcija sa slike 7.12 užasan potrošač memorije: za N=25 će rekurzivno biti pozvano 25 potprograma. Ako se za pozivanje svakog potprograma troši po osam bajtova za beleženje adrese povratka i drugih informacija plus još bar 6 bajtova za smeštanje novog argumenta (koji se uvek zove N), računar sa tridesetak kilobajta RAM-a bi mogao da izračuna najviše 2000! (2000! se, jasno, ne može izračunati jer je opseg brojeva sa kojima barata računar ograničen dok je 2000! broj sa preko 5730 cifara) dok u prvom primeru, kada smo koristili petlju, takvog ograničenja nije bilo. Uopšte su programi koji koriste rekurzije naoko kratki, elegantni i jednostavni ali su veliki konzumatori memorije i računarskog vremena pa ih treba izbegavati kada je to moguće (a moguće je uvek, uz više ili manje problema).

Završavajući ovu kratku priču o rekurzijama, ne možemo da ne navedemo jedan divan primer njihove upotrebe. Možda vam je poznat problem nazvan "Kule Hanoja": priča se da u gradu Benaresu postoji podzemni hram u kome je induski bog Brama pri stvaranju sveta postavio tri dijamantska štapića i na jedan od njih stavio 64 zlatna kotura različitih veličina: na zemlju je stavio najveći kotur, na njega manji i tako sve do najmanjeg kotura koji se nalazi na vrhu tajantstvene kupe. Stotinu sveštenika danju i noću prenosi koturove sa prvog na drugi štapić služeći se trećim kao pomoćnim i poštujući ograničenja koja je zadao Brama: prsteni smeju da se premeštaju samo jedan po jedan i nikada veći prsten ne sme da se stavi preko manjeg. Kada sveštenici budu obavili ovaj na prvi pogled jednostavan posao, nastupiće kraj sveta (ne plašite se: jednostavan račun pokazuje da nam je do kraja sveta ostalo još bar 500 milijardi godina). Nije ni malo lako sastaviti program koji rešava ovaj problem bez rekurzija; rekurzivni program je, kao što vidimo sa slike 7.13, prava dečja igra!

slika 7.13:
/*   Rekurzivno rešenje Kula Hanoja
(C) 1983, 1987 D. Ristanović   */
#define maxn 64
main () {
   int n;
   printf ("Koliko diskova?\n");
   scanf ("%d", &n);
   if (n>=maxn) printf ("Cekaj do kraja sveta!");
   else prenos (n, 1, 2, 3);
}
prenos (n, x, y, z) int n,x,y,z; {
   if (n!=0) {
      prenos (n-1, x, z, y);
      printf ("Sa %d na %d\n", x, y);
      prenos (n-1, z, y, x);
   }
}

Argumenti glavnog programa

Do sada smo napisali podosta programa na C-u i svaki od njih započeli sa main (). Šta znače ove zagrade? One, jasno, uokviruju nepostojeće argumente glavnog programa tj. procedure main. A šta ako ti argumenti postoje?

slika 7.14:
main (argc, argv) int argc; char *argv[]; {
   while (--argc > 0)
   printf ((argc > 1) ? "%s " : "%s\n", *++argv);
}

Otkucajte, prevedite i povežite program sa slike 7.14 a onda otkucajte "ponovi ovo što kucam" - na ekranu će se pojaviti tekst "ovo što kucam". Program je, dakle, uspeo da analizira komandnu liniju koju smo otkucali i da je prepiše na ekran; obrada je, jasno, mogla da bude i mnogo složenija. Da vidimo kako se ovaj specijalni efekat postiže.

Argumenti funkcije main su niz alfanumerika argv i celobrojna promenljiva argc. Argc je, pogađate, broj argumenata, argv[1] je prvi argument, argv[2] drugi i tako dalje - smatra se da je argument bilo koji niz slova u kome se ne nalaze blanko simboli. Obzirom da u C-u dimenzije nizova počinju od nule, argv[0] bi trebao da sadrži naredbu kojom je program pozvan (u našem slučaju reč ponovi) ali se to na PC-ju nije dešavalo - uputstva za LC i MWC kompajlere detaljno objašnjavaju ovu pojavu pri čemu se sva objašnjenja svode na šest reči "mi to nismo umeli da uradimo".

Program sa slike 7.14, najzad, ispravno detektuje situaciju u kojoj korisnik nije otkucao ni jedan argument. Dobar sistemski program bi u tom slučaju trebao da postavi pitanje i primi argument koji će korisnik naknadno otkucati.

Rad sa datotekama

C je jezik za sistemske programere što znači da je njegov rad sa datotekama prilagođen razvoju operativnih sistema - ako treba da računate plate ili vodite magacin, pređite na kobol! Verujemo da ćete, kao početnik, minimalno operisati sa datotekama ali nam se ipak čini da bi ovaj umetak bio nepotpun bez poglavlja koje ste upravo počeli da čitate.

slika 8.1:
/*  MTYPE.C  */
#include "stdio.h"
main (argc, argv) int  argc; char *argv[]; {
FILE *fp, *fopen();
   if (argc == 1)  /* nema argumenata  */
      copyfile (stdin);
   else
      while (--argc > 0) {
         if ((fp=fopen(*++argv, "r")) == NULL) {
            printf ("Datoteka %s nije pronađena\n", *argv);
            break;
         }
         else {
            copyfile (fp);
            fclose (fp);
         }
      }
   }
   copyfile (pointer) FILE *pointer; {
   int c;
   while ((c = getc (pointer)) != EOF)
      putc (c, stdout);
}

Program 8.1 će obogatiti operativni sistem vašeg računara naredbom mtype koja ispisuje sadržaje nekoliko datoteka: mtype a.dat b.dat će nadovezati tekst iz b.dat na sadržaj a.dat i rezultat ispisati na ekranu. Iako je program sam po sebi sasvim upotrebljiv, nas prevashodno interesuje njegova struktura.

Kao i sve druge strukture, datoteku (file) moramo najpre da deklarišemo. Prva linija, #include "stdio.h", predstavlja uputstvo kompajleru: na početak programa treba ubaciti kompletnu datoteku stdio.h (ne zaboravite da je prekopirate na RAM disk) koja sadrži definicije funkcija i makroa neophodnih za rad sa datotekama. Datoteku smo deklarisali sa FILE *fopen(), *fp. Pravo otvaranje datoteke je smešteno nekoliko redova docnije: fp = fopen (ime, "r"). "Ime" je ime datoteke koje smo prethodno "pokupili" iz komandne linije dok "r" označava da datoteku otvaramo za čitanje (read); "w" bi značilo upis (write) tj. uništavanje ranijeg sadržaja datoteke a "a" nadovezivanje (append) na taj sadržaj.

Funkcija fopen, vidimo, vraća logički broj koji je dodeljen datoteci: docniji rad se svodi na operacije sa tim logičkim brojem dok nas pravo ime datoteke na disku ne zanima. Ukoliko datoteka ne postoji ili iz nekog razloga ne može da bude otvorena (disketa je, na primer, zaštićena od upisa), fopen će vratiti vrednost NULL (NULL je konstanta definisana u stdio.h) što smo mi iskoristili da još jednom ilustrujemo naredbu break.

Pošto smo otvorili datoteku, treba da je čitamo a za to se koristi funkcija getc(fp) gde je fp logički broj datoteke. Datoteku, očito, čitamo znak po znak ali te znake prenosimo u celobrojnu a ne alfanumeričku promenljivu. Zašto? Osim ASCII karaktera, getc može da vrati i broj EOF (konstanta koja obično ima vrednost -1) što označava da je čitanje datoteke završeno.

Šta da radimo sa pročitanim karakterima? Program sa slike 8.1 ih šalje u drugu datoteku nazvanu stdout. Šta je sad pa to? U normalnim je uslovima stdout ekran a stdin tastatura ali neki operativni sistemi (računajući i MS DOS) omogućavaju da se rezultati izvršavanja nekog programa usmere u neku datoteku - program sa slike 8.1 možemo da startujemo i za mtype a.dat b.dat > c.dat pa će sadržaji datoteka A i B biti prepisani u datoteku C a ne na ekran. Pošto se pozivi getc(stdin) i putc(znak, stdout) veoma često koriste, možete da ih zamenite sa getchar() odnosno putchar(znak); getchar i putchar su makro definicije iz stdio.h.

Mana programa sa slike 8.1 je što, u slučaju da datoteka ne postoji, šalje poruko o grešci na stdout - ako smo otkucali mtype a.dat b.dat > c.dat pri čemu a.dat ne postoji, na ekranu se neće pojaviti nikakva poruka koja bi to signalizirala. Zato smo na slici 8.2 ovu poruku preusmerili u datoteku stderr (koristili smo novu formu funkcije printf zvanu fprintf - verujemo da je semantika i sintaksa očigledna) koja uvek predstavlja ekran. Iz programa smo izašli sa exit(1) što bi trebalo da signalizira grešku; kompajleri za PC koje smo koristili, međutim, ignorišu ovaj signal.

slika 8.2:
/*  MTYPE.C  */
#include "stdio.h"
main (argc, argv) int  argc; char *argv[]; {
FILE *fp, *fopen();
   if (argc == 1)  /* nema argumenata  */
      copyfile (stdin);
   else
      while (--argc > 0)
         if ((fp=fopen(*++argv, "r")) == NULL) {
            fprintf (stderr, "Datoteka %s nije pronađena\n", *argv);
            break;
         }
         else {
            copyfile (fp);
            fclose (fp);
         }
}
copyfile (pointer) FILE *pointer; {
int c;
   while ((c = getc (pointer)) != EOF)
      putc (c, stdout);
}

Ostalo je još da datoteku, pošto smo završili rad sa njom, propisno zatvorimo koristeći fclose(fp). Zašto je zatvaranje potrebno? Na taj se način, pre svega, podaci koji se još nalaze u baferu prepisuju na disk što znači da bi prekid napajanja računara mogao da osakati nezatvorene datoteke. Zatvaranjem se, osim toga, oslobađaju kanali za nove datoteke - većina operativnih sistema dozvoljava korisniku da otvori samo određen broj fajlova (na PC-ju ovaj broj možete da povećate menjajući sadržaj CONFIG.SYS). Standardnom C-a je, istina, predviđeno da se datoteke automatski zatvore kada se izvršavanje programa završi ali vam ipak predlažemo da i sami koristite fclose: zlatno je pravilo da se datoteke otvaraju tek kada je njihov sadržaj potreban i zatvaraju čim je rad sa njima završen!

Puj, pike ...

Standardna biblioteka sadrži i jednu neobičnu funkciju ungetc(c, fp) - C je znak a FP logički broj datoteke. Ova se funkcija koristi za vraćanje jednog znaka nazad u datoteku - pročitali smo ga ali smo onda uvideli da nam taj znak ne treba! Primenu ove funkcije nismo uspeli da ilustrujemo primerom što ne znači da je nećete koristiti - često se događa da neki bajt predstavlja kraj jedne strukture i početak druge. Ako svaku strukturu obrađuje različita procedura koja očekuje "uvodni" bajt, moraćete da vratite pročitani podatak u datoteku pre nego što je pozovete!

Možda ste pomislili da su ove "razdvojene strukture" zapravo linije teksta. Na sreću, C obezbeđuje komforan rad sa linijama bez potrebe za funkcijom ungetc!

Rad sa linijama

Kada radimo sa tekstom, navikli smo da on bude podeljen na linije jednakih ili (češće) nejednakih dužina. Za računar, sa druge strane, linije ne postoje - tekst se smešta sekvencijalno u memoriju. Pošto su ljudi napravili računare a ne računari ljude, naše je gledište "starije" pa su siroti kompjuteri morali da mu se prilagode: tako se na kraj svake linije stavlja jedan specijalni znak (na primer EOLN=&0D - nezavisno od koda, ovaj se znak obeležava sa \n) koji, istina, predstavlja bacanje prostora ali omogućava da jasno razgraničimo delove teksta.

Šta će se događati ako tekst učitavamo znak po znak, ne obraćajući pažnju na linije? Znak EOLN (end of line) će biti karakter kao i svaki drugi što znači da će ga funkcija getc sasvim normalno tretirati. Za nas je, međutim, korisno da sukcesivne linije smestimo u sukcesivne elemente alfanumeričkog niza što znači da nam je potrebna procedura koja čita neku datoteku liniju po liniju. Ovakva je procedura sadržana u standardnoj biblioteci pa smo je iz nje prepisali na sliku 8.3.

slika 8.3:
/*  FGETS.C iz standardne biblioteke  */
#include "stdio.h"
char *fgets (s,n,iop)  /* Najviše N znakova) */
int  n; char *s; register FILE *iop;
{
register int c;
register char *cs;
   cs = s;
   while (--n > 0 && (c=getc(iop)) != EOF)
   if ((*cs++ = c) == '\n')
      break;
   *cs = '\0';
   return ((c == EOF && cs == s) ? NULL : s);
}

Funkcija fgets(line, maxline, fp) prepisuje liniju teksta iz datoteke čiji je logički broj FP u alfanumeričku promenljivu line pri čemu se prepisivanje završava kada se naiđe na \n ili kada se učita maxline karaktera. Fputs(line, fp) prepisuje string line u datoteku čiji je logički broj FP.

Ako pažljivo proučite sliku 8.3, videćete da su neke promenljive definisane kao register. To je uputstvo kompajleru da po mogućnosti vrednosti ovih promenljivih drži u registrima procesora jer će se njima izuzetno često pristupati. Svi će kompajleri prihvatiti ovu sintaksu ali je pitanje da li će uvažiti vašu preporuku; jednostavan način da saznate da li su promenljive zaista u registrima ne postoji!

Sekvencijalne i ostale datoteke

Sve datoteke koje smo do sada opisivali su sekvencijalne što znači da se podaci u njih upisuju i čitaju isključivo od početka prema kraju - ako vam je potreban hiljaditi zapis, morate da pročitate i prethodnih 999! Iako su datoteke u praksi često sekvencijalne, nemogućnost rada sa relativnim i indeksnim datotekama je strahovita slabost standardnog C-a. Njegovi su tvorci, međutim, smatrali da se relativne i indeksne datoteke ne mogu uvesti na jednostavan a ipak portabilan način tj. na način koji bi bio primenjiv na svim kompjuterima. Vaš C kompajler sasvim sigurno podržava relativne datoteke ali ćete morati da ih upoznate čitajući njegovo uputstvo; ove ćete datoteke i onako retko koristiti.

Mašinski su zavisne čak i procedure koje omogućavaju da "premotate" datoteku na početak pošto ste završili njeno čitanje. Ukoliko vam uputstvo za upotrebu kompajlera nije pri ruci, pokušajte da zatvorite datoteku i da je onda ponovo otvorite - operacija je nešto sporija ali je vrlo efikasna.

C i alfanumerici

Ako izuzmemo bejzik i njegove naslednike poput komala, programski su jezici relativno slabo "naoružani" za rad sa alfanumericima: paskal je na ovom polju katastrofalan a ni fortran 77 se baš nije proslavio! Što se C-a tiče, rad sa alfanumericima je prilično potpuno rešen ali nije sređen kao na bejziku - alfanumerici se mahom svode na pointere što često izaziva zabune. Pođimo, ipak, nekim redom.

Već znamo da C tretira stringove na dva međusobno gotovo ekvivalentna načina: kao matricu karaktera i kao memorijsku zonu na koju pokazuje pointer. Istom se stringu može pristupati na dva načina što ilustruje i slika 9.1.

slika 9.1:
main () {
char *ispis;
int   i;
   for (i=1; i < 3; i++)
   {
      ispis = "C je super jezik!";
      printf ("%s\n", ispis);
      ispis [5] = 'g';
      ispis [6] = 'l';
      ispis [7] = 'u';
      ispis [8] = 'p';
      ispis [9] = ' ';
      printf ("%s\n", ispis);
   }
}

Svaki dobar učenik ove Škole će pogoditi šta ovaj program treba da radi ali će malobrojni pogoditi rezultate: umesto da se naizmenično ispisuju poruke "C je super jezik" i "C je glup jezik", ova druga poruka apsolutno preovladava. Kako to? Linija ispis="C je super jezik" će, umesto da prepiše string u neku slobodnu zonu memorije, usmeriti pointer ispis na ovaj string što znači da će promena ispis-a efektivno promeniti samu naredbu koja sada glasi ispis="C je glup jezik"; reč "super" je nepovratno izgubljena. Zanimljivo je da ni u jednom udžbeniku C-a nismo naišli na primer poput ovoga - zar se stvarno toliko teško dosetiti slične cake?

slika 9.2:
main () {
char ispis [100];
int  i;
   for (i=1; i < 3; i++)
   {
      strcpy (ispis, "C je super jezik!");
      printf ("%s\n", ispis);
      ispis [5] = 'g';
      ispis [6] = 'l';
      ispis [7] = 'u';
      ispis [8] = 'p';
      ispis [9] = ' ';
      printf ("%s\n", ispis);
   }
}

Slika 9.2 prikazuje korektnu verziju programa 9.1 - za dodeljivanje stringa smo, umesto znaka jednakosti, koristili funkciju strcpy. Sličnu praksu savetujemo i vama - operišite sa stringovima isključivo pozivajući odgovarajuće potprograme. A tih potprograma u standardnoj biblioteci nema baš malo! Da ne biste mislili da se radi o nekim čarolijama sa mašincem, na slici 9.3 dajemo listing funkcije strcpy(x, y) koja kopira string Y u string X - kao da smo na bejziku napisali X$=Y$. Zanimljivo je da argumenti strcpy (i bilo koje druge funkcije) mogu da budu matrice karaktera ili pointeri - ako baš želite da se lukzuzirate, jedan argument može da bude matrica a drugi pointer!

slika 9.3:
strcpy (s, t) char *s, *t; {
   while (*s++ = *t++);
}

Slika 9.4 nabraja funkcije za rad sa stringovima kojima je opremljena standardna biblioteka; obzirom da se bejzik programeri uvek rado petljaju sa alfanumericima, potrošićemo malo prostora na detaljniji opis ovih funkcija.

Strlen (x) je funkcija koja vraća dužinu stringa X. Sećamo se da se završetak stringa obeležava nula bajtom (\0) što znači da je strlen obična while petlja.

Strcat (x, y) je konkatenacija: string Y se nadovezuje na string X.

Strcmp (x, y) poredi stringove X i Y. Ukoliko su jednaki, funkcija vraća nulu. Ukoliko je X "veći" (poređenje se, jasno, vrši po abecedi ili, tačnije rečeno, po ASCII setu), funkcija vraća pozitivan a ako je X "manji" negativan broj. Zapamtite da se stringovi nikada ne porede naredbama poput if (X==Y) - na taj biste način poredili pointere pa bi rezultat bio potpuno bezvredan.

Sprintf (string, "format", argumenti) je zamena za bejzik funkciju ASC: ova procedura omogućava da konvertujete jedan ili nekoliko argumenata u ASCII formu koristeći sve blagodeti procedure printf - ispis celih i racionalnih brojeva, heksadekadni ili oktalni sistem i tako dalje.

Sscanf (string, "format", argument) je, pogađate, zamena za VAL: string se "čita" prema zadatom formatu i prenosi u numeričku promenljivu. Ne smemo da zaboravimo da ispred imena promenljive stavimo znak and (&) - ona se u proceduru prenosi po imenu.

slika 9.4:
Funkcija Bejzik ekvivalent
Sprintf (a, "format", b)
a$= str$(b)
Sscanf (a, "format", &b)
b = val(a$)
Strcat (x, y)
x$ = x$ + y$
Strcmp (x, y)
IF x$=y$ THEN =0
IF x$>y$ THEN =1
IF x$<y$ THEN =-1
Strcpy (x, y)
x$=y$
Strlen (x)
len (x$)

Primer 9.4 će, verujemo, poslužiti kao dobar model za rad sa funkcijama sscanf i sprintf - verujemo da ćete bez problema zaključiti šta program radi i protumačiti rezultate.

Lepa osobina C-a je što možete da ga proširujete po volji - ako vam neke funkcije nedostaju, ništa vas ne sprečava da ih sami uvedete! Nama su nedostajale funkcije MID$, LEFT$ i RIGHT$ iz Microsoftovog bejzika pa smo ih, na slici 9.5, zamenili odgovarajućim procedurama. Upotreba ovih procedura je sasvim standardna s tim što je rezultujući string jedan od argumenata (umesto A$=LEFT$(B$,3) pišemo left (&a, b, 3)).

slika 9.5:
#define maxs 255
main () {
char ulaz [maxs], izlaz [maxs];
int  brojd;
   printf ("Unesi neki heksadekadni broj: ");
   scanf ("%s", ulaz);
   sscanf (ulaz, "%x", &brojd);
   printf ("To je %d dekadno\n", brojd);
   sprintf (izlaz, "%o", brojd);
   printf ("Ili %s oktalno\n", izlaz);
}

Šta nedostaje?

Deset poglavlja je sasvim dovoljno za kratku Školu C-a - verujemo da vam je tekst čije čitanje upravo privodite kraju omogućio da upoznate ovaj interesantan programski jezik i počnete da programirate na njemu. Prostor koji nam je bio na raspolaganju je, na žalost, nedovoljan da bismo pokrili čitav C - iako se ovaj jezik sastoji od relativno malog broja naredbi, njegova je sintaksa i semantika prilično složena ali su zato mogućnosti ogromne.

Moramo da kažemo da smo, kroz čitav ovaj umetak, pokušavali da prikažemo C kao pristojan viši programski jezik. Sada, kada ste na putu da prevladate početne teškoće, saopštićemo vam istinu: C je "prljav" jezik, premazan sa šesnaest nijansi crne boje; to je jezik koji omogućava sve vrste trikova, nestrukturiranih caka i drugih ataka na operativni sistem vašeg kompjutera. Otisnite se malo dublje u vode pointera, registarskih promenljivih, alokacije memorije i sličnih stvari pa ćete dvadeset puta na dan "rušiti sistem" tj. toliko zbunjivati siroti kompjuter da ćete morati da ga isključujete - nešto slično se paskal programerima dešava jednom ili ni jednom u životu! C je jezik koji retko prijavljuje greške tj. jezik koji vam dopušta da radite sve što želite - zato ga valjda hakeri i obožavaju!

slika 9.6:
/*
Skup procedura za rad sa stringovima
po ugledu na bejzik
Dejan Ristanovic  1987. */
#define maxstr 256
mid (s1, s2, start, count) char s1 [], s2 []; int  start, count; {
int i, j, fine, strlen();
   j=0; start--;
   if (count == 0 || start+count > strlen (s2))
      fine = strlen (s2);
   else
      fine = start+count-1;
   for (i=start; i<=fine; i++)
   s1 [j++] = s2 [i];
   s1 [j] = '\0';
}
right (s1, s2, count) char s1 [], s2 []; int  count; {
int start;
   start=strlen(s2)-count+1;
   if (start<=0)
      s1[0]='\0';
   else mid (s1, s2, start, 0);
}
left (s1, s2, count) char s1 [], s2 []; int  count; {
   if (count==0)
      s1[0]='\0';
   else mid (s1, s2, 1, count);
}
main () {
char s1 [maxstr], s2 [maxstr], s3 [maxstr], s4 [maxstr];
   strcpy (s2, "Ovo je proba");
   mid   (s1, s2, 3, 4);
   left  (s3, s2, 5);
   right (s4, s2, 6);
   printf ("%s\n%s\n%s\n%s\n", s2, s1, s3, s4);
}

Literatura

1. Lattice C-Compiler, Lifeboat Associates; New York 1983.

2. MWC86 C Compiler, Mark Williams Company 1983.

3. Hancock, I., Krieger, M.: The C Primer, McGraw-Hill Inc. 1982.

4. Kernigham, B.W., Ritchie, D.M.: The C Programming Language, Prentice-Hall Inc. 1978.

5. Mihajlović, R.: Poliklinika C (1 i 2), Računari 16, Računari 17; Beograd 1986.

6. Plum, T.: Learning to Program in C, Plum-Hall Inc. 1983.

7. Purdum J.: C Programming Guide, Que Corp. 1983.

8. Traister, R.: Going from BASIC to C, Prentice-Hall Inc.; New Jersey 1985.

9. Traister, R.: Programming in C for the Microprocessor User, Prentice-Hall Inc.; New Jersey 1984.