Kezdőlap

|

Mi a kreditvadasz.hu Egy felsőoktatási közösségi oldal amely segít kapcsolatot tartani a hallgatók között, így segítséget nyújt a sikeres tanulmányokhoz...

Kernighan - Ritchie - A C programozási nyelv

Országok listájaHungaryDebreceni EgyetemInformatikai KarProgramtervező informatikusMagas Szintű Programozási Nyelvek 1.JegyzetekKernighan - Ritchie - A C programozási nyelv

2009.02.22 12:38:16
(10)
Szerző: Simintoo
Cimkék: c,, programozás, kernighan,, ritchie, prog1


Az alábbi szöveg egy formázás és képek nélküli előnézete a dokumentumnak. A tökéletes megjelenítéshez jelentkezz be, majd töltsd le a dokumentumot.
A C programozási nyelv

ElQszó a magyar kiadáshoz

A C programnyelvet eredetileg a Bell Laboratóriumban az UNIX operációs rendszerhez, az alatt fejlesztették ki PDP-11 számítógépen. A kifejlesztése óta eltelt évek során bebizonyosodott, hogy a C nem egyszerqen egy a napjainkban gombamód szaporodó programnyelvek közül. Korszerq vezérlési és adatszerkezetei, rugalmassága, könnyq elsajátíthatósága széles alkalmazási területet biztosított számára, különösen a 16 bit-es mikroprocesszorok megjelenése óta rendkívül sok gépen dolgoznak C nyelven. C fordító készült olyan gépekre, mint az IBM System/370, a Honeywell 6000 és az Interdata 8/32. A nyelv a kutatás-fejlesztési, tudományos célú programozás népszerq eszközévé vált.
Magyarországon szintén egyre több olyan számítógép mqködik, amely alkalmas a C megvalósítására. Ilyenek a hazai gyártmányok közül a TPA-11 sorozatú, az R-11 , a szocialista gyártmányok közül az SZM-4 számítógépek, de meg kell említenünk a hazánkban ugyancsak elterjedt PDP-11 sorozat tagjait is. Így érthetQen a magyar számítástechnikai szakemberek körében mind nagyobb az érdeklQdés a C nyelv iránt, egyre többen szeretnének megtanulni programozni ezen a nyelven.
Ebben szeretnénk segíteni e könyv megjelentetésével, amely didaktikusan, bQ példa- és gyakorlatanyaggal kiegészítve szól a C összetevQirQl, jellemzQirQl, de tartalmazza a nyelv referencia-kézikönyvét is. Az Olvasó a legavatottabb forrásból meríthet : a világhírq szerzQpáros egyik tagja, Dennis Ritchie a C nyelv tervezQje, a másik, Brian W. Kernighan több, magyarul is megjelent nagy sikerq szakkönyv szerzQje. Reméljük, mind a kezdQ, mind a gyakorlott C-programozók haszonnal forgatják majd a mqvet.

A Kiadó

ElQszó

A C általános célú programnyelv. Tömörség, a korszerq vezérlési és adatstruktúrák használata, bQséges operátorkészlet jellemzi. Nem nevezhetQ sem nagyon magas szintq, sem nagy nyelvnek, és nem kötQdik egyetlen speciális alkalmazási területhez sem. Ugyanakkor a megkötések hiánya, az általános jelleg sok magas szintq nyelvnél kényelmesebbé és hatékonyabbá teszi.
A C nyelvet tervezQje, Dennis Ritchie eredetileg az UNIX operációs rendszer programnyelvének szánta. Az operációs rendszer, a C fordító és lényegében az összes UNIX alkalmazási program (a könyv eredetijének a nyomdai elQkészítéséhez használt szoftver is) C nyelven íródott. Dennis Ritchie az elsQ C fordítót PDP- 11-en írta meg, de azóta néhány más gépre, így az IBM System/370-re, a Honeywell 6000-re és az Interdata 8/32-re is készült C fordító: A C nyelv nem kötQdik szorosan egyetlen hardverhez vagy rendszerhez sem, könnyen írhatunk olyan programokat, amelyek változtatás nélkül futnak bármely más, a C nyelvet támogató gépen.
Könyvünkkel a C nyelvq programozás elsajátításához szeretnénk segítséget adni. Az olvasó már az Alapismeretek c. fejezet megértése után elkezdhet programozni. A könyv ezután külön-külön fejezetben ismerteti a C nyelv fQ összetevQit, majd referencia-kézikönyv formájában is összefoglalja a nyelvet. Az anyag túlnyomórészt példaprogramok írásából, olvasásából és módosításából áll, nem száraz szabálygyqjteményt adunk az olvasó kezébe. A legtöbb példa teljes, ellenQrzött, mqködQképes program, és nem csupán elszigetelt programrész. Könyvünkben nemcsak a nyelv hatékony használatát kívántuk ismertetni. Törekedtünk arra is, hogy jó stílusú, áttekinthetQ, hasznos algoritmusokat és programozási elveket mutassunk be.
A könyv nem bevezetQ jellegq programozási segédkönyv; feltételezi, hogy az olvasó ismeri a programozás olyan alapfogalmait, mint: változók, értékadó utasítások, ciklusok, függvények. [A C nyelvben használatos terminológia szerint a szubrutinokat függvényeknek (functions) nevezik. A ford.] Ugyanakkor a könyv alapján egy kezdQ programozó is megtanulhatja a nyelvet, bár szüksége lehet jártasabb kolléga segítségére.
Tapasztalataink szerint a C sokféle feladat megfogalmazására alkalmas, kellemes, kifejezQ és rugalmas nyelv. Könnyen elsajátítható, és aki megismerte, szívesen használja. Reméljük, hogy könyvünk segítséget nyújt a nyelv hatékony használatában.
A könyv megszületéséhez és a megírásakor érzett örömünkhöz nagyban hozzájárultak barátaink, kollégáink gondolatgazdag bírálatai és javaslatai. Különösen hálásak vagyunk Mike Bianchinak, Jim Blue-nak, Stu Feldmannek, Doug Mcllroynak, Bill Roome-nak, Bob Rosinnek és Larry Roslernek, akik figyelmesen elolvasták a könyv több változatát is. Al Aho, Steve Bourne, Dan Dvorak, Chuck Haley, Debbie Haley, Marion Harris, Rick Holt, Steve Johnson, John Mashey, Bob Mitze, Ralph Muha, Peter Nelson, Elliot Pinson, Bill Plauger, Jerry Spivack, Ken Thompson és Peter Weinberger megjegyzéseikkel sokat segítették munkánkat különbözQ fázisaiban. Köszönet illeti továbbá Mike Lesket és Jim Ossanát a könyv szedésében való értékes közremqködésükért.

Brian W. Kernighan
Dennis M. Ritchie

Bevezetés

A C általános célú programnyelv. Történetileg szorosan kapcsolódik az UNIX operációs rendszerhez, mivel ezen rendszer alatt fejlesztették ki és mivel az UNIX és szoftvere C nyelven készült. A nyelv azonban nem kötQdik semmilyen operációs rendszerhez vagy géphez. Noha _ rendszer-programnyelvnek szokás nevezni, mivel operációs rendszerek írásában jól használható, ugyanolyan célszerqen alkalmazható nagyobb numerikus, szövegfeldolgozó és adatbázis-kezelQ programok esetében is.
A C viszonylag alacsony szintq nyelv. Ez nem lebecsülést jelent, csupán azt, hogy a C nyelv - mint a legtöbb számítógép - karakterekkel, számokkal és címekkel dolgozik. Ezek kombinálhatók, és az adott gépen rendelkezésre álló aritmetikai és logikai operátorokkal mozgathatók.
A C nyelvben nincsenek olyan mqveletek, amelyekkel összetett objektumokat, pl. karakterláncokat, halmazokat, listákat vagy tömböket egy egésznek tekinthetnénk. Hiányzik például azoknak a PL/1 mqveleteknek a megfelelQje, amelyek egy egész tömböt vagy karakterláncot kezelnek. A nyelvben nincs más tárfoglalási lehetQség, mint a statikus definíció és a függvények lokális változóinál alkalmazott verem elv. Nincs továbbá olyan, hulladék tárterületek összegyqjtésére alkalmas mechanizmus (garbage collection), mint amilyet az ALGOL 68 nyújt. Végül pedig maga a C nyelv nem biztosít be- és kiviteli szolgáltatásokat: nincsenek read és write utasítások, sem rögzített állományelérési (file-elérési) módszerek. Az összes ilyen magasabb szintq tevékenységet explicit módon hívott függvényekkel kell megvalósítani.
Hasonlóképpen a C nyelv csak egyszerq, egy szálon futó vezérlésátadási struktúrákat tartalmaz: ellenQrzéseket, ciklusokat és alprogramokat, de nem teszi lehetQvé a multiprogramozást, a párhuzamos mqveleteket, a szinkronizálást vagy párhuzamos rutinok (korutinok) használatát.
Bár e szolgáltatások némelyikének hiánya súlyos hiányosságnak tqnhet, a nyelv szerény méretek közé szorítása valójában elQnyökkel járt. A C nyelv viszonylag kicsi, ezért kis helyen leírható és gyorsan elsajátítható. A C fordító egyszerq és tömör lehet, ugyanakkor könnyen megírható: a jelenlegi technológiával egy új gépen futó fordító néhány hónap alatt elkészíthetQ, és kódjának 80%-a várhatólag közös lesz a már létezQ fordítók kódjával. Ez nagyban segíti a nyelv terjedését, a programok cseréjét. Mivel a C nyelvben definiált adattípusokat és vezérlési szerkezeteket a legtöbb mai számítógép közvetlenül támogatja, kicsi lesz az önálló programok megvalósításához futási idQben szükséges rutinkönyvtár, amely a PDP-11 -en például csak a 32 bit-es szorzást és ; osztást végrehajtó rutinokat, illetve a szubrutinba való belépést és az onnan való kilépést szolgáló szekvenciákat tartalmazzák. Természetesen a nyelv valamennyi megvalósítását kiterjedt, az adott géphez illeszkedQ függvénykönyvtár teszi teljessé. A függvények biztosítják a be- és kiviteli mqveletek elvégzését, a karakterláncok kezelését és a tárfoglalási mqveletek végrehajtását. Mivel azonban csak explicit módon hívhatók, szükség esetén elhagyhatók. Ezenkívül C programként gépfüggetlen módon is megírhatók.
Minthogy a C a mai számítógépek képességeit tükrözi, a C nyelvq programok általában elég hatékonyak ahhoz, hogy ne kelljen helyettük assembly programokat írni. Ennek legjellemzQbb példája maga az UNIX operációs rendszer, amely majdnem teljes egészében C nyelven íródott. 13000 sornyi rendszerkódból csak a legalacsonyabb szinten elhelyezkedQ 800 sor készült assemblyben. Ezenkívül majdnem minden UNIX alkalmazási szoftver forrásnyelve is a C; az UNIX felhasználók túlnyomó többsége (beleértve e könyv szerzQinek egyikét is) nem is ismeri a PDP-11 assembly nyelvet.
A C nyelv sok számítógép képességeihez illeszkedik, minden konkrét számítógép-architektúrától független, így könnyen írhatunk gépfüggetlen, tehát olyan programokat, amelyek különféle hardvereken változtatás nélkül futtathatók. A szerzQk környezetében ma már szokássá vált, hogy az UNIX alatt kifejlesztett szoftvert átviszik a helyi Honeywell, IBM és Interdata rendszerekre. Valójában az ezen a négy gépen mqködQ C fordítók és futtatási szolgáltatások egymással sokkal inkább kompatibilisek, mint az ANSI-szabványos FORTRAN megfelelQ változatai. Maga az UNIX operációs rendszer jelenleg mind a PDP-11-en, mind pedig az Interdata 8/32-n fut. Azokon a programokon kívül, amelyek szükségszerqen többé-kevésbé gépfüggQek, mint a C fordító, az assembler vagy a debugger, a C nyelven írt szoftver mindkét gépen azonos. Magán az operációs rendszeren belül az assembly nyelvq részeken és a perifériahandlereken kívüli 7000 sornyi kód mintegy 95%-a azonos.
Más nyelveket már ismerQ programozók számára az összehasonlítás kedvéért érdemes megemlíteni a C nyelv néhány történeti, technikai és filozófiai vonatkozását.
A C nyelv sok alapötlete a nála jóval régebbi, de még ma is élQ BCPL nyelvbQl származik, amelyet Martin Richards fejlesztett ki. A BCPL a C nyelvre közvetett módon, a B nyelven keresztül hatott, amelyet Ken Thompson 1970-ben a PDP-7-esen futó elsQ UNIX rendszer számára írt.
Bár a C nyelvnek van néhány közös vonása a BCPL-lel, mégsem nevezhetQ semmilyen értelemben a BCPL egyik változatának. A BCPL és a B típus nélküli nyelvek: az egyetlen adattípus a gépi szó és másféle objektumokhoz való hozzáférés speciális operátorokkal és függvényhívásokkal történik. A C nyelvben az alapvetQ adatobjektumok a karakterek, a különféle méretq egész (integer) típusok és a lebegQpontos számok. Ehhez járul még a származtatott adattípusok hierarchiája, amelyek mutatók (pointerek), tömbök, struktúrák, unionok és függvények formájában hozhatók létre.
A C nyelv tartalmazza a jól struktúrált programok készítéséhez szükséges alapvetQ vezérlési szerkezeteket: az összetartozó utasítássorozatot, a döntéshozatalt (if), a programhurok tetején (while for) vagy alján (do) vizsgálatot tartalmazó ciklust és a több eset valamelyikének kiválasztását (switch). (Ezek mindegyike rendelkezésre állt a BCPL-ben is, szintaxisuk azonban némileg különbözött a C-belitQl; a BCPL néhány évvel megelQzte a struktúrált programozás elterjedését.)
A C nyelv lehetQvé teszi a mutatók használatát és a címaritmetikát. A függvények argumentumainak átadása az argumentum értékének lemásolásával történik, és a hívott függvény nem képes megváltoztatni az aktuális argumentumot a hívóban. Ha név szerinti hívást akarunk megvalósítani, egy mutatót adhatunk át explicit módon és a függvény megváltoztathatja azt az objektumot, amire a mutató mutat. A tömbnév úgy adódik át, mint a tömb kezdQcíme, tehát tömbargumentumok átadása név szerinti hívással történik.
Bármely függvény rekurzív módon hívható és lokális változói rendszerint automatikusak, azaz a függvény minden egyes meghívásakor újra létrejönnek. A függvénydefiníciók nem skatulyázhatók egymásba, a változók azonban blokkstruktúrában is deklarálhatók. A C programokban szereplQ függvények külön is fordíthatók. Megkülönböztethetünk egy függvényre nézve belsQ, külsQ (csak egyetlen forrásállományban ismert) és teljesen globális változókat. A belsQ változók automatikusak és statikusak lehetnek. Az automatikus változók a hatékonyság növelése érdekében regiszterekbe helyezhetQk, de a register deklaráció csak ajánlás a fordítónak és nem vonatkozik adott gépi regiszterekre.
A PASCAL-lal vagy az ALGOL 68-cal összehasonlítva a C nem szoros típusmegkötésq nyelv, viszonylag engedékeny az adatkonverziókat illetQen, de az adattípusok konverziója nem a PL/1-re jellemzQ szabadossággal történik. A jelenlegi fordítók nem ellenQrzik futás közben a tömbindexeket, argumentumtípusokat stb.
Ha szigorú típusellenQrzés szükséges, a C fordító egy speciális változatát, a lint-et kell használni. A lint nem generál kódot, hanem a fordítás és töltés során lehetséges legtöbb szempontból igen szigorúan ellenQriz egy adott programot. Jelzi a nem illeszkedQ típusokat, a következetlen argumentumhasználatot, nem használt vagy nyilvánvalóan inicializálatlan változókat, az esetleges gépfüggetlenségi problémákat stb. Azok a programok, amelyekben a lint nem talál hibát, ritka kivételektQl eltekintve körülbelül ugyanolyan mértékben mentesek a típushibáktól, mint például az ALGOL 68 programok. A megfelelQ helyen a lint további szolgáltatásait is ismertetjük.
Végezetül a C-nek, mint minden más nyelvnek, megvannak a maga gyengeségei. Némelyik operátorának rossz a precedenciája; a szintaxis bizonyos részei jobbak is lehetnének; a nyelvnek több, kismértékben eltérQ változata él. Mindezzel együtt a C nyelv széles körben alkalmazható, rendkívül hatékony és kifejezQképes nyelvnek bizonyult.
A könyv felépítése a következQ: Az 1. fejezet a nyelv megtanulását segítQ bevezetés a C nyelv központi részébe. Ennek az a célja, hogy az olvasó minél hamarabb elkezdhessen programozni, mivel a szerzQk hite szerint egy új nyelv megtanulásának egyetlen módja, ha programokat írunk az illetQ nyelven. A fejezet feltételezi, hogy az olvasó rendelkezik a programozás alapjainak aktív ismeretével; az anyag nem magyarázza meg, hogy mi a számítógép, mi a fordítás, sem pedig az olyan kifejezések jelentését, mint n = n + 1 . Bár lehetQleg mindenütt hasznos programozási módszereket próbáltunk bemutatni, nem szántuk mqvünket az adatstruktúrák és algoritmusok kézikönyvének: kényszerq választás esetén elsQsorban a nyelvre koncentráltunk.
A 2. ... 6. fejezet részletesen, az 1. fejezetnél precízebben tárgyalja a C nyelv különbözQ elemeit, a hangsúly azonban itt sem a részleteken, hanem a teljes, a gyakorlatban alkalmazható példaprogramokon van. A 2. fejezet az alapvetQ adattípusokat, operátorokat és kifejezéseket ismerteti. A 3. fejezet a programvezérléssel: if-else, while, for stb. foglalkozik. A 4. fejezet témái : a függvények és a program felépítése, külsQ változók, az érvényességi tartomány szabályai stb. Az 5. fejezet a mutatókkal és a címaritmetikával, a 6. fejezet a struktúrákkal és unionokkal kapcsolatos tudnivalókat tartalmazza.
A 7. fejezet a szabványos be- és kiviteli (I/o) könyvtárat ismerteti, amely közös csatlakozófelületet képez az operációs rendszer felé. Ezt a be- és kiviteli könyvtárat minden olyan gép támogatja, amely a C-t támogatja, tehát azok a programok, amelyek ezt használják bevitel, kivitel és más rendszerfunkciók céljából, lényegében változtatás nélkül vihetQk át egyik rendszerrQl a másikra.
A 8. fejezet a C programok és az UNIX operációs rendszer közötti csatlakozásokat írja le, elsQsorban a be- és kivitelre, az állományrendszerre és a gépfüggetlenségre koncentrálva. Bár e fejezet egy része UNIX-specifikus, a nem UNIX-ot használó programozók is hasznos tudnivalókat találhatnak benne - megtudhatják pl., hogyan valósították meg a szabványos könyvtár adott verzióját, és hogyan nyerhetünk gépfüggetlen programkódot.
Az A. függelék a C nyelv referencia-kézikönyvét, a C szintaxisának és szemantikájának hivatalos leírását tartalmazza. Ha az elQzQ fejezetekben esetleg kétértelmqségekre vagy hiányosságokra bukkanunk, mindig ezt kell végsQ döntQbírónak tekinteni.
Mivel a C olyan, még fejlQdésben levQ nyelv, amely számos rendszeren fut, elQfordulhat, hogy a könyv egy-egy része nem felel meg valamely adott rendszer fejlQdése pillanatnyi állapotának. Igyekeztünk elkerülni az ilyen problémákat, és megpróbáltuk felhívni a figyelmet a lehetséges nehézségekre. Kétes esetekben azonban általában a PDP-11 UNIX rendszer esetében érvényes helyzet leírását választottuk, mivel a C programozók többségének ez a munkakörnyezete. Az A. függelékben ismertetjük a fontosabb C rendszerek megvalósításaiban mutatkozó különbségeket is.


A kézikönyv a DEC PDP 11 , a Honeywell 6000, az IBM System/370 és az Interdata 8/32 gépeken használható C nyelvet ismerteti. Eltérések esetén a PDP 11 -es változatot helyezi elQtérbe, de igyekszik rámutatni a megvalósításfüggQ részletekre. Néhány kivételtQl eltekintve ezek a gépfüggQ részletek közvetlenül a hardver alaptulajdonságaiból következnek; a különféle fordítók általában eléggé kompatibilisek.

2. Szintaktikai egységek

A szintaktikai egységek hat osztályba sorolhatók: azonosítók, kulcsszavak, állandók, karakterláncok, operátorok és egyéb szeparátorok. A szóközöket, tabulátorokat, újsorokat, megjegyzéseket (közös nevükön üres helyeket), mint az alábbiakban is látni fogjuk, a C fordító nem veszi figyelembe, eltekintve attól, hogy feladatuk a szintaktikai egységek elválasztása. Üres helyre van szükség az egyébként szomszédos azonosítók, kulcsszavak és állandók elválasztására.
Ha a beolvasott szöveg szintaktikai egységekre bontása adott karakterig megtörtént, a fordító azt a lehetQ leghosszabb karakterláncot tekinti a következQ egységnek, amelyrQl feltételezhetQ, hogy még egyetlen szintaktikai egységet képez.

2.1. Megjegyzések

A /* karakterek megjegyzést (comment) vezetnek be, amely a */ karakterekkel zárul. A megjegyzések nem skatulyázhatók egymásba.

2.2. Azonosítók (nevek)

Az azonosító betqk és számjegyek sorozata; az elsQ karakter betq kell, hogy legyen. A aláhúzásjel betqnek számít. A nagy- és kisbetqk különbözQk. Csupán az elsQ nyolc karakter értékes, bár több is használható. A különféle , assemblerek és betöltQprogramok által használt külsQ azonosítók ennél kötöttebbek:

DEC PDP 11 7 karakter, kétféle betqtípus (kis- és nagybetq).
Honeywell 6000 6 karakter, egyféle betqtípus.
IBM 360/370 7 karakter, egyféle betqtípus.
Interdata 8/32 8 karakter, kétféle betqtípus.

2.3. Kulcsszavak

Az alábbi azonosítók a nyelv kulcsszavai, így egyéb célra nem használhatók:

int extern else char
register for float typedef
do double static while
struct goto switch union
return case long sizeof
default short break entry
auto unsigned continue if

Az entry kulcsszót egyetlen jelenleg mqködQ fordítóban sem valósították meg, késQbbi fejlesztésekhez tartottuk fenn. Bizonyos megvalósításokban a fortran és az asm szavak is kulcsszóként szerepelnek.

2.4. 9llandók

Többfajta állandó van; ezeket a következQkben soroljuk fel. A méreteket érintQ hardverjellemzQket a 2.6. pontban foglaljuk össze.

2.4.1. Egész állandók

A számjegyek sorozatát tartalmazó egész típusú (integer) állandót a fordító oktálisnak tekinti, ha 0-val (a nulla számjeggyel) kezdQdik, egyébként decimálisnak veszi. A 8 és 9 számjegyek oktális értéke 10, ill. 11 . Az olyan számjegysorozatot, amelyet 0X vagy 0x (a 0 a nulla számjegy) elQz meg, a fordítóprogram hexadecimális egésznek tekinti. Hexadecimális számjegyek az a-tól, ill. A-tól f-ig, ill. F-ig elhelyezkedQ karakterek, amelyeknek értéke 10, . . ., 15. Azt a decimális állandót, amelynek értéke meghaladja a gépenábrázolható legnagyobb elQjeles egészt, a fordítóprogram long-nak veszi; hasonlóképpen long lesz az az oktális vagy hexadecimális állandó, amelynek értéke meghaladja a legnagyobb, elQjel nélküli gépi egészt.

2.4.2. Explicit long állandók

Az a decimális, oktális vagy hexadecimális egész, amelyet közvetlenül l ("el" betq) vagy L követ, long (hosszú) állandó. Amint arról az alábbiakban szó lesz, bizonyos gépeken az int és long értékek azonosak.

2.4.3. Karakterállandók

A karakterállandó aposztrófok (szimpla idézQjelek) közé zárt karakter, pl. 'x'. A karakterállandó értéke a karakternek a gép karakterkészletében szereplQ numerikus értéke.
Bizonyos nem grafikus karaktereket, pl. az aposztrófot (') vagy a fordított törtvonalat (\) az alábbi escape-szekvenciákkal ábrázolhatunk:

újsor NL (LF) \n
vízszintes tab HT \t
vissza-szóköz BS \b
kocsi-vissza CR \r
lapdobás FF \f
fordított törtvonal \ \\
aposztróf ' \'
bitminta ddd \ddd

A \ddd escape-szekvencia egy fordított törtvonalat és 1 , 2 vagy 3 rákövetkezQ oktális számjegyet tartalmaz, amelyek a kívánt karakter értékét határozzák meg. E konstrukció speciális esete a \0 (amit nem követ számjegy), amely a NULL karaktert jelöli. Ha a fordított törtvonalat követQ karakter nem az elQbbiek egyike, a fordító a fordított törtvonalat nem veszi figyelembe.

2.4.4. LebegQpontos állandók

A lebegQpontos állandó egész részbQl, tizedespontból, törtrészbQl, e-bQl vagyE-bQl és (esetleg elQjeles) kitevQbQl áll. Mind az egész, mind a tört rész számjegyek sorozata. Akár az egész, akár a tört rész hiányozhat (de mind a kettQ nem!); ill. a tizedespont vagy az e és a kitevQ közül az egyik szintén elmaradhat. Minden lebegQpontos állandó duplapontosságú.

2.5. Karakterláncok

A karakterlánc idézQjelek közé zárt karaktersorozat: ". . .". A karakterlánc típusa szerint karaktertömb, tárolási osztálya static (l. a következQkben a 4. szakaszt), és a megadott karakterek inicializálják. Az egyes karakterláncok, még az azonos módon leírtak is, külön egységet képeznek. A fordító minden karakterlánc végére elhelyezi a \0 nullabyte-ot abból a célból, hogy a karakterláncot vizsgáló programok megtalálják a karakterlánc végét. A karakterláncon belül elhelyezett " idézQjelet \ kell, hogy megelQzze; a karakterállandóknál ismertetett összes escape-szekvencia használható. Végül megjegyezzük, hogy az \-t és az azt közvetlenül követQ újsort a fordító nem veszi figyelembe.

2.6. HardverjellemzQk

Az alábbi táblázatban néhány olyan hardvertulajdonságot foglaltunk össze, amely géprQl gépre változik. Noha ezek a programok gépfüggetlenségét érintik, mégis jóval kisebb problémát okoznak, mint azt valaki eleve gondolná. (A számokbitekben értendQk.)

DEC PDP-11 Honeywell 6000 IBM 370 Interdata 8/32
ASCII ASCII EBCDIC ASCII
char 8 9 8 8
int 16 36 32 32
short 16 36 16 16
long 32 36 32 32
float 32 36 32 32
double 64 72 64 64
értéktar-
tomány +-10+-38 +-10+-38 +-10+-76 +-10+-76

E négy gép esetében a lebegQpontos számoknak 8 bites kitevQjük van.

3. A szintaxis jelölése

A kézikönyvben használt szintaktikai jelölésmódban a kulcsszavakat és karaktereket - ahol az egyértelmqség megkívánja - kövér szedéssel jelöljük. A választható (alternatív) kategóriák külön sorban szerepelnek. Az elhagyható (opcionális) szimbólumokat az "opc" index jelöli, így

{ kifejezésopc }

kapcsos zárójelek közé zárt elhagyható kifejezést jelöl. A szintaxist késQbb a 18. pontban foglaljuk össze.

4. Az azonosítók értelmezése

A C nyelv az azonosítók értelmezését az azonosítók két tulajdonságára alapozza: a tárolási osztályára és a típusára. A tárolási osztály az azonosítóhoz rendelt tárhely elhelyezkedését és élettartamát, a típus az azonosítóhoz rendelt tárterületen talált értékek jelentését határozza meg.
Négy deklarálható tárolási osztály van: automatikus, statikus, külsQ és regiszterosztály. Az automatikus változók egy blokk minden hívására nézve lokálisak (l. a 9.2. pontot) értéküket a blokkból való kilépéskor elvesztik; a statikus változók egy blokkra nézve lokálisak, de még akkor is megtartják értéküket, ha a vezérlés idQközben kilépett a blokkból; a külsQ változók megmaradnak és megtartják értéküket az egész program végrehajtása során és függvények közötti kommunikációra használhatók, még külön-külön lefordított függvények esetében is. A regiszterváltozók (ha lehetséges) a gép gyors regisztereiben tárolódnak; az automatikus változókhoz hasonlóan az egyes blokkokra nézve lokálisak és a blokkból való kilépéskor eltqnnek.
A C nyelv több alapvetQ objektumtípus használatát engedi meg:
A karakterként (char) deklarált objektumok elegendQen nagyok ahhoz, hogy az adott implementáció karakterkészletének tetszQleges elemét tárolni tudják, és ha valóban egy, az illetQ karakterkészletbQl vett karaktert akarunk karakter típusú változóban tárolni, annak értéke meg fog egyezni a karakter egész értékq kódjával. Más mennyiségek is tárolhatók karakter típusú változókban, de ennek megvalósítása gépfüggQ.
Maximum háromféle egész típusú méret áll rendelkezésre, amelyeket short int (rövid egész), int (egész) és long int (hosszú egész) alakban deklarálunk. A hosszabb egészek bizonyosan nem igényelnek kevesebb tárhelyet, mint a rövidebbek, de az adott nyelvi megvalósítás a short int-eket a long int-ekkel vagy akár mind a kettQt közönséges egészekkel (int) egyenlQ méretqvé teheti. A közönséges egészeknek a befogadó gép architektúrájából következQ természetes méretük van; a többi méret speciális igények kielégítésére szolgál.
Az unsigned-ként deklarált, elQjel nélküli egészekre a modulo 2n aritmetika szabályai vonatkoznak, ahol n a bitek száma az adott megvalósításban. (A PDP-11 az elQjel nélküli long mennyiségeket nem támogatja.)
Az egyszeres pontosságú lebegQpontos (float) és a duplapontosságú lebegQpontos (double) ábrázolás egyes gépeken azonos lehet.
Mivel az említett típusú objektumok célszerqen értelmezhetQk számokként, ezekre mint aritmetikai típusokra fogunk hivatkozni. Az összes char és int típust (mérettQl függetlenül) együttesen integrális tipusnak, a float-ot és a double-t együttesen lebegQpontos tipusnak fogjuk nevezni.
Az alapvetQ aritmetikai típusokon kivül elvileg végtelen számú leszármaztatott típus képezhetQ az alaptípusokból, az alábbi módokon:

tömbök, amelyek a legtöbb típusú objektumból képezhetQk;
függvények, amelyek adott típusú objektumot adnak vissza;
mutatók, amelyek adott típusú objektumra mutatnak;
struktúrák, amelyek különféle típusú objektumok sorozatát tartalmazzák;
unionok, amelyek különféle típusú objektumok bármelyikét tartalmazhatják.

Az objektumok létrehozásának ezek a módszerei általában rekurzív módon alkalmazhatók.

5. Objektumok és balértékek

Az objektum a tár valamely mqveletekkel kezelhetQ része; a balérték (lvalue) objektumra hivatkozó kifejezés. A balérték kifejezésre kézenfekvQ példa az azonosító. Bizonyos operátorok balértékeket eredményeznek: ha E mutató típusú kifejezés, akkor *E olyan balérték kifejezés, amely arra az objektumra hivatkozik, amire az E mutat. A balérték elnevezés az E1 =E2 értékadó kifejezésbQl származik, amelyben az E1 bal oldali operandusnak balérték kifejezésnek kell lennie. Az egyes operátorok alább következQ ismertetése során közöljük hogy az adott operátor balérték operandusokat vár-e és hogy balértéket ad-e eredményül.

6. Konverziók

Operandusuktól függQen számos operátor válthatja ki valamelyik operandusa értékének egyik típusból valamilyen másik típusba történQ átalakítását. Ebben a szakaszban az ilyen konverziók várható eredményét ismertetjük. A közönséges operátorok többsége által megkövetelt konverziókat a 6.6. pontban foglaltuk össze; ezt szükség szerint az egyes operátorok tárgyalásánál további információkkal egészítettük ki.

6.1. Karakterek és egészek

Karaktert és rövid _egészt mindenütt használhatunk, ahol közönséges egész használható. Az érték minden esetben int-té alakul. Rövidebb egész hosszabb egésszé történQ konvertálása mindig elQjel-kiterjesztéssel jár: az egészek elQjeles mennyiségek. Az adott géptQl függ, hogy karakterek esetében is történik-e elQjel-kiterjesztés, de annyi bizonyos, hogy a szabványos karakterkészlet valamennyi eleme nemnegatív. Azok közül a számítógépek közül, amelyeket ez a kézikönyv figyelembe vesz, csak a PDP- 11 végez elQjel-kiterjesztést. A PDP-11 -en a karakter típusú változók értéktartománya -128 és 127 között van; az összes ASCII karakter pozitív. Az oktális escape-szekvencia segítségével megadott karakterállandókra elQjel-kiterjesztés történik, és negatívként is megjelenhetnek, pl. '\077' értéke -1.
Ha egy hosszabb egészt rövidebb egésszé vagy char-rá alakítunk, a levágás bal oldalon történik : a felesleges bitek egyszerqen elmaradnak.

6.2. Float és double

A C-ben mindenféle lebegQpontos mqvelet duplapontosságú; amikor egy kifejezésben float fordul elQ, az a tört rész nullákkal való kitöltése révén double-lá hosszabbodik. Ha double-t kell float-tá alakítani, pl. értékadás során, a double elQször kerekítQdik és csak ezután rövidül float hosszúságúvá.

6.3. LebegQpontos és integrális mennyiségek

A lebegQpontos értékek integrális típusúvá alakítása általában eléggé gépfüggQ mqvelet; különösképpen a negatív számok csonkításának iránya változik géprQl gépre. Ha a rendelkezésre álló helyen az eredmény nem fér el, határozatlan lesz.
Integrális értékek lebegQpontossá alakítása problémamentes. A pontosság némileg csökken, ha a célhelyen nincs elegendQ bit.

6.4. Mutatók és egészek

Az int vagy long int mennyiségek a mutatókhoz hozzáadhatók vagy azokból levonhatók; ebben az esetben az elQbbiek az összeadó operátornál leírtak szerint alakulnak át.
Két, ugyanolyan típust megcímzQ mutató egymásból kivonható: ez esetben az eredmény egésszé alakul át, amint azt a kivonó operátornál tárgyaljuk.

6.5. ElQjel nélküli egészek

Ha elQjel nélküli (unsigned) és közönséges egészeket kombinálunk, a közönséges egész elQjel nélkülivé alakul át, és az eredmény is elQjel nélküli. Az érték az a legkisebb elQjel nélküli egész, amely kongruens az elQjeles egésszel (modulo 2szóméret). 2-es komplemensq ábrázolásban a konverzió csupán elvi, a bitminta valójában nem változik.
Ha az elQjel nélküli egész long-gá alakul, az eredmény értéke számszerqleg ugyanaz, mint az elQjel nélküli egészé. Igy a konverzió csupán a bal oldali kitöltQ nullák elhelyezésébQl áll.

6.6. Aritmetikai konverziók

Számos operátor hasonló konverziót vált ki, és az eredményt ugyanabban a típusban szolgáltatja. Ezt az eljárást szokásos aritmetikai konverziónak nevezni.
ElQször is minden char vagy short típusú operandus int-té és minden float operandus double-lá alakul.
Ezután, ha valamelyik operandus double, akkor a másik is double-lá alakul, és az eredmény szintén double lesz.
Egyébként, ha valamelyik operandus long, a másik operandus és az eredmény típusa is long lesz.
Egyébként, ha valamelyik operandus unsigned, a másik is unsigned-dá alakul, és ez lesz az eredmény típusa is.
Minden más esetben mindkét operandusnak int-nek kell lennie és ez lesz az eredmény típusa is.

7. Kifejezések

A kifejezésekben elQforduló operátorok precedenciája ugyanaz, mint ebben a fejezetben az alfejezetek (pontok) sorrendje; a legmagasabb precedencia az elsQ. îgy pl. azokat a kifejezéseket, amelyekre mint a + operandusaira hivatkozunk (7.4. pont) a 7.1 ... 7.3. pontokban definiáljuk. Az egyes pontokon belül minden operátor azonos precedenciájú. Minden pontban megadjuk, hogy az ott tárgyalt operátorokra bal-, ill. jobbirányú asszociativitás vonatkozik-e. A kifejezésekben alkalmazott operátorok precedenciáját és asszociativitását a 18. pontban közölt nyelvtan foglalja össze.
Egyéb esetekben a kifejezések kiértékelésének sorrendje határozatlan. A fordítóprogram a részkifejezéseket saját megítélése szerint abban a sorrendben számítja ki, amit leghatékonyabbnak vél, még abban az esetben is, ha a részkifejezéseknek mellékhatásaik vannak. A mellékhatások elQfordulásának sorrendje meghatározott. Kommutatív és asszociatív operátorokat (*, +, &, |, n~) tartalmazó kifejezések tetszés szerint rendezhetQk még zárójelek jelenlétében is; ha adott sorrendben végzendQ kiértékelést kívánunk elQírni, explicit ideiglenes változót kell használnunk.
A kifejezések kiértékelése során a túlcsordulás és az osztás ellenQrzésének kezelése gépfüggQ. A C nyelv minden létezQ megvalósítása figyelmen kívül hagyja az egészek túlcsordulását; a 0-val való osztás kezelése, ill. a lebegQpontos kivételek géprQl gépre változnak, és általában valamilyen könyvtári függvénnyel módosíthatók.

7.1. ElsQdleges kifejezések

A . és -> szimbólumokat, indexelést és függvényhívásokat tartalmazó elsQdleges kifejezések csoportosítása balról jobbra történik.

elsQdleges_kifejezés:
azonosító
állandó
karakterlánc
(kifejezés)
elsQdleges_kifejezés [kifejezés]
elsQdleges_kifejezés [kifejezéslistaopc]
elsQdleges_balérték.azonosító
elsQdleges_kifejezés->azonosító
Kifejezéslista:
kifejezés
kifejezéslista, kifejezés

Az azonosító elsQdleges kifejezés, feltéve, hogy az alábbi ismertetett módon helyesen deklarálták. Típusát a deklarációja határozza meg. Ha azonban az azonosító típusa valamilyen tömb, akkor az azonosító kifejezés értéke a tömb elsQ objektumát megcímzQ mutató, és a kifejezés típusa a tömb alaptípusára hivatkozó mutató. A tömbazonosító továbbá nem balérték kifejezés. Hasonlóképpen a függvényként deklarált azonosító is a függvény mutatójává alakul át, kivéve, ha valamely függvényhívás függvénynév-pozícióján fordul elQ.
Az állandó elsQdleges kifejezés. Típusa az alakjától függQen lehet int, long vagy double. A karakterállandók típusa int, a lebegQpontos állandóké double.
A karakterlánc elsQdleges kifejezés. Típusa eredetileg char-ok tömbje, de az azonosítókra vonatkozó fenti szabály értelmében az a char-mutatóvá módosul, és az eredmény a karakterlánc elsQ karakterét megcímzQ mutató. (Kivételt képeznek egyes kezdetiérték-beállítók (l. a 8.6. pontot.))
A zárójelezett kifejezés olyan elsQdleges kifejezés, amelynek típusa és értéke azonos a zárójel nélküli kifejezésével. A zárójelek jelenléte nem befolyásolja azt a tényt, hogy a kifejezés balérték-e vagy sem.
Az elsQdleges kifejezés és az azt követQ szögletes zárójelek közötti kifejezés szintén elsQdleges kifejezést képez [kifejezés]. Az elsQdleges kifejezés általában valamilyen mutató típusú, az index kifejezés int, és az eredmény típusa az a típus, amelyre a mutató mutat. Az E1[E2] kifejezés definíció szerint azonos a *((E1)+(E2))-vel. Ez a pont, valamint az azonosítókkal, a +-szal, ill. *-gal foglalkozó 7.1., 7.2., ill. 7.4. pont az összes tudnivalót tartalmazza, ami ennek a jelölésmódnak a megértéséhez szükséges. Az indexelésrQl a 14.3. pontban szólunk.
A függvényhívás olyan elsQdleges kifejezés, amelyet zárójelek között a függvény aktuális argumentumait alkotó kifejezések esetleg üres, vesszQkkel elválasztott listája követ. Az elsQdleges kifejezésnek "függvény, amely visszaadja . . .-t" típusúnak kell lennie, és a függvényhívás eredménye " . . . " típusú. Mint a következQkben látni fogjuk, minden korábban elQ nem fordult azonosító, amelyet közvetlenül nyitó zárójel követ, a szövegkörnyezet alapján egészt visszaadó függvényként deklarálódik, így a legközönségesebb esetben az egész értékq függvényeket nem kell deklarálni.
A float típusú argumentumok hívás elQtt double-lá alakulnak át; minden char és short int-té konvertálódik, és a tömbnevek, mint mindig, mutatókká alakulnak. Automatikusan semmilyen más konverzió nem történik; lényeges tudnunk, hogy a fordító az aktuális argumentumok típusát nem hasonlítja össze a formális argumentumokéval. Ha konverzióra van szükség, használjunk típusmódosító szerkezetet (l. a 7.2. és 8.7. pontot).
A függvényhívás elQkészítéseképpen másolat készül minden aktuális paraméterrQl, így a C nyelvben minden argumentumátadás szigorúan érték szerint történik. A függvény megváltoztathatja formális paramétereinek értékét, de ezek a változtatások nem befolyásolhatják az aktuális paraméterek értékét. LehetQség van viszont mutató átadására, tudva azt, hogy a függvény megváltoztathatja annak az objektumnak az értékét, amelyre a mutató mutat. A tömbnév mutatókifejezés. Az argumentumok kiértékelésének sorrendjét a nyelv nem definiálja; ne feledjük, hogy a különbözQ fordítók eltérQek!
Bármilyen függvény rekurzív módon hívható.
Egy elsQdleges kifejezés, az azt követQ pont és az azután következQ azonosító együttesen kifejezést alkot. Az elsQ kifejezésnek olyan balértéknek kell lennie, amely struktúrát vagy uniont nevez meg, az azonosító pedig meg kell, hogy nevezze a struktúra vagy union egy tagját. Az eredmény a struktúra vagy union megnevezett tagjára vonatkozó balérték.
Egy elsQdleges kifejezés, az azt követQ nyíl (amelyet egy - és egy > alkot) és az azután következQ azonosító együttesen kifejezést alkot. Az elsQ kifejezésnek struktúrát vagy uniont megcímzQ mutatónak kell lennie, és az azonosítónak a struktúra vagy union egy tagját kell megneveznie. Az eredmény olyan balérték, amely a mutatókifejezés által megcímzett struktúra vagy union megnevezett tagjára vonatkozik.
îgy az E1->MOS kifejezés azonos a (*E1).MOS kifejezéssel. A struktúrákkal és unionokkal a 8.5. pont foglalkozik. A használatukra vonatkozóan itt megadott szabályokat a fordító rugalmasan alkalmazza, hogy ki lehessen lépni a típusmechanizmusból (l. a 14.1. pontot).

7.2. Egyoperandusú operátorok

Az egyoperandusú operátorokkal alkotott kifejezések csoportosítása jobbról balra történik.

egyoper_kifejezés:
*kifejezés
&balérték
-kifejezés
!kifejezés
~kifejezés
++balérték
--balérték
balérték++
balérték--
(típusnév) kifejezés
sizeof kifejezés
sizeof (típusnév)

Az egyoperandusú * operátor indirekciót fejez ki: a kifejezés mutató kellhogy legyen, és az eredmény olyan balérték, amely a kifejezés által megcímzett objektumra vonatkozik. Ha a kifejezés mutató típusú, akkor az eredmény típusa a mutatóval megcímzett objektum típusa.
Az egyoperandusú & operátor hatására a balérték által hivatkozott objektumot megcímzQ mutató keletkezik. Ha a balérték típusa ". . .", akkor az eredmény típusa "mutató . . .-ra"
Az egyoperandusú - operátor az operandus negatív értékét eredményezi. A szokásos aritmetikai konverziók mennek végbe. ElQjel nélküli (unsigned) mennyiség esetében a negatív értéket úgy kell kiszámítani, hogy 2n-bQl levonjuk az operandus értékét, (n az int-beli bitek száma). Egyoperandusú + operátor nincs.
A ! logikai negálóoperátor hatására az eredmény 1 lesz, ha az operandus nulla, 0 lesz, ha az operandus nemnulla. Az eredmény típusa int. Bármilyen aritmetikai típusra és mutatókra alkalmazható.
A ~ operátor hatására az operandus 1-es komplemense jön létre. Megtörténnek a szokásos aritmetikai konverziók. Az operandus integrális típusú kell, hogy legyen.
A ++ operátor balérték operandusa elQtt alkalmazva inkrementálja az operandus által hivatkozott objektumot. Az érték az operandus új értéke, amely azonban nem balérték. A ++x kifejezés x+=1-gyel egyenértékq. A konverziókra vonatkozóan l. az összeadásra (7.4. pont) és értékadó operátorokra (7.14. pont) vonatkozó ismertetést.
A -- operátor, ha balérték operandusa elQtt áll, az elQbbiekhez hasonlóan dekrementálja az operandust.
Ha a ++ operátort valamely balérték után alkalmazzuk, az eredmény a balérték által hivatkozott objektum értéke lesz. Az eredmény feljegyzése után az objektum ugyanúgy inkrementálódik, mint az elölrQl alkalmazott ++ operátor esetében. Az eredmény típusa ugyanaz, mint a balérték kifejezésé.
Ha a -- operátort valamely balérték után alkalmazzuk, az eredmény a balérték által hivatkozott objektum értéke lesz. Az eredmény feljegyzése után az objektum ugyanúgy dekrementálódik, mint az elQtag -- operátor esetében. Az eredmény típusa ugyanaz, mint a balérték kifejezésé.
Ha egy kifejezést valamelyik adattípus zárójelek közé írt neve elQz meg, a kifejezés értéke a megadott típusúvá alakul át. Ezt a konstrukciót típusmódosító szerkezetnek (cast) nevezzük. A típusneveket a 8.7. pontban írjuk le.
A sizeof operátor az operandusának a byte-okban kifejezett méretét állítja elQ. (A byte-ot a nyelv csupán sizeof értékének segítségével definiálja. Azonban minden létezQ megvalósításban a byte az a terület, amely alkalmas egy char tárolására.) Tömbre alkalmazva az eredmény az összes tömbbeli byte-ok száma lesz. A méretet a kifejezésben elQforduló objektumok deklarációi határozzák meg. Ez a kifejezés szemantikailag egész típusú állandó, bárhol használható, ahol állandóra van szükség. Leginkább olyan rutinokkal történQ kommunikáció céljaira használatos, mint pl. a tárterület-foglaló függvények és a be- és kivitel rendszerek.
A sizeof operátor zárójelben álló típusnévre is alkalmazható. Ekkor egy, a megjelölt típusú objektum méretét szolgáltatja byte-okban.
A sizeof(típus) szerkezet összefüggQ egység, így a sizeof(típus)-2 kifejezés ugyanaz, mint (sizeof(típus))-2.

7.3. Multiplikatív operátorok

A * , / és % multiplikatív operátorok balról jobbra csoportosítanak. Megtörténnek a szokásos aritmetikai konverziók.

multiplikatív_kifejezés:
kifejezés * kifejezés
kifejezés / kifejezés
kifejezés % kifejezés

A kétoperandusú * operátor a szorzást jelöli. A * operátor asszociatív, és az ugyanazon a szinten több szorzást tartalmazó kifejezéseket a fordító átrendezheti.
A kétoperandusú / operátor az osztást jelöli. Pozitív egészek osztásakor a csonkítás nulla felé történik, de ha bármelyik operandus negatív, akkor a csonkítás formája gépfüggQ. Az ebben a kézikönyvben figyelembe vett gépek esetében az osztandó és a maradék elQjele megegyezik. Mindig igaz, hogy

(a / b) * b + a % b

megegyezik a-val (ha b nemnulla).
A kétoperandusú % operátor az elsQ kifejezésnek a másodikkal történQ osztásából származó maradékot állítja elQ. A mqvelet szokásos aritmetikai konverziókkal jár. Az operandusok nem lehetnek float típusúak.

7.4. Additív operátorok

A + és - additív operátorok balról jobbra csoportosítanak. A szokásos aritmetikai konverziókat eredményezik. Mindkét operátor esetében vannak további típuslehetQségek.

additív_kifejezés:
kifejezés + kifejezés
kifejezés - kifejezés

A + operátor alkalmazásának eredménye az operandusok összege. Egy tömbbeli objektumot megcímzQ mutató és bármelyik integrális típus értéke összeadható. Az utóbbi minden esetben relatív címmé alakul oly módon, hogy megszorzódik annak az objektumnak a hosszúságával, amelyre a mutató mutat. Az eredmény az eredetivel megegyezQ típusú mutató, amely ugyanannak a tömbnek egy másik elemére mutat, megfelelQ eltolással az eredeti objektumhoz képest. Ha tehát P tömbelemet megcímzQ mutató, akkor a P+1 kifejezés a tömb következQ elemét megcímzQ mutató lesz.
Mutatókra semmilyen más típusú kombináció sem megengedett!
A + operátor asszociatív, és az ugyanazon a szinten több összeadást tartalmazó kifejezéseket a fordító átrendezheti.
A - operátor alkalmazásának hatására a két operandus különbsége keletkezik, a szokásos aritmetikai konverziók alkalmazásával. Ezenkívül mutatókból le szabad vonni bármely integrális típusú értéket, ekkor megtörténnek ugyanazok a konverziók, mint az összeadásnál.
Ha két ugyanolyan típusú objektumot megcímzQ mutatót vonunk ki egymásból, az eredmény (az objektum hosszával történQ osztás révén) int-té alakul, és a megcímzett objektumok között elhelyezkedQ objektumok darabszámát adja meg. 9ltalános esetben ez a konverzió váratlan eredményre vezet, kivéve, ha a mutatók ugyanannak a tömbnek az elemeire mutatnak. Ennek az az oka, hogy még az ugyanolyan típusú objektumok távolsága sem feltétlenül az objektumhosszúság többszöröse.

7.5. LéptetQ operátorok

A << és >> léptetQ (shift) operátorok balról jobbra csoportosítanak. MindkettQ elvégzi az operandusokon a szokásos aritmetikai konverziókat; az operandusok mindegyike integrális kell, hogy legyen. A mqvelet során a jobb oldali operandus int-té alakul át; az eredmény típusa megegyezik a bal oldali operanduséval. Az eredmény határozatlan, ha a jobb oldali operandus negatív vagy nagyobb, mint az objektum bitekben mért hosszúsága, vagy pedig azzal megegyezik.

léptetQ_kifejezés:
kifejezés << kifejezés
kifejezés >> kifejezés

Az E1<>E2 értéke úgy áll elQ, hogy E1 értéke E2 bittel balra léptetQdik. A jobbra garantáltan logikai jellegq (0-val történQ feltöltés), ha az E1 unsigned; más esetben aritmetikai lehet (és a PDP 11 -en az is lesz) ilyenkor a feltöltQdés az elQjelbittel történik.

7.6. Relációs operátorok

A relációs operátorok balról jobbra csoportosítanak, de ez a tény nem különösebben hasznos: a < b < cjelentése nem az, amit gondolnánk.

relációs_kifejezés:
kifejezés < kifejezés
kifejezés > kifejezés
kifejezés <= kifejezés
kifejezés >= kifejezés

A < (kisebb, mint), > (nagyobb, mint), <= (kisebb vagy egyenlQ) és >= (nagyobb vagy egyenlQ) operátorok mindegyike 0-át eredményez, ha a megadott reláció értéke hamis, és 1 -et, ha igaz. Az eredmény típusa int. A mqveletek a szokásos aritmetikai konverziókkal járnak. Két mutató összehasonlítható: az eredmény a megcímzett objektumok címének a címtartományban való egymáshoz képesti elhelyezkedésétQl függ. A mutató összehasonlítás csak akkor gépfüggetlen, ha a mutatók ugyanabban a tömbben elhelyezkedQ objektumokra mutatnak.

7.7. EgyenlQségi operátorok

egyenlQség_kifejezés:
kifejezés == kifejezés
kifejezés != kifejezés

A == (egyenlQ) és != (nem egyenlQ) operátorok pontosan ugyanolyanok, mint a relációs operátorok - csak a precedenciájuk alacsonyabb. (Igy

a < b == c < d

értéke 1 , ha a < b és c < d igazságértéke megegyezik.)
Mutató és egész összehasonlítható, de az eredmény gépfüggQ, kivéve ha az egész a 0 állandó. Az a mutató, amelyhez a 0-t rendeltünk hozzá, garantáltan nem mutat semmilyen objektumra, és 0-val egyenlQként fog megjelenni; a hagyományos használatban az ilyen mutatót nullának tekintjük.

7.8. Bitenkénti ÉS operátor

és_kifejezés:
kifejezés & kifejezés

Az & operátor asszociatív, és az &-et tartalmazó kifejezések átrendezhetQk. A szokásos aritmetikai konverziók mennek végbe; az eredmény az operandusok bitenkénti ÉS függvénye. Az operátor csak integrális operandusokra alkalmazható!

7.9. Bitenkénti kizáró VAGY operátor

kizáró_vagy_kifejezés:
kifejezés ^ kifejezés

A ^ operátor asszociatív, és a ^-t tartalmazó kifejezések átrendezhetQk. A mqvelet a szokásos aritmetikai konverziókkal jár; az eredmény az operandusok bitenkénti kizáró VAGY függvénye. Az operátor csak integrális operandusokra alkalmazható!

7.10. Bitenkénti inkluzív VAGY operátor

inkluzív_vagy_kifejezés:
kifejezés | kifejezés

A | operátor asszociatív, és a |-ot tartalmazó kifejezések átrendezhetQk. A mqvelet a szokásos aritmetikai konverziókkal jár; az eredmény az operandusok bitenkénti inkluzív VAGY függvénye. Az operátor csak integrális operandusokra alkalmazható!

7.11. Logikai ÉS operátor

logikai_és_kifejezés:
kifejezés && kifejezés

Az && operátor balról jobbra csoportosít. 1-et ad vissza; ha egyik operandusa sem nulla, egyébként 0-t. Az &-tQl eltérQen az && biztosítja a balról jobbra történQ kiértékelést; ezen felül a második operandus nem értékelQdik ki, ha az elsQ 0.
Az operandusoknak nem kell azonos típusúaknak lenniük, de mindegyikük típusa vagy valamelyik alaptípus, vagy pedig mutató kell, hogy legyen. Az eredmény mindig int.

7.12. logikai VAGY operátor

logikai_vagy_kifejezés:
kifejezés || kifejezés

A || operátor balról jobbra csoportosít. 1_t ad vissza, ha valamelyik operandusa nemnulla, 0-t egyébként. A |-tól eltérQen a || biztosítja a balról jobbra történQ kiértékelést; ezen felül a második operandus nem értékelQdik ki, ha az elsQ nemnulla.
Az operandusoknak nem kell azonos típusúaknak lenniük, de mindegyikük típusa vagy valamelyik alaptípus, vagy pedig mutató kell, hogy legyen. Az eredmény mindig int.

7.13. A feltételes operátor

feltételes_kifejezés:
kifejezés ? kifejezés : kifejezés

A feltételes kifejezések balról jobbra csoportosítanak. Az elsQ kifejezés kiértékelQdik, és ha az értéke nemnulla, az eredmény a második kifejezés értéke lesz, egyébként pedig a harmadik kifejezésé. LehetQség szerint megtörténnek a szokásos aritmetikai konverziók, amelyek révén a második és a harmadik kifejezés azonos típusúvá válik; egyébként, ha mindkettQ ugyanolyan típusú mutató, az eredmény típusa ez a közös típus lesz; vagy pedig az egyiknek mutatónak, a másiknak a 0 állandónak kell lennie, és az eredmény típusa a mutató típusa lesz. A második és a harmadik kifejezés közül csak az egyik értékelQdik ki.

7.14. Értékadó operátorok

Több értékadó operátor van, amelyek mindegyike jobbról balra csoportosít. Bal oldali operandusként mindegyikük egy-egy balértéket igényel, az értékadó kifejezés típusa a bal oldali operandus típusával fog megegyezni. Az értékadó_ kifejezés értéke az az érték lesz, amely az értékadás után a bal oldali operandusban található. Az összetett értékadó operátor két része különálló szintaktikai egységet képez.

értékadó_kifejezés:
balérték = kifejezés
balérték += kifejezés
balérték -= kifejezés
balérték *= kifejezés
balérték /= kifejezés
balérték %= kifejezés
balérték >>= kifejezés
balérték <<= kifejezés
balérték &= kifejezés
balérték ^= kifejezés
balérték |= kifejezés

A legegyszerqbb értékadásnál, ahol az = operátort alkalmazzuk, a kifejezés értéke behelyettesítQdik a balérték által hivatkozott objektum értékébe. Ha mindkét operandus aritmetikai típusú, a jobb oldali operandus még az értékadás elQtt bal oldali típusúvá alakul át. Az

E1 op= E2

alakú kifejezés hatását kikövetkeztethetjük, ha azt

E1 = E2 op (E2)

alakúnak tekintjük; az E1 azonban csak egyszer értékelQdik ki. A += és -= esetben a bal oldali operandus mutató is lehet, ekkor az (integrális)jobb oldali operandus a 7.4. pontban mondottak szerint alakul át; minden jobb oldali operandus és az összes nem-mutató jellegq bal oldali operandus aritmetikai típusú kell, hogy legyen.
A jelenlegi fordítók megengedik mutató értékül adását egésznek, egészt mutatónak, valamint mutatót más típusú mutatónak. Az értékadás tisztánmásolási mqvelet, konverzió nélkül. Ez a fajta használat gépfüggQ, és olyan mutatókat eredményezhet, amelyek használatuk során címzési problémákhoz vezetnek. Annyi azonban bizonyos, hogy a 0 állandónak mutatóhoz való hozzárendelése olyan nulla-mutatót eredményez, amely bármilyen objektumot jelölQ mutatótól megkülönböztethetQ.

7.15. A vesszQ operátor

vesszQ_kifejezés:
kifejezés , kifejezés

A vesszQvel elválasztott kifejezéspár balról jobbra értékelQdik ki, és a bal oldali kifejezés értéke megegyezik a jobb oldali operandus típusával és értékével. Ez az operátor balról jobbra csoportosít. Olyan szövegkörnyezetben, ahol a vesszQnek speciális jelentése van, pl. függvények aktuális argumentumainak listájában (7.1. pont) és a kezdeti értékek listájában (8.6. pont), az itt ismertetett vesszQ operátor csak zárójelek között jelenhet meg; pl.

f (a, (t = 3 , t + 2), c)

-nek három argumentuma van; ezek közül a másodiknak az értéke 5.

8. Deklarációk

A deklarációk segítségével határozzuk meg, hogyan értelmezze a C fordító az egyes azonosítókat; a deklarációk nem feltétlenül jelentenek tárterület-foglalást az azonosító számára. A deklarációk alakja:

deklaráció:
dekl._specifikátorok deklarátorlistaopc;

A deklarátorlistában elhelyezkedQ deklarátorok a deklarálandó azonosítókat tartalmazzák. A deklarációspecifikátorok típus- és tárolásiosztály-meghatározások sorozatából állnak.

dekl._specifikátorok:
típusspecifikátor dekl._specifikátorokopc
t.o._specifikátor dekl._specifikátorokopc

A listát az alábbiak szerint következetesen kell megszerkeszteni.

8.1. Tárolásiosztály-specifikátorok

A tárolásiosztály-specifikátorok az alábbiak:

t.o._specifikátor:
auto
static
extern
register
typedef

A typedef specifikátor nem foglal tárhelyet, és csak a szintaktikai kényelem kedvéért nevezzük tárolásiosztály-specifikátornak (l. a 8.8. pontot). A különféle tárolási osztályok jelentését a 4. pontban ismertettük.
Az auto, static és register deklarációk definícióként is szolgálnak, amennyiben megfelelQ nagyságú tárterület lefoglalását is elQidézik. Az extern esetben a megadott azonosítók külsQ definíciójának (10. pont) is szerepelnie kell valahol azon a függvényen kívül, amelyben deklaráltuk Qket.
A register deklarációt legcélszerqbb olyan auto deklarációnak tekinteni, amely még azt is jelzi a fordítónak, hogy a deklarált változókat sqrqn fogjuk használni. Csupán az elsQ néhány ilyen deklarációnak lesz hatása. Ezenkívül csupán néhány típus tárolódik ténylegesen regiszterekben; a PDP- 11 -en ezek a típusok az int, a char és a mutató. Még egy megszorítás vonatkozik a regiszter típusú változókra: nem alkalmazható rájuk az & (címe valaminek) operátor. A regiszterdeklarációk megfelelQ használatával kisebb méretq, gyorsabb programokhoz juthatunk, a kódgenerálás továbbfejlesztésével azonban lehet, hogy alkalmazásuk feleslegessé válik.
Egy deklarációban legfeljebb egy t. o. -specifikátort lehet megadni. Ha a t.o._specifikátor hiányzik a deklarációból, akkor azt a fordító függvényen belül auto-nak, függvényen kívül extern-nek tekinti. Kivétel: a függvények sohasem automatikusak!

8.2. Típus-specifikátorok

A típus-specifikátorok az alábbiak :

típus-specifikátor:
char
short
int
long
unsigned
float
double
strukt._vagy_union_specifikátor
typedef_név

A long (hosszú), short (rövid) és unsigned (elQjel nélküli) szavakat jelzQknek tekinthetjük; az alábbi kombinációk fogadhatók el:

short int
long int
unsigned int
long float

Az utóbbi ugyanazt jelenti, mint a double. Egyébként egy deklaráción belül legfeljebb egy típus-specifikátor adható meg. Ha a deklarációból hiányzik a típus-specifikátor, akkor a deklarált változót a fordító int-nek tekinti.
Struktúrák és unionok specifikátoraival a 8.5. pont foglalkozik; a typedef nevekkel történQ deklarációkat a 8.8. pont tárgyalja.

8.3. Deklarátorok

A deklarációban megjelenQ deklarátorlista deklarátorok vesszQkkel elválasztott sorozata, amelyek mindegyike kezdeti értékkel (k.é.) rendelkezhet.

deklarátorlista:
k.é._deklarátor
k.é._deklarátor , deklarátorlista
k.é._deklarátor:
deklarátor inicializálóopc

A kezdeti értékekkel a 6.6. pont foglalkozik. A deklarációbeli specifikátorok megadják azoknak az objektumoknak a típusát és tárolási osztályát, amelyekre a deklarátorok vonatkoznak. A deklarátorok szintaxisa:

deklarátor:
azonosító
(deklarátor)
*deklarátor
deklarátor ()
deklarátor [állandó_kifejezésopc]

A csoportosítás ugyanolyan, mint a kifejezésekben.

8.4. A deklarátorokjelentése

Minden deklarátort arra vonatkozó állításnak tekinthetünk, hogy ha valamely kifejezésben a deklarátorral megegyezQ alakú szerkezet jelenik meg, akkor az a megjelölt típusú és tárolási osztályú objektumot fogja eredményezni. Minden deklarátor pontosan egy azonosítót tartalmaz, ez az azonosító az, amelyet deklarálunk.
Ha deklarátorként bQvítmény nélküli azonosító szerepel, akkor annak típusa az lesz, amit a deklarációt bevezetQ specifikátor megjelöl.
A zárójelek közötti deklarátor azonos a zárójel nélkülivel, de az összetett deklarátorok kötési sorrendje zárójelekkel megváltoztatható (l. a következQ példákat).
Most képzeljük el a

T D1

deklarációt, ahol T a típus-specifikátor (mint az int stb.) és D1 a deklarátor. Tegyük fel, hogy e deklaráció hatására az azonosító típusa ". . .T" lesz, ahol ". . " üres, ha D1 csupán sima azonosító (tehát x típusa int x-ben egyszerqen int). Ha viszont D1 alakja

*D

akkor az általa tartalmazott azonosító típusa ". . .mutató T-re".
Ha D1 alakja

D()

akkor az általa tartalmazott azonosító típusa ". . . függvény, amely T-t ad vissza".
Ha D1

D[állandó_kifejezés]

vagy

D[]

alakú, akkor az általa tartalmazott azonosító típusa "T . . . tömbje". Az elsQ esetben az állandó kifejezés olyan kifejezés, amelynek értéke fordítási idQben meghatározható és amelynek típusa int Az állandó kifejezések pontos definíciója a 15. pontban található.) Ha több . . .tömbje specifikáció egymással szomszédos, akkor többdimenziós tömb keletkezik; a tömbhatárokat rögzítQ állandó kifejezések csupán a sorozat elsQ tagjánál hiányozhatnak. Ez az elhagyás akkor hasznos, ha külsQ tömbrQl van szó, és a tárfoglalást elQidézQ definíció máshol szerepel. Az elsQ állandó kifejezés akkor is elhagyható, ha a deklarátort kezdeti érték követi. Ilyenkor a fordító a méretet a megadott kezdeti értékek számából számítja ki.
Tömböt az alaptípusok valamelyikébQl, mutatókból, struktúrákból, unionokból vagy más tömbökbQl (többdimenziós tömböt generálva) alkothatunk.
A fenti szintaxissal definiált lehetQségek közül nem mindegyik megengedett. A megszorítások a következQk : függvények nem adhatnak vissza tömböket, struktúrákat, unionokat vagy függvényeket, de visszaadhatnak ilyeneket megcímzQ mutatókat; függvényekbQl nem képezhetQ tömb, de létezik függvényeket megcímzQ mutatókból képzett tömb. Hasonlóképpen, a struktúrák és unionok sem tartalmazhatnak függvényt, legfeljebb függvényt megcímzQ mutatót.
Például

int i, *ip, f (), *fip (), (*pfi) ()

deklarálja az i egészt, az ip egészt megcímzQ mutatót, az egészt visszaadó f függvényt, az egészt megcímzQ mutatót visszaadó fip függvényt és a pfi mutatót, amely egy egészt visszaadó függvényre mutat. Különösen hasznos ha a két utolsót hasonlítjuk össze. A *fip() kötési sorrendje *(fip()), így a deklaráció azt írja elQ, ill. egy kifejezésben elQforduló ilyen szerkezet azt váltja ki, hogy a fip függvény meghívása után a (mutatójellegq) eredményen keresztüli indirekcióval egy egész álljon elQ. A (*pfi)() deklarátorban (vagy a szerkezetet felhasználó kifejezésekben) a plusz zárójelek szükségesek: azt jelzik, hogy a függvényt megcímzQ mutatón keresztüli indirekció függvényt eredményez, amely meghívása után egészt ad vissza.
Másik példaként

float fa[17], *afp[17];

egy float számokból álló tömböt és egy float számokat megcímzQ mutatókból álló tömböt deklarál. Végezetül

static int x3d[3)[5][7];

egészek statikus, háromdimenziós tömbjét deklarálja, amelynek mérete 3 * 5 * 7. Részleteiben nézve x3d háromelemq tömb; minden elem öt tömböt tartalmaz; az utóbbiak mindegyike 7 darab egészbQl áll. Az x3d, x3d[i], x3d[i][j], x3d[i][j][k] alakok bármelyike elQfordulhat valamely kifejezésben. Az elsQ három tömb típusú, az utolsó típusa int.

8.5. Struktúra- és union deklarációk

A struktúra névvel ellátott tagok sorozatát tartalmazó objektum. Minden tag tetszQleges típusú lehet. Az union olyan objektum, amely adott idQpillanatban több lehetséges tag bármelyikét tartalmazhatja. A struktúra- és az unionspecifikátorok azonos alakúak.

strukt._vagy_union_specifikátor:
strukt._vagy_union { strukt._dekl._lista}
strukt._vagy_union azonosító {strukt._dekl._lista}
strukt._vagy_union azonosító
strukt._vagy_union:
struct
union

A struktúradeklarátor-lista a struktúra vagy union tagjaira vonatkozó deklarációk felsorolása:

strukt._dekl._lista:
strukt._deklaráció
strukt._deklaráció strukt._dekl._lista
strukt._deklaráció:
típus_specifikátor strukt._deklarátor_lista
strukt._deklarátor_lista:
strukt._deklarátor
strukt._deklarátor , strukt._deklarátor_lista

Közönséges esetben a strukt. deklarátor egyszerqen a struktúra vagy union valamely tagjának deklarátora. A struktúra tagjai adott számú bitet is tartalmazhatnak. Az ilyen tag neve mezQ (field), hosszát a névtQl kettQspont választja el.

strukt. _deklarátor:
deklarátor
deklarátor : állandó_kifejezés
: állandó_kifejezés

A struktúrán belül a deklarált objektumok címei a deklarációkban balról jobbra haladva növekednek. A struktúra minden nem-mezQ tagja a típusának megfelelQ címhatáron kezdQdik, így a struktúrában név nélküli lyukak helyezkedhetnek el. A mezQ jellegq tagok gépi egészekben helyezkednek el, szóhatárokon nem nyúlnak át. Az a mezQ, amely nem fér el egy szóban még fennmaradt helyen, a következQ szóba kerül. A mezQ nem lehet szélesebb, mint a szó. MezQk hozzárendelése PDP-11-en jobbról balra, más gépeken balróljobbra történik.
A deklarátor nélküli, csupán kettQspontot és a szélességet tartalmazó struktúradeklarátor olyan név nélküli mezQt jelöl ki, amelyet kívülrQl elQírt elrendezéseknek megfelelQ kitöltésre használhatunk. Speciális esetben a 0 szélességq név nélküli mezQ a következQ mezQ szóhatárra történQ illesztését írja elQ. A "következQ mezQ" feltehetQen tényleg mezQ, nem pedig közönséges struktúratag, mivel az utóbbi esetben ez az illesztés automatikusan megtörténne.
A nyelv nem ír elQ korlátozást a mezQként deklarált objektumok típusára vonatkozólag, a megvalósításoktól azonban nem várjuk el, csak az egész típusú mezQk támogatását. SQt, még az int mezQket is elQjel nélkülinek tekinthetik. A PDP-11-en a mezQknek nincs elQjelük, és csak egész értékqek lehetnek. Egyetlen megvalósításban sincsenek mezQkbQl képzett tömbök, továbbá a mezQkre az & címoperátor sem alkalmazható, vagyis nincsenek mezQket megcímzQ mutatók sem.
Az uniont olyan struktúrának képzelhetjük, amelynek tagjai a 0 relatív címen kezdQdnek, és amelynek mérete elegendQen nagy ahhoz, hogy bármelyik tagját tartalmazhassa. Az union egyszerre legfeljebb egy tagját tartalmazhatja.
A második alakú struktúra- vagy unionspecifikátor, vagyis a

struct azonosító {strukt._dekl._lista}
union azonosító {strukt._dekl._lista}

egyike, az azonosítót a lista által meghatározott struktúra struktúracímkéjeként (vagy unioncímkéjeként) deklarálja. Az ezt követQ deklarációkban azután a specifikátor harmadik alakja, a

struct azonosító
union azonosító

alakok egyike használható. A struktúracímkék lehetQvé teszik önhivatkozó struktúrák definiálását; megengedik, hogy a deklaráció hosszú részét csupán egyszer adjuk meg és több alkalommal használjuk. Tilos olyan struktúrát vagy uniont deklarálni, amelyben saját maga elQfordul, de a struktúra vagy union tartalmazhat saját magát megcímzQ mutatót!
A tagok és címkék nevei megegyezhetnek a közönséges változók neveivel. A címkék és a tagok nevének azonban egymástól el kell térniük!
Két struktúrának lehet közös kezdeti tagsorozata, azaz ugyanaz a tag két különbözQ struktúrában is megjelenhet, ha mindkettQben azonos a típusa és ha az összes megelQzQ tag is mind a kettQben azonos. (A fordító tulajdonképpen csak azt ellenQrzi, hogy a két különbözQ struktúrában elQforduló név típusa és relatív címe megegyezik-e, de ha a megelQzQ tagok különböznek, akkor a szerkezet nem gépfüggetlen.)
A struktúradeklaráció egyszerq példája:

struct tnode {
char tword [20];
int count;
struct tnode * left;
struct tnode *right;
};

amely 20 karakterbQl álló tömböt, egy egészt és két, hasonló struktúrát megcímzQ mutatót tartalmaz. E deklaráció megadása után a

struct tnode s, *sp;

deklaráció szerint s a megadott jellegq struktúra lesz, és sp az ilyen jellegq struktúrát megcímzQ mutató. Ezeknek a deklarációknak az alapján az

sp->count

kifejezés annak a struktúrának a count nevq mezQjére mutat, amelyre az sp utal;

s.left

az s struktúra bal oldali részfájának mutatójára vonatkozik, míg

s.right->tword [0]

az s struktúra jobb oldali részfája tword nevq tagjának elsQ karakterére utal.

8.6. Inicializálás

A deklarátor megadhatja a deklarált azonosító kezdeti értékét. Az inicializálót = elQzi meg, és kapcsos zárójelek közé zárt kifejezést vagy értéklistát tartalmaz.

inicializáló:
= kifejezés
= { inicializáló_lista }
= { inicializáló_lista ,}
inicializáló_lista:
kifejezés
inicializáló_lista , inicializáló_lista
( inicializáló_lista )

A statikus vagy külsQ változók inicializálóiban kizárólag állandó kifejezések (l. a 15. pontot), vagy pedig olyan kifejezések szerepelhetnek, amelyek valamelyik korábban deklarált változó címére redukálhatók (az alábbitól egy állandó kifejezéssel való címeltolás is lehetséges). Az automatikus és regiszterváltozók esetében tetszQleges inicializálás lehetséges állandók, korábban deklarált változók és függvények bevonásával.
Inicializálatlan statikus és külsQ változók kezdeti értéke garantáltan nulla; az inicializálatlan automatikus és regiszterváltozókban pedig induláskor biztos hulladék van.
Ha az inicializálót skalár mennyiségre (mutatóra vagy aritmetikai típusú objektumra) alkalmazzuk, tartalma egyetlen, esetleg kapcsos zárójelek közötti kifejezés. Az objektum kezdeti értékét a gép a kifejezés alapján számítja ki; a konverziók ugyanazok, mint értékadásnál.
Ha a deklarált változó aggregátum (struktúra vagy tömb jellegq összetett mennyiség), akkor az inicializáló az aggregátum tagjainak kapcsos zárójelek közötti, vesszQkkel elválasztott listáját tartalmazza. Az inicializálókat az indexek vagy tagok növekvQ sorrendjében adjuk meg. Ha az aggregátum részaggregátumokat tartalmaz, ugyanez a szabály vonatkozik rekurzív módon az aggregátum tagjaira. Ha a listában kevesebb inicializáló van, mint ahány tagja van az aggregátumnak, akkor az aggregátum nullákkal töltQdik ki. Unionok és automatikus aggregátumok inicializálása nem megengedett!
A kapcsos zárójeleket a következQ módon hagyhatjuk el. Ha az inicializáló bal oldali kapcsos zárójellel kezdQdik, akkor a rákövetkezQ, vesszQkkel elválasztott inicializálólista az aggregátum tagjait inicializálja; Ha, ha itt több inicializáló van, mint tag. Ha azonban az inicializáló nem bal oldali kapcsos zárójellel kezdQdik, akkor a fordítóprogram a listából csupán az aggregátum tagjainak megfelelQ számú elemet vesz figyelembe; a listában fennmaradó tagok annak az aggregátumnak a következQ elemét fogják inicializálni, amelynek a szóban forgó aggregátum a része.
Végül megemlítjük, hogy a char tömbök röviden, karakterláncokkal inicializálhatók. Ez esetben a lánc egymást követQ karakterei a tömb egyes elemeit inicializálják.
Inicializálási példák:

int x [] = {1,3,5};

az x-et olyan egydimenziós tömbként deklarálja és inicializálja, amelynek három eleme van, mivel méretet nem adtunk meg és három inicializáló van.

float y [4][3] ={
{1, 3, 5},
{2, 4, 6},
{3, 5, 7},
};

teljes zárójelezett inicializálás: 1 , 3 és 5 az y[0] tömb elsQ sorát, mégpedig az y[0][0], y[0][1] és y[0][2] elemeket inicializálják. A következQ két sor hasonló módon inicializálja y[1]-et és y[2]-t. Az inicializáló túl hamar ér véget, és ezért y[3] 0-val inicializálódik. Pontosan ugyanezt az eredményt értük volna el

float y [4][3] ={
1, 3, 5, 2, 4, 6, 3, 5, 7
};

megadásával. y inicializálója bal oldali kapcsos zárójellel kezdQdik, de y[0]-é nem, így a gép a listából három elemet használ fel. Hasonlóképpen a következQ három y[1 ]-é, az azt követQ három pedig y[2]-é lesz. Ugyanígy, ;

float y [4][3] ={
{1}, {2}, {3}, {4}
};

a (kétdimenziós tömbnek tekintett) y elsQ oszlopát inicializálja és a többi elemet 0 értékqnek hagyja meg.
Végezetül

char msg [] = "Szintaktikai hiba a %s-edik sorban \n";

olyan karaktertömböt mutat, amelynek elemeit karakterlánccal inicializáltuk.

8.7. Típusnevek

Két összefüggésben (típusmódosító szerkezettel végzett explicit típuskonverzió esetén és a sizeof argumentumaként) kell valamilyen adattípus nevét megadnunk. Ez típusnév használatával történik, ami lényegében egy adott típusú objektum olyan deklarációja, amelybQl hiányzik az objektum neve.

típus_név:
típus_specifikátor absztrakt_deklarátor
absztrakt_deklarátor:
üres
( absztrakt_deklarátor )
*absztrakt_deklarátor
absztrakt_deklarátor ()
absztrakt_deklarátor [állandó_kifejezésopc]

A kétértelmqség elkerülése érdekében az

( absztrakt_deklarátor )

szerkezetben az absztraktdeklarátor nem lehet üres. E megszorítás figyelembevételével egyértelmqen azonosítható az absztrakt-deklarátorban az a hely, ahol az azonosító megjelenne, ha a szerkezet egy deklaráción belüli deklarátor lenne. A megnevezett típus ekkor ugyanaz lesz, mint a hipotetikus azonosító típusa. Pl.

int
int *
int *[3]
int (*) [3]
int * ()
int (*) ()

sorban megnevezi az egész, egészt megcímzQ mutató, 3 darab egészmutatóból álló tömb, 3 egészbQl álló tömböt megcímzQ mutató, egészt megcímzQ mutatót visszaadó függvény és az egészt visszaadó függvényt megcímzQ mutatótípusokat.

8.8. Typedef

Az olyan deklarációk, amelyeknek a tárolási osztálya typedef, nem tárterületet definiálnak, hanem olyan azonosítókat, amelyeket a késQbbiekben úgy használhatunk, mintha az alapvetQ vagy a leszármaztatott típusokat megnevezQ kulcsszavak lennének:

typedef_név:
azonosító

A typedef-et tartalmazó deklaráció érvényességi tartományán belül minden ott elQforduló deklarátor részeként megjelenQ azonosító szintaktikusan egyenértékq lesz azzal a típuskulcsszóval, amely a 8.4. pontban leírt módon megnevezi az azonosítóhoz társított típust. Pl.

typedef int MILES, *KLICKSP;
typedef struct { double re, im;}complex;

után a

MILES distance;
extern KLICKSP metricp;
complex z, *zp;

szerkezetek mindegyike megengedett deklaráció; a distance típusa int, a metricp-é int-et megcímzQ mutató, a z-é pedig a megadott struktúra. zp az ilyen struktúrát megcímzQ mutató.
A typedef nem teljesen új típusokat vezet be, csupán más módon is megadható típusok szinonimáit. Igy a fenti példában distance pontosan ugyanolyan típusú, mint minden más int objektum.

9. Utasítások

Az utasítások egymást követQen, sorban hajtódnak végre, az ettQl való eltérést külön jelezzük.

9.1. A kifejezés utasítás

A legtöbb utasítás kifejezés jellegq; ezek alakja:

kifejezés;

A kifejezés jellegq utasítások legtöbbször értékadások vagy függvényhívások.

9.2. Az összetett utasítás vagy blokk

Annak érdekében, hogy ott, ahol elvileg csak egy utasítás helyezhetQ el, több utasítás is használható legyen, rendelkezésre áll az összetett utasítás (más szóval blokk).

összetett_utasítás:
{ deklarációlistaopc utasításlistaopc}
deklarációlista:
deklaráció
deklaráció deklarációlista
utasításlista:
utasítás
utasítás utasításlista

Ha a deklarációlistában elQforduló bármelyik azonosítót már korábban deklaráltuk, a külsQ deklaráció a blokk végrehajtásának idQtartamára érvényét veszti, majd annak befejeztével visszanyeri hatályát.
Az auto és register változók bármilyen inicializálása minden alkalommal újra megtörténik, amikor a vezérlés a blokkba felülrQl belép. Jelenleg lehetséges (de helytelen gyakorlat) a blokk belsejébe való ugratás; ez esetben az inicializálások elmaradnak. A static változók kezdeti értékének beállítása csupán egyszer, a program végrehajtásának kezdetén történik meg. A blokkon belül az extern deklarációk hatására nincs tárfoglalás, így ezek inicializálása nem megengedett.

9.3. A feltételes utasítás

if (kifejezés)
utasítás
if (kifejezés)
utasítás
else utasítás

A gép mindkét esetben kiértékeli a kifejezést, és ha értéke nemnulla, az elsQ alutasítást hajtja végre. A második esetben, ha a kifejezés értéke 0, a második alutasítást hajtja végre. Az else-vel kapcsolatos szokásos kétértelmqséget a C úgy oldja fel, hogy az else az utoljára talált else nélküli if-hez kötQdik.

9.4. A while utasítás

A while utasítás alakja:

while (kifejezés)
utasítás

Az alutasítás végrehajtása mindaddig ismétlQdik, amíg a kifejezés értéke nemnulla marad. A vizsgálat mindig az utasítás egyes végrehajtásai elQtt történik.

9.5. A do utasítás

A do utasítás alakja

do
utasítás
while (kifejezés);

Az alutasítás végrehajtása mindaddig ismétlQdik, amíg kifejezés értéke nullává nem válik. A vizsgálat mindig az utasítás egyes végrehajtásai után történik.

9.6. A for utasítás

A for utasítás alakja:

for (1._kifejezésopc; 2._kifejezésopc; 3._kifejezésopc)
utasítás

Ez az utasítás egyenértékq az

1._kifejezés;
while (2._kifejezés) {
utasítás
3._kifejezés;
}

alakkal. Eszerint az elsQ kifejezés a ciklust inicializálja; a második azt a vizsgálatot határozza meg, amely minden iterációt megelQz, és a vezérlés kilép a ciklusból, ha a kifejezés nullává válik; a harmadik kifejezés gyakran az egyes iterációk után végrehajtandó inkrementálást határozza meg.
A kifejezések bármelyike, vagy akár mindegyik elhagyható. Ha a 2. kifejezés hiányzik, akkor a megfelelQ while utasításból while( 1 ) lesz; a többi hiányzó kifejezés egyszerqen elmarad az elQbbi kifejtett formából.

9.7. A switch utasítás

A switch utasítás hatására a megadott kifejezés értékétQl függQen a vezérlés több utasítás valamelyikére adódik át. Alakja:

switch (kifejezés)
utasítás

A kifejezésben megtörténnek a szokásos aritmetikai konverziók, de az eredménynek int-nek kell lennie. Az utasítás általában összetett. A switch utasításon belül elQforduló bármelyik utasítás megcímkézhetQ egy vagy több case elQtaggal az alábbi módon :

case állandó_kifejezés:

ahol az állandó kifejezés int kell, hogy legyen. Ugyanazon a switch-en belül két case állandónak nem lehet egyforma értéke. Az állandó kifejezések pontos definícióját a 15. pont tartalmazza.
Legfeljebb egy darab

default:

alakú utasítás-elQtag is elQfordulhat a switch utasításban. A switch utasítás végrehajtása során a gép kiértékeli a benne elQforduló kifejezést és összehasonlítja minden egyes case állandóval. Ha a case állandók valamelyike megegyezik a kifejezés értékével, a vezérlés az illeszkedQ case elQtagot követQ utasításra adódik át. Ha egyik állandó sem egyezik meg a kifejezés értékével, és szerepel a default elQtag, akkor a program végrehajtása az ezt követQ utasításon folytatódik. Ha egyik case sem illeszkedik és nincs default, akkor a gép a switch-ben elQforduló utasítások közül egyiket sem hajtja végre.
A case és default elQtagok egymagukban nem változtatják meg a vezérlés menetét, amely zavartalanul végighalad ezeken az elQtagokon. A switchbQl való kilépésre vonatkozólag l. a break utasítást a 9.8. pontban.
A switch tárgyát képezQ utasítás legtöbbször összetett. Deklarációk szerepelhetnek ennek az utasításnak a fejében, de az automatikus és regiszterváltozók inicializálásai hatástalanok.

9.8. A break utasítás

A

break;

utasítás hatására befejezQdik a break-et körülvevQ legbelsQ while, do, for vagy switch utasítás végrehajtása; a vezérlés a befejezett utasítást követQ utasításra adódik át.

9.9. A continue utasítás

A

continue;

utasítás hatására a vezérlés a continue-t körülvevQ legbelsQ while, do vagy for utasítás ciklusfolytató részére adódik át, vagyis a ciklus végére. Pontosabban, a

while (...) { do { for (...) {
... ... ...
contin: ; contin: ; contin: ;
} } while (...); }

utasítások mindegyikében a continue utasítás egyenértékq a goto contin-nel. A contin: után nulla utasítás szerepel, (l. a 9.13. pontot).

9.10. A return utasítás

A függvény a hívójához a return utasítás segítségével tér vissza, amelynek lehetséges alakja:

return ;
return kifejezés;

Az elsQ esetben a visszaadott érték határozatlan. A második esetben a kifejezés értéke kerül vissza a függvény hívójához. Szükség esetén az értékadáshoz hasonlóan a kifejezés olyan típusúvá alakul át, mint amilyen típusú függvényben elQfordul. A függvény végének átlépése azonos a visszatérési érték nélküli return-nel.

9.11. A goto utasítás

A vezérlés feltétel nélkül a

goto azonosító;

utasítás segítségével adható át. Az azonosító az éppen végrehajtott függvényen belül elhelyezett címke (l. a 9.12. pontot) kell, hogy legyen.

9.12. A címkézett utasítás

Bármelyik utasítást megelQzhetik az

azonosító:

alakú elQtagok, amelyek az azonosítót címkeként deklarálják. A címke egyedül a goto célpontjaként szolgál. A címke érvényességi tartománya az a függvény, amelyben elQfordul, kivéve azokat az alblokkokat, amelyekben ugyanezt az azonosítót újradeklarálták (l. a 11. pontot).

9.13. A nulla utasítás

A nulla utasítás alakja:

;

A nulla utasítás hordozhat pl. címkét közvetlenül valamely összetett utasítás }-e elQtt, vagy pedig a while-hoz hasonló valamelyik ciklusutasítás számára üres ciklustörzset képezhet.

10. KülsQ definíciók

A C program külsQ (external) definíciók sorozatát tartalmazza. A külsQ definíció a változót extern (ez az alapértelmezés) vagy static tárolási osztályúnak és megadott típusúnak deklarálja. A típus-specifikátor (l. a 8.2. pontot) lehet üres, ebben az esetben a típust int-nek tekintjük. A külsQ definíciók érvényességi tartománya annak az állománynak a végéig tart, amelyben deklarálták Qket; hasonlóképpen a deklarációk is az állomány végéig érvényesek. A külsQ definíciók szintaxisa ugyanaz, mint az összes deklarációé, azzal a különbséggel, hogy a függvényeket csak ezen a szinten lehet definiálni.

10.1. KülsQ függvénydefiníciók

A függvénydefiníciók alakja:

függvénydefiníció:
dekl._specifikátorokopc függvény deklarátor függvénytörzs

A deklarációspecifikátorok közül tárolásiosztály-specifikátorként csupán az extern és a static megengedett; a kettQ közötti különbségre nézve l. a 11.2. pontot. A függvénydeklarátor hasonló a "függvény, amely ...-t ad vissza" jellegq deklarátorhoz, azzal a különbséggel, hogy megadja a definiált függvény formális paramétereinek listáját.

függvénydeklarátor:
deklarátor (paraméterlistaopc)
paraméterlista:
azonosító
azonosító , paraméterlista

A függvénytörzs alakja:

függvénytörzs:
deklarációlista összetett_utasítás

A paraméterlistabeli azonosítók - és csakis ezek deklarálhatók a deklarációlistában. Az olyan azonosítót, amelynek típusát nem adtuk meg, a fordítás int-nek tekinti. Az egyetlen megadható tárolási osztály a register; ha ez szerepel, akkor a neki megfelelQ aktuális paraméter, amennyiben lehetséges, a függvény végrehajtásának kezdetén valamelyik regiszterbe kerül.
Egyszerq példa a teljes függvénydefinícióra:

int max (a, b, c)
int a, b, c;
{
int m;
m = (a > b) ? a : b;
return ((m > c) ? m : c);
}

Itt az int a típus-specifikátor; max(a, b, c) a függvénydeklarátor;

int a, b, c;

a formális paraméterek deklarációinak listája; { . . } az utasítás programkódját megadó blokk.
A C az összes float típusú aktuális paramétert double-lá alakítja át, így a float-nak deklarált formális paraméterek deklarációi is double-lá módosulnak. Továbbá, mivel a tömbre történQ hivatkozás bármilyen összefüggésben különösen aktuális paraméterként) olyan mutatót jelent, amely a tömb elsQ elemére mutat, a ". . . tömbje" alakban deklarált formális paraméterek deklarációi "mutató . . .-ra" alakúra módosulnak. Végezetül, mivel a struktúrák, unionok és függvények nem adhatók át függvénynek, értelmetlen dolog formális paramétereket struktúrának, unionnak vagy függvénynek deklarálni (az ilyen objektumokat megcímzQ mutatók természetesen megengedettek).

10.2. KülsQ adatdefiníciók

A külsQ adatdefiníciók alakja:

adatdefiníció:
deklaráció

Az ilyen adatok tárolási osztálya lehet extern (ez az alapértelmezés) vagy static, de nem lehet auto, sem pedig register.

11. Az érvényességi tartomány szabályai

Nem szükséges az egész C programot egyszerre fordítani: a program forrásszövege több állományban tárolható, és könyvtárakból elQre lefordított rutinokat lehet betölteni. A program függvényei közötti kommunikáció akár explicit hívásokkal, akár külsQ adatokon keresztül megvalósítható.
Ennek következtében kétféle érvényességi tartományról kell beszélnünk: elQször is arról, amit az azonosító lexikális érvényességi tartományának nevezünk, és ami lényegében a programnak az a része, amelyben a definiálatlan azonosító ("undefined identifier") hibaüzenet elQfordulása nélkül használhatjuk, másodszor pedig a külsQ azonosítókhoz tartozó érvényességi tartományról, amelyre az a szabály jellemzQ, hogy az ugyanarra a külsQ azonosítóra vonatkozó hivatkozások ugyanarra az objektumokra való hivatkozásokat jelentenek.

11.1. Lexikális érvényességi tartomány

A külsQ definíciókban deklarált azonosítók lexikális érvényességi tartománya a definícióktól az Qket tartalmazó forrásállomány végéig tart. A formális paraméterként elQforduló azonosítók érvényességi tartománya az a függvény, amelyhez tartoznak. A blokkok fejében deklarált azonosítók érvényességi tartománya a blokk végéig terjed. A címkék érvényességi tartománya az egész függvény,amelyben elQfordulnak.
Mivel az ugyanarra a külsQ azonosítóra utaló összes hivatkozás ugyanarra az objektumra vonatkozik (l. a 11.2. pontot), a fordítóprogram ellenQrzi, hogy ugyanannak a külsQ azonosítónak az összes deklarációja kompatibilis-e; valójában ezek érvényességi tartománya kiterjed az egész állományra, amelyben elQfordulnak.
Minden esetben fennáll azonban, hogy ha egy azonosító explicit módon egy blokk - akár függvényt alkotó blokk - fejében deklarálunk, akkor annak végéig az illetQ azonosító összes, a blokkon kívül elQforduló deklarációja felfüggesztQdik.
Emlékezzünk arra is (l. a 8.5. pontot), hogy egyrészt a közönséges változókhoz, másrészt a struktúra-, ill. uniontagokhoz és - címkékhez kapcsolódó változók két külön osztályt alkotnak, amelyek között nincs ütközés. A tagokra és címkékre ugyanazok az érvényességi tartomány szabályok vonatkoznak, mint a többi azonosítókra. A typedef nevek ugyanabba az osztályba tartoznak, mint a közönséges azonosítók, belsQ blokkokban újradeklarálhatók, de a belsQ deklarációban a tipust explicit módon meg kell adni:

typedef float distance;
. . .
{
auto int distance;
. . .

Az int-nek szerepelnie kell a második deklarációban, különben a fordító deklarátor nélküli, distance típusú deklarációnak tekintené.

11.2. A külsQ azonosítók érvényességi tartománya

Ha egy függvény extern-ként deklarált azonosítóra hivatkozik, akkor a teljes programot alkotó állományok, ill. könyvtárak közül valamelyikben szerepelnie kell az azonosító külsQ definíciójának. Egy adott programban elQforduló minden olyan függvény, amely ugyanarra a külsQ azonosítóra hivatkozik, egyben ugyanarra az objektumra is hivatkozik, ezért ügyelnünk kell arra, hogy a definícióban megadott típus és méret kompatibilis legyen minden egyes, az adatokra hivatkozó függvényben megadott típussal és mérettel. Az extern kulcsszó a külsQ definícióban azt jelzi, hogy a deklarált azonosítók számára szükséges tárhelyet valamely másik állományban foglaljuk le. îgy több állományból álló programban extern specifikátor nélküli külsQ adatdefiníció egy és csakis egy állományban szerepelhet. Az összes többi állományban, ahol külsQ definícióval kívánjuk valamelyik változót megadni, a definícióban szerepelnie kell az extern-nek. Az azonosító csak abban a deklarációban inicializálható, ahol a tárhely lefoglalása történt.
A legfelsQ szinten külsQ definíciókban static-ként deklarált azonosítók más állományokban nem láthatók. Függvények is deklarálhatók static-ként.

12. A fordítónak szóló vezérlQsorok

A C fordító része egy elQfeldolgozó program, amely makrohelyettesítésre, feltételes fordításra és megadott nevq állományok beiktatására képes. Az elQfeldolgozó a # karakterrel kezdQdQ sorokat értelmezi. E sorok szintaxisa független a nyelv többi részétQl, bárhol elQfordulhatnak, és (érvényességi tartománytól függetlenül) hatásuk az adott forrásprogram-állomány végéig tart.

12.1. Szintaktikai egységek helyettesítése

A

#define azonosító szint._egységek_karakterlánca

alakú fordító vezérlQ sor (vigyázat: nincs záró pontosvesszQ) hatására az elQfeldolgozó az azonosító minden további elQfordulását a szintaktikai egységek megadott karakterláncával helyettesíti. A

#define azonosító(azonosító, . . .,azonosító)
szint._egységek_karakterlánca

alakú sor, ahol az elsQ azonosító és a ( között nincs szóköz, argumentumokkal ellátott makrodefiníció. Az elsQ azonosítónak azon további elQfordulásait, ahol az azonosítót ( , szintaktikai egységek vesszQkkel elválasztott sorozata és egy ) követi, a definícióban megadott szintaktikai egység karakterlánccal helyettesíti. A definíció formális paraméterlistájában említett azonosító összes elQfordulása helyére a hívás hatására a megfelelQ szintaktikai egység karakterlánc kerül. A hívás aktuális argumentumai vesszQkkel elválasztott szintaktikai egység karakterláncok, azonban az idézQjelek közötti vagy zárójelekkel védett vesszQk nem argumentumelválasztók. A formális és aktuális paraméterek darabszáma egyenlQ kell, hogy legyen. Karakterláncon vagy karakterállandón belüli szövegre nem vonatkozhat a helyettesítés.
A helyettesítQ karakterláncot (mindkét változatban) újra átvizsgálja az elQfeldolgozó, hogy megtalálja az esetleges további definiált azonosítókat. A hosszú definíciók mindkét alakban új sorban folytathatók oly módon, hogy a folytatandó sor végére \-t írunk.
A #define használat_ leginkább a hangsúlyozott funkciójú állandók definiálására elQnyös, pl.:

#define TABSIZE 100
int table[TABSIZE];

Az

#undef azonosító

alakú vezérlQsor hatására megszqnik az azonosító elQfeldolgozó-definíciója.

12.2. 9llományok beiktatása

Az

#include "állománynév"

alakú vezérlQsort az elQfeldolgozó program az állománynév nevq állomány teljes tartalmával helyettesíti. A megnevezett állomány keresése az eredeti forrásállomány katalógusában kezdQdik, majd sorban, szabványos helyeken folytatódik. Megadhatjuk az

#include <állománynév>

alakú vezérlQsort is, amikor a keresés csak a szabványos helyeken történik, és nem terjed ki a forrásállomány katalógusára. Az #include-ok egymásba skatulyázhatók.

12.3. Feltételes fordítás

Az

#if állandó_kifejezés

alakú fordításvezérlQ sor ellenQrzi, hogy az állandó kifejezés (l. a 15. pontban) értéke nemnulla-e. Az

#ifdef azonosító

alakú vezérlQsor megvizsgálja, hogy az azonosító pillanatnyilag definiálva van-e az elQfeldolgozóban, azaz szerepelt-e már valamelyik #define vezérlQsorban. Az

#ifndef azonosító

alakú vezérlQsor azt ellenQrzi, hogy az azonosító pillanatnyilag definiálatlan-e az elQfeldolgozóban.
Mindhárom alakot tetszQleges számú, esetleg az

#else

vezérlQsort is tartalmazó sor, majd az

#endif

vezérlQsor követi. Ha a vizsgált feltétel igaz, akkor az #else és az #endif közötti sorok hatástalanok. Ha a vizsgált feltétel hamis, akkor az ellenQrzés és az #else vagy annak hiányában a #endif közötti sorok lesznek hatástalanok.
E szerkezetek egymásba skatulyázhatók.

12.4. Sorvezérlés

Egyéb, C programokat létrehozó elQfeldolgozók szempontjából hasznos a

#line állandó_azonosító

alakú sor. Hatására - diagnosztikai célokból a fordító azt hiszi, hogy a következQ forrássor sorszáma az állandó által megadott érték, és a pillanatnyi bemeneti állomány az, amelyet az azonosító megnevez. Azonosító hiányában a megnevezett állománynév nem változik.

13. Implicit deklarációk

A deklarációban nem mindig kell a tárolási osztályt és az azonosítók tipusát is megadnunk. A tárolási osztályt külsQ definíciókban és formális paraméterek ill. a struktúratagok deklarációiban a szövegkörnyezet határozza meg. Függvényen belüli deklarációban, ha a tárolási osztályt megadtuk, de a típust nem, az azonosító feltételezés szerint int; ha típus szerepel, de tárolási osztály nem, akkor az azonosítót auto-nak tekinti a fordító. Az utóbbi szabály alól kivételek a függvények, mivel az auto függvényeknek nincs értelmük (a C nem képes kódot generálni a verembe); ha valamely azonosító típusa "függvény, amely ...-t ad vissza", akkor az implicite extern-nek deklarálódik.
Kifejezésekben az olyan, még nem deklarált azonosítót, amelyet ( követ, a szövegkörnyezet alapján a fordító int-et visszaadó függvénynek tekinti.

14. Még egyszer a típusokról

Ez a szakasz azokat a mqveleteket foglalja össze, amelyeket csak bizonyos típusú objektumokon lehet elvégezni.

14.1. Struktúrák és unionok

Struktúrákkal és unionokkal két dolgot tehetünk: megnevezhetjük valamelyik tagjukat (a . operátorral), vagy elQállíthatjuk a címüket (az egyoperandusú &-tel). Az egyéb mqveletek, mint a struktúrák vagy unionok valamihez történQ hozzárendelése, paraméterként való átadása, vagy nekik való értékadás hibaüzenetet von maga után. Reméljük, hogy a jövQben a C, ha egyebekkel nem is, de ezekkel a mqveletekkel kiegészül.
A 7.1. pontban mondottak szerint a ( . vagy -> segítségével történQ) direkt vagy indirekt struktúrahivatkozásban a jobb oldalon álló névnek a bal oldali kifejezés által megnevezett vagy megcímzett struktúra tagjának kell lennie. A rugalmas típuskezelés érdekében ezt a megkötést a fordító követeli meg szigorúan. Valójában a . elQtt bármilyen balérték megengedett, és a fordító feltételezi, hogy ez a balérték olyan alakú struktúra, mint amilyen a jobb oldali név tagja. A -> elQtti kifejezésnek ugyancsak mutatónak vagy egésznek kell lennie. Ha a kifejezés mutató, akkor feltételezés szerint arra a struktúrára mutat, amelyiknek a jobb oldalon álló név tagja. Ha a kifejezés egész típusú, akkor a fordító a megfelelQ struktúra (gépi tárolási egységekben kifejezett) abszolút címének tekinti.
Az ilyen konstrukciók nem gépfüggQek.

14.2. Függvények

Függvénnyel csupán két mqveletet végezhetünk: meghívhatjuk vagy elQállíthatjuk a címét. Ha a függvény neve kifejezésen belül nem valamely hívás függvénynév-pozícióján jelenik meg, akkor a függvényt megcímzQ mutató jön létre. Ha tehát egy függvényt egy másiknak akarunk átadni, azt mondhatjuk, hogy:

int f ();
. . .
g (f);

Ekkor a g definíciója

g (funcp)
int (*funcp) ();
{
. . .
(*funcp) ();
. . .
}

lehet. Jegyezzük meg, hogy f-et a hívó rutinban explicit módon deklarálni kell, mivel g (f)-beli elQfordulását nem követte (.

14.3. Tömbök, mutatók és indexelés

Minden alkalommal, amikor tömb típusú azonosító jelenik meg egy kifejezésben, az azonosító a tömb elsQ elemét megcímzQ mutatóvá alakul át. E konverzió miatt a tömbök nem balértékek. Definíció szerint a [ ] indexoperátor értelmezése olyan, hogy

E1 [E2]

azonos

*((E1)+(E2))

-vel. A +-ra vonatkozó konverziós szabályok következtében, ha E1 tömb és E2 egész, akkor

E1 [E2]

az E1 tömb E2-dik elemére hivatkozik. Emiatt - _aszimmetrikus megjelenése ellenére - az indexelés kommutatív mqvelet.
A többdimenziós tömbökre következetes szabály vonatkozik. Ha E n-dimenziós, i * j * . . . * k-rangú tömb , akkor kifejezésekben (n-1 )-dimenziós, j*...*k-rangú tömböt megcímzQ mutatóvá alakul át. Ha a * operátor akár explicit, akár indexelés következtében implicit módon erre a mutatóra alkalmazzuk, az eredmény a megcímzett (n-1 )-dimenziós tömb, amely maga is azonnal mutatóvá alakul át.
Tekintsük pl. az

int x [3][5];

deklarációt. Itt x 3*5-ös egész tömb. Ha x kifejezésben jelenik meg, akkor x a három darab 5-tagú egész tömb közül az elsQt megcímzQ mutatóvá alakul át. Az x[i) kifejezésben, amely *(x+i)-vel egyenértékq, x elQször az ismertetett módon mutatóvá, majd i az x típusával azonos típusúvá alakul, ami magában foglalja azt, hogy i megszorzódik annak az objektumnak a hosszával, amelyre a mutató mutat: ez jelen esetben 5 egész objektum. Az eredmények összeadódnak, és indirekció alkalmazásával (5 egészbQl álló) tömb keletkezik, amely viszont ezen egészek közül az elsQt megcímzQ mutatóvá alakul át. Ha még további index is van, ismét ugyanezt a megfontolást kell alkalmazni; esetünkben az eredmény egész.
A fentiekbQl következik, hogy a C-ben a tömbök sorfolytonosan tárolódnak (az utolsó index változik a leggyorsabban), továbbá, hogy a deklarációban elQforduló elsQ index segítségével határozható meg a tömb által elfoglalt tárterület nagysága, egyéb szerepe azonban az indexszámításokban nincs.

14.4. Explicit mutatókonverziók

A mutatókra bizonyos konverziók megengedettek ugyan, de gépfüggQ vonatkozásaik vannak. Valamennyi ilyen konverziót explicit típuskonverziós operátorral írhatjuk elQ (l. a 7.2. és 8.7. pontot).
Mutatók bármely olyan integrális típussá átalakíthatók, amelyben elférnek. Az, hogy ez a típus int vagy long-e, gépfüggQ. A leképzés maga is gépfüggQ, de azok számára, akik ismerik a gép címzési struktúráját, nem okozhat meglepetést. A késQbbiekben néhány gépre vonatkozóan a részleteket is ismertetjük.
Az integrális típusú objektumok explicit módon mutatókká alakíthatók át. A leképzés hatására a mutatókból létrejött egészek ugyanazokká a mutatókká alakulnak vissza, egyébként a folyamat gépfüggQ.
Adott típust megcímzQ mutató más típust megcímzQ mutatóvá alakítható. Az eredményül kapott mutató címzési zavarokat okozhat, ha a szóban forgó mutató által megcímzett objektum illeszkedése a tárban nem megfelelQ. Bizonyos azonban, hogy adott méretq objektumot megcímzQ mutató változatlan marad, ha elQször kisebb méretq objektumot, majd ismét az eredeti méretq objektumot megcímzQ mutatóvá alakítjuk.
A tárterület-foglaló rutin pl. elfogadhatja valamely kiutalandó objektum (byte-okban megadott) méretét és char mutatót adhat vissza:

extern char *alloc ();
double *dp;
dp = (double *) alloc (sizeof (double));
*dp = 22.0 / 7.0;

Az alloc-nak (gépfüggQ módon) biztosítani kell, hogy a visszaadott értéket át lehessen alakítani double mutatóvá; ebben az esetben a függvény használata gépfüggetlen.
A PDP- 11 mutatóábrázolása 16 bites egésznek felel meg, egysége a byte. A char-okkal szemben nincsenek illeszkedési követelmények; minden másnak páros címqnek kell lennie.
A Honeywell 6000 gépen a mutató 36 bites egésznek felel meg: a szórész a bal oldali 18 biten van, és az a két bit, amely a szón belül a karaktert választja ki, ettQl közvetlenül jobbra található. Igy a karaktermutatókat a 216 byte-os egységekben mérjük, minden más 218 gépi szó egységekben mérhetQ. A double mennyiségeknek és az azokat tartalmazó aggregátumoknak páros szócímen kell elhelyezkedniük (0 mod 219).
Az IBM 370 és az Interdata 8/32-es gépek hasonlóak. A címeket mindkettQn byte-okban mérjük; az elemi objektumoknak a hosszuknak megfelelQ határra kell illeszkedniük, így a short-ot megcímzQ mutatóknak (0 mod 2)-nek, az int-re és float-ra mutatóknak (0 mod 4)-nek és a double-ra mutatóknak (0 mod 8)-nak kell lenniük. Aggregátum illesztése az alkotóelemeire vonatkozó illeszkedési feltételek közül a legszigorúbb szerint történik.

15. 9llandó kifejezések

A C nyelvben több helyen kell alkalmaznunk olyan kifejezéseket, amelyeket kiértékelve állandó eredményt kapunk: case után, tömbhatárként, kezdeti értékekként. Az elsQ két esetben a kifejezésben csupán egész állandók, karakterállandók és sizeof kifejezések szerepelhetnek, amelyeket a

+ - * / % & | ^ << >> == != < > <= >=

két-, ill a

- ~

egyoperandusú operátorok valamelyike vagy a háromoperandusú

? :

operátor köthet össze egymással. A zárójelek csoportosításra használhatók, függvényhívásra azonban nem.
Kevesebb megkötés vonatkozik a kezdeti értékekre; az elQbb tárgyalt állandó kifejezéseken kívül külsQ és statikus objektumokra, valamint állandó kifejezéssel indexelt külsQ és statikus tömbökre is alkalmazható az egyoperandusú & operátor. Implicit módon az egyoperandusú &-et indexeletlen tömbök és függvények megjelenésekor ugyancsak alkalmazhatjuk. Az alapszabály az, hogy a kezdeti értékek kiértékelésével vagy állandót, vagy pedig valamely már korábban deklarált külsQ vagy statikus objektum (esetleg állandóval növelt vagy csökkentett) címét kell megkapnunk.

16. Gépfüggetlenség

A C nyelv bizonyos részei lényegüknél fogva gépfüggQk. Az alábbiakban nem térhettünk ki minden problémára, csak a legfontosabbakat akartuk kiemelni.
Az olyan tisztán hardverkérdések, mint a szavak mérete, a lebegQpontos aritmetika tulajdonságai és az egészek osztása a gyakorlatban nem okoztak különösebb gondot. A hardver egyéb jellegzetességei az eltérQ megvalósításokban mutatkoznak meg. Ezek némelyike, különösen az elQjel-kiterjesztés (negatív karakter negatív egésszé történQ átalakítása), valamint a byte-ok szavakon belüli elhelyezkedési sorrendje olyan kellemetlen tényezQk, amelyekre különös figyelmet kell fordítanunk. Az egyéb gépfüggQ tulajdonságok már nem jelentenek nagyobb problémát.
A regiszterekben ténylegesen elhelyezkedQ register típusú változók száma - a megengedett típuskészlethez hasonlóan - géprQl gépre változik. Minden fordító helyesen végzi azonban a dolgát a saját gépe szempontjából: a fölös számú vagy érvénytelen register deklarációkat nem veszi figyelembe.
Nehézségek csak akkor támadnak, amikor valaki rossz programozási módszereket alkalmaz. Ne írjunk olyan programokat, amelyek az adott architektúra bármilyen specifikus tulajdonságától függetlenek!
A függvényargumentumok kiértékelési sorrendjét a nyelv nem határozza meg. PDP- 11-en jobbról balra, a többi gépen balról jobbra történik. A mellékhatások érvényesülésének sorrendje ugyancsak nem meghatározott.
Mivel a karakterállandók valójában int típusú objektumok, több karakterbQl álló karakterállandók használata is megengedett. Ennek megvalósítása azonban rendkívül gépfüggQ, mivel a karakterek szóhoz történQ hozzárendelésének sorrendje géprQl gépre változik.
MezQk hozzárendelése szavakhoz, karaktereké egészekhez a PDP 11-en jobbról balra, a többi gépen balról jobbra történik. Elszigetelt programok számára e különbségek láthatatlanok maradnak, hacsak nem viszik túlzásba a típusokkal folytatott játékot (pl. azáltal, hogy valamely int mutatót char mutatóvá alakítanak át, majd megvizsgálják a megcímzett tárterületet). Számolnunk kell azonban e különbségekkel akkor, ha a programunkat kívülrQl megszabott tárterület-elrendezésekkel akarjuk összhangba hozni.
A különféle fordítók által elfogadott nyelvek csupán egészen kis részletekben térnek el egymástól. A leglényegesebb, hogy a pillanatnyilag használatos PDP-11-es fordító nem inicializálja a bitmezQket tartalmazó struktúrákat, és egyes értékadó operátorokat nem fogad el olyan környezetben, ahol ki akarjuk használni a hozzárendelés értékét.

17. Anakronizmusok

Mivel a C fejlQdésben levQ nyelv, egyes régebbi programokban bizonyos elavult szerkezetek találhatók. Bár a fordító legtöbb változata az ilyen anakronizmusokat is támogatja, elQbb-utóbb ezek el fognak tqnni, csupán gépfüggQségi problémát hagyva maguk után.
A C nyelv korábbi változatai értékadó operátorként az =op alakot használták az op= alak helyett. Ez kétértelmqségekhez vezet, amelynek tipikus esete

x = -1

amely a valóságban x-et dekrementálja, mivel az = és a - szomszédosak, de amivel könnyen az lehetett a szándékunk, hogy -1-et rendeljünk x-hez.
A kezdeti értékek szintaxisa megváltozott: korábban a kezdeti értéket bevezetQ egyenlQségjel nem szerepelt, így az

int x = 1;

alak helyett az

int x 1;

alak volt használatban. A változtatás azért történt, mert az

int f (1+2)

alakú inicializálás éppen eléggé hasonlít a függvénydeklarációra ahhoz, hogy megtévessze a fordítókat.

18. A szintaxis összefoglalása

A C nyelv szintaxisának összefoglalása sokkal inkább tömör segédletül, mintsem a nyelv rövid összefoglalásául szolgál.

18.1. Kifejezések

Az alapvetQ kifejezések a következQk:

kifejezés:
elsQdleges_kifejezés
*kifejezés
&kifejezés
-kifejezés
!kifejezés
~kifejezés
++balérték
--balérték
balérték ++
balérték --
sizeof kifejezés
(típus_név) kifejezés
kifejezés kétop kifejezés
kifejezés ? kifejezés : kifejezés
balérték értékadó_op kifejezés
kifejezés , kifejezés
elsQdleges_kifejezés:
azonosító
állandó
karakterlánc
( kifejezés )
elsQdleges_kifejezés ( kifejezés_listaopc)
elsQdleges_kifejezés [ kifejezés ]
balérték . azonosító
elsQdleges_kifejezés -> azonosító
balérték:
azonosító
elsQdleges_kifejezés [ kifejezés ]
balérték . azonosító
elsQdleges_kifejezés -> azonosító
*kifejezés
( balérték )

A

() [] . ->

elsQdleges kifejezés operátorok prioritása a legmagasabb, és az ilyen operátorok balról jobbra kötnek. Az egyoperandusú

* & - ! ~ ++ -- sizeof (típusnév)

operátorok prioritása az elsQdleges operátorokénál alacsonyabb, de magasabb az összes kétoperandusú operátorénál: ezek az operátorokjobbról balra kötnek. Az összes kétoperandusú operátor és a feltételes operátor balróljobbra köt, ezeket az alábbiakban csökkenQ prioritási sorrendben soroljuk fel:

kétop:
* / %
+ -
>> <<
< > <= >=
== !=
&
^
|
&&
||
? :

Az értékadó operátorok mindegyike azonos prioritású, és mindegyik jobbról balra köt.

értékadó_op:
= += -= *= /= %= >>= <<= &= ^= |=

A vesszQ operátor (,) prioritása a legalacsonyabb, és balról jobbra csoportosít.

18.2. Deklarációk

deklaráció:
dekl._specifikátorok deklarátorlistaopc;
dekl._specifikátorok:
típus_specifikátor dekl._specifikátorokopc
t.o._specifikátor dekl._specifikátorokopc
t.o._specifikátor:
auto
static
extern
register
typedef
típus_specifikátor:
char
short
int
long
unsigned
float
double
strukt._vagy_union_specifikátor
typedef_név
k.é._deklarátorlista:
k.é._deklarátor
k.é._deklarátor , k.é._deklarátorlista
k.é._deklarátor:
deklarátor inicializálóopc
deklarátor:
azonosító
( deklarátor )
*deklarátor
deklarátor ()
deklarátor [ állandó_kifejezésopc]
strukt._vagy_union_specifikátor:
struct { strukt._dekl._lista }
struct azonosító { strukt._dekl._lista }
struct azonosító
union { strukt._dekl._lista }
union azonosító {strukt._dekl._lista }
union azonosító
strukt._dekl._lista:
strukt._deklaráció
strukt._deklaráció strukt._dekl._lista
strukt._deklaráció:
típus_specifikátor strukt._deklarátor_lista;
strukt._deklarátor_lista:
strukt._deklarátor
strukt._deklarátor , strukt._deklarátor_lista
strukt._deklarátor:
deklarátor
deklarátor : állandó_kifejezés
: állandó_kifejezés
inicializáló:
= kifejezés
= { inicializáló_lista }
= { inicializáló_lista, }
inicializáló_lista:
kifejezés
inicializáló_lista , inicializáló_lista
{ inicializáló_lista }
típus_név:
típus_specifikátor absztrakt_deklarátor
absztrakt_deklarátor:
üres
( absztrakt_deklarátor )
*absztrakt_deklarátor
absztrakt_deklarátor ()
absztrakt_deklarátor [ állandó_kifejezésopc]
typedef_név:
azonosító

18.3. Utasítások

összetett_utasítás:
{ deklarációlistaopc utasításlistaopc }
deklarációlista:
deklaráció
deklaráció deklarációlista
utasításlista:
utasítás
utasítás utasításlista
utasítás:
összetett_utasítás
kifejezés;
if ( kifejezés )
utasítás
if ( kifejezés )
utasítás
else utasítás
while ( kifejezés )
utasítás
do
utasítás
while ( kifejezés ) ;
for (kifejezés_1opc; kifejezés_2opc; kifejezés_3opc)
utasítás
switch ( kifejezés )
utasítás
case állandó_kifejezés:
utasítás
default:
utasítás
break;
continue;
return;
return kifejezés;
goto azonosító;
azonosító:
utasítás
;

18.4. KülsQ definíciók

program:
külsQ_definíció
külsQ_definíció program
külsQ_definíció:
függvénydefiníció
adatdefiníció
függvénydefiníció:
dekl._specifikátoropc függvénydeklarátor függvénytörzs
függvénydeklarátor:
deklarátor ( paraméterlistaopc )
paraméterlista:
azonosító
azonosító , paraméterlista
függvénytörzs:
deklarációlista függvény_utasítás
függvény_utasítás:
deklarációlistaopc utasításlista
adatdefiníció:
externopc típus_specifikátoropc k.é._deklarátorlista;
staticopc típus_specifikátoropc k.é._deklarátorlista;

18.5. ElQfeldolgozó

#define azonosító szint._egységek_karakterlánca
#define azonosító(azonosító, ..., azonosító)
szint._egységek_karakterlánca
#undef azonosító
#include "állománynév"
#include <állománynév>
#if állandó_kifejezés
#ifdef azonosító
#ifndef azonosító
#else
#endif
#line állandó azonosító

1. fejezet: Alapismeretek

A C nyelv tanulását kezdjük az alapismeretek gyors elsajátításával. Célunk az, hogy mqködQképes programokon, de a részletekbe, formális szabályokba és kivételekbe való belebonyolódás nélkül mutassuk be a nyelv legfontosabb elemeit. Nem törekszünk tehát teljességre, sQt pontosságra sem (eltekintve attól, hogy a példáknak helyeseknek kell lenniük). Az olvasónak a lehetQ leggyorsabban el kell jutnia addig a pontig, ahol már használható programokat tud írni. Éppen ezért bevezetQnk az alapvetQ tudnivalókra koncentrál: a változókra, az állandókra, az aritmetikára, a vezérlésátadásra, a függvényekre, a be- és kivitellel kapcsolatos elemi ismeretekre. Tudatosan kihagytuk ebbQl a fejezetbQl a C nyelv olyan összetevQit, amelyek nagyobb programok írásánál létfontosságúak. Ilyenek a mutatók, a struktúrák, a C nyelv gazdag operátorkészletének legnagyobb része, néhány vezérlésátadó utasítás és még ezernyi részlet.
Ennek a megközelítésnek megvannak természetesen a maga hátrányai is. Ezek közül a legfontosabb, hogy a nyelv valamely összetevQjét leíró összes információ nem egy helyen található, és a bevezetQ fejezet rövidségénél fogva félrevezetQ lehet. Továbbá, mivel nem használható a C nyelv egész fegyvertára, a példák nem olyan tömörek és elegánsak, mint lehetnének. Ezeket a hátrányokat igyekeztünk a minimálisra csökkenteni, de minderrQl azért ne feledkezzünk meg.
További hátrány, hogy a bevezetQ egyes részei a késQbbiekben szükségszerqen megismétlQdnek. Reméljük, hogy ez az ismétlés inkább segíti, mintsem bosszantja az olvasót.
Tapasztalt programozók mindenesetre már ennek a fejezetnek az anyagából ki tudják következtetni a számukra szükséges programozási információt. KezdQknek ajánljuk, hogy maguk is írjanak az itt bemutatottakhoz hasonló, kisméretq programokat. A bevezetQ mindkét csoportnak keretként szolgálhat a késQbbi fejezetek anyagának befogadásához.

1.1. Indulás

Egy új programnyelv elsajátításának egyetlen módja, ha programokat írunk az adott nyelven. Az elsQ megírandó program minden nyelv tanulásakor hasonló szokott lenni : Nyomtassuk ki a

Figyelem, emberek!

szavakat.
Ez az elsQ akadály. Ahhoz, hogy átugorjuk, képesnek kell lennünk arra, hogy (valahol) létrehozzuk a programszöveget, sikeresen lefordítsuk, betöltsük, lefuttassuk, és ki kell találnunk, hová kerül a kivitt szöveg. Ha ezeken a_mechanikus részleteken túljutottunk, minden más viszonylag könnyen fog menni.
C nyelven a "Figyelem, emberek!" szöveget kinyomtató program a következQ:

main ()
{
printf ("Figyelem, emberek! \ n");
}

A program futtatásának módja az éppen használt rendszertQl függ. Az UNIX operációs rendszerben pl. a forrásprogramot olyan állomány alakjában kell létrehozni, amelynek a neve .c-re végzQdik, mint például figyel.c, majd ezt a

cc figyel.c

paranccsal le kell fordítani. Ha nem követtünk el semmilyen hibát, pl. nem hagytunk ki egy karaktert, vagy nem írtunk valamit hibásan, a fordítás rendben végbemegy és egy végrehajtható állomány keletkezik, amelynek neve a.out . Ha ezt az

a.out

paranccsal lefuttatjuk, akkor a

Figyelem, emberek!

szöveg jelenik meg a kimeneten. Más operációs rendszerekben a szabályok eltérQek, ilyen esetben forduljunk megfelelQ szakemberhez.

1.1. Gyakorlat. Futtassa le a fenti programot a saját rendszerén! Kísérletezzen a program egyes részeinek elhagyásával, hogy meglássa, milyen hibaüzenetek érkeznek!

Most pedig néhány megjegyzés magáról a programról. A C programok méretüktQl függetlenül egy vagy több függvényt tartalmaznak, amelyek meghatározzák az éppen elvégzendQ számítási mqveleteket. A C-beli függvények a FORTRAN függvényeihez vagy szubrutinjaihoz, ill. a PL/1 , a PASCAL stb. eljárásaihoz hasonlítanak. Példánkban a main ilyen függvény. A függvény neve általában tetszQleges lehet, de a main speciális név - programunk végrehajtása mindig a main elején kezdQdik. EbbQl az következik, hogy minden programban elQ kell hogy forduljon egy main valahol. A main a feladat végrehajtása érdekében általában más függvényeket fog meghívni, amelyek közül egyesek ugyanabban a programban szerepelnek, míg mások elQzQleg megírt függvénykönyvtárakból származnak.
A függvények közötti adatátadás egyik módja az argumentumok használata. A függvénynevet követQ zárójelek az argumentumlistát határolják: jelen esetben main argumentum nélküli függvény, amit () jelöl. A { } kapcsos zárójelek a függvényt alkotó utasításokat zárják közre; szerepük a PL/1beli do-end-del, az ALGOL és PASCAL begin-end-jével azonos. A függvény hívása a függvény megnevezésével történik, amit az argumentumok zárójelezett listája követ. A FORTRAN-tól vagy PL/1-tQl eltérQen itt nincs call utasítás. A zárójeleknek akkor is szerepelniük kell, ha egyetlen argumentum sincs. A

printf ("Figyelem, emberek!\ n");

sor nem más, mint függvényhívás, amely a printf nevq függvényt hívja a "Figyelem, emberek!\ n" argumentummal. printf könyvtári függvény, amely az "eredményt" - esetünkben az argumentumát alkotó karakterláncot - (egyéb periféria megadása híján) a terminálra írja.
Egy tetszQleges számú karakterbQl álló, idézQjelek (") közé zárt karaktersorozatot karakterláncnak, karakter-állandónak (stringnek, ill. stringkonstansnak) nevezünk. Pillanatnyilag a karakterláncokat csak a printf és más függvények argumentumaiként használjuk.
A karakterláncban elQforduló \ n karaktersorozat az újsor karakter C-beli jelölésmódja. Hatására a kiírás a következQ sor bal szélén folytatódik. Ha a \ n-et elhagyjuk (érdemes megkísérelni), azt tapasztaljuk, hogy a kiírás végén a kocsi vissza -soremelés elmarad. Az újsor karaktert csakis egy \n segítségével iktathatjuk be a printf-be: ha valami olyasmivel próbálkozunk, mint

printf ("Figyelem, emberek!
");

akkor a C fordító barátságtalan üzeneteket fog küldeni bizonyos hiányzó idézQjelekrQl.
A printf sohasem helyez el újsor karaktert automatikusan, így többszöri hívás segítségével egyetlen kimeneti sort fokozatosan rakhatunk össze. ElsQ programunkat így is írhattuk volna:

main ()
{
printf ("Figyelem, ");
printf ("emberek!");
printf ("\ n");
}

miáltal a korábbival azonos kimenetet kaptunk volna.
Megjegyezzük, hogy \ n egyetlen karaktert jelent. A \ n-hez hasonló, ún. escape jelsorozatok általánosan használható és bQvíthetQ mechanizmust alkotnak nehezen elQállítható vagy láthatatlan karakterek jelölésére. A C nyelvben ilyenek még a \t a tabulátor, a \b a visszaléptetés (backspace), a \" az idézQjel és a \\ magának a fordított törtvonalnak (backslash) a jelölésére.

1.2. Gyakorlat. Próbálja ki, mi történik, ha a printf argumentum-karakterlánca \ x-et tartalmaz, ahol x egy, a fenti listában nem szereplQ karakter!

1.2. Változók és aritmetika

Az alábbi program a következQ Fahrenheit-hQmérsékleteket és a megfelelQ Celsius-értékeket tartalmazó táblázatot nyomtatja ki a

C = (5 / 9) * (F - 32)

képlet alkalmazásával.

0 -17.8
20 -6.7
40 4.4
60 15.6
... ...
260 126.7
280 137.8
300 148.9

îme maga a program:

/* Fahrenheit-Celsius táblázat kinyomtatása
f = 0, 20, . . ., 300 értékekre */
main ()
{
int lower, upper, step;
float fahr, celsius;
lower = 0; /* A hQmérséklet-táblázat alsó határa */
upper = 300; /* felsQ határ */
step = 20; /* lépésköz */
fahr = lower;
while (fahr <= upper) {
celsius = (5.0 / 9.0) * (fahr - 32.0);
printf ("%4.0f %6.1f \n", fahr, celsius);
fahr = fahr + step;
}
}

Az elsQ két sor:

/* Fahrenheit-Celsius táblázat kinyomtatása
f = 0, 20, . . ., 300 értékekre */

egy megjegyzés (comment), amely esetünkben röviden elmondja, hogy mit csinál a program. A fordító minden, a /* és */ között elQforduló karaktert figyelmen kívül hagy; így ide tetszQleges, a program megértését segítQ szöveget beírhatunk. Megjegyzések mindenütt elQfordulhatnak, ahol szóköz vagy újsor elQfordulhat.
A C nyelvben használat elQtt minden változót deklarálni kell, általában a függvény elején, az elsQ végrehajtható utasítás elQtt. Ha errQl megfeledkezünk, hibaüzenetet kapunk a fordítótól. A deklaráció egy típus megadásából és az illetQ típusú változók felsorolásából áll. Példa erre az elQbbi program két sora:

int lower, upper, step;
float fahr, celsius;

Az int típus azt jelenti, hogy a felsorolt változók egész (integer) típusúak. float jelöli a lebegQpontos (floating point) változókat, vagyis az olyan számokat, amelyeknek tört részük is van. Mind az int, mind a float számok pontossága az adott számítógéptQl függ. A PDP-11 -en például az int 16 bit-es elQjeles szám, vagyis olyan szám, amelynek értéke -32768 és +32767 között van. A float szám 32 bites mennyiség, ami körülbelül 7 értékes számjegyet jelent, 10-38 és 1038 közötti nagyságrendben. A 2. fejezet más gépekre is közli a számábrázolási tartományokat.
Az int és float mellett a C nyelvben más alapvetQ adattípusok is vannak:

char - karakter egyetlen byte,
short - rövid egész,
long - hosszú egész,
double - duplapontosságú lebegQpontos szám.

Ezen objektumok méretei ugyancsak gépfüggQek, a részleteket a 2. fejezet tartalmazza. EzekbQl az alapvetQ típusokból tömbök, struktúrák és unionok képezhetQk, mutatók mutathatnak rájuk, függvények térhetnek vissza a hívóhoz ezekkel a típusokkal: mindezekkel rövidesen találkozunk.
A hQmérséklet-átszámító programban a tényleges számítás a

lower = 0;
upper = 300;
step = 20;
fahr = lower;

értékadó utasításokkal kezdQdik, amelyek a változók kezdeti értékét állítják be. Az egyes utasításokat pontosvesszQ zárja le.
A táblázat minden sorát azonos módon kell kiszámítani, ezért egy ciklust használunk, amely táblázatsoronként egyszer ismétlQdik; ez a célja a while utasításnak :

while (fahr <= upper) {
...
}

Programfutás közben a gép megvizsgálja, teljesül-e a zárójelek közötti feltétel. Ha az értéke igaz (fahr kisebb vagy egyenlQ, mint upper), akkor végrehajtja a ciklustörzs (a { és } kapcsos zárójelek közé zárt) utasításait. Ezután ismét megvizsgálja a feltételt, és ha az értéke igaz, újra végrehajtja a törzset. Ha a vizsgálat a hamis logikai értéket szolgáltatja (fahr meghaladja upper-t), akkor a ciklus lezárul és a végrehajtás a ciklust követQ elsQ utasításon folytatódik. Az adott program nem tartalmaz több utasítást, tehát a program véget ér.
A while törzse egy vagy több, kapcsos zárójelek közé zárt utasítás lehet, mint a hQmérséklet-átszámító programban, vagy egyetlen, kapcsos zárójel nélküli utasítás, mint pl.:

while (i < j)
i = 2 * i;

A while által vezérelt utasításokat mindkét esetben két pozícióval beljebb írtuk, hogy elsQ pillantásra világos legyen, mely utasítások helyezkednek el a cikluson belül. A bekezdés a program logikai szerkezetét hangsúlyozza. Bár a C nyelv meglehetQsen kötetlen az utasítások pozícionálását illetQen, ha azt akarjuk, hogy programunk könnyen olvasható legyen, nagyon fontos a megfelelQ bekezdések és üres helyek használata. Célszerq, ha egy sor egy utasítást tartalmaz, és (általában) hagyjunk egy-egy szóközt az operátorok elQtt és után. A zárójelek pozíciója kevésbé lényeges: e tekintetben a többféle divatos stílus egyikét választottuk. Az olvasó bármilyen neki megfelelQ stílus mellett dönthet, célszerq azonban, ha ezt azután következetesen használja.
A munka nagyja a ciklus törzsében készül el. A

celsius = (5.0 / 9.0) * (fahr - 32.0);

utasítással kiszámítjuk a Celsius-fokokban kifejezett hQmérsékletet, és értékét hozzárendeljük a celsius változóhoz. Az ok, ami miatt 5.0 / 9.0-át használtunk, 5 / 9 helyett az, hogy a C nyelv csakúgy, mint sok más nyelv, az egész számokkal végzett osztásnál az eredmény tört részét elhagyja. Tehát 5 / 9 értéke 0, és 0 lenne az összes hQmérséklet is. Az állandón belüli tizedespont jelzi, hogy az illetQ állandó lebegQpontos, így 5.0 / 9.0 értéke 0.555..., amire szükségünk van.
Ugyancsak 32.0-át írtunk 32 helyett, noha mivel a fahr változó float 32 automatikusan float-tá (32.0-vá) alakulnak át a kivonás elQtt. Bölcsebb azonban azt a stílust követni, hogy a lebegQpontos állandókat tizedesponttal írjuk akkor is, ha az értékük egész : ezzel az olvasó számára hangsúlyozzuk ezek lebegQpontos természetét, és biztosítjuk, hogy a fordító is eszerintkezelje Qket.
A 2. fejezet részletesen tartalmazza annak szabályait, hogy az egész számok mikor alakulnak át lebegQpontossá. EgyelQre csak annyit jegyzünk meg, hogy a

fahr = lower;

értékadó utasítás és a

while (fahr <= upper)

vizsgálat egyaránt a várt módon mqködik (az int a mqvelet elvégzése elQtt float-tá alakul át).
Ez a példa a printf mqködésébQl is valamivel többet mutat meg. A printf általános célú formátumkonvertáló függvény, amelyet teljes egészében majd a 7. fejezetben ismertetünk. ElsQ argumentuma a kinyomtatandó karakterlánc, ahol az egyes %-jelek mutatják, hogy hová kell a további (második, harmadik, . . .) argumentumokat behelyettesíteni és milyen formátumban kell azokat kinyomtatni. Például a

printf ("%4.0f %6.1f \n", fahr, celsius);

utasításban a %4.0f konverzió-elQírás szerint a lebegQpontos számot egy legalább négy karakter széles helyre kell beírni úgy, hogy a tizedespontot nem követik számjegyek. %6.1f egy másik számot ír le, amely legalább 6 szóközt foglal el és a tizedespont után 1 számjegyet tartalmaz - hasonlóan a FORTRAN-beli F6.1 vagy a PL/1-beli F(6,1) alakhoz. A specifikáció egyes részei elhagyhatók:%6f azt írja elQ, hogy a szám legalább 6 karakter széles; %.2f legalább két helyet igényel a tizedespont után, de a szélességet nem korlátozza, és %f egyszerqen azt mondja, hogy a számot lebegQpontosként kell kinyomtatni. Hozzátehetjük, hogy a printf a %d-t decimálisként, %o-t oktálisként, %x-et hexadecimálisként, %c-t karakterként, %s-et karakterláncként és %%-ot %-ként értelmezi.
A printf elsQ argumentumában található minden egyes % konstrukcióhoz hozzárendelQdik a neki megfelelQ második, harmadik stb. argumentum; ezeknek szám szerint és típus szerint is meg kell egyezniük, ellenkezQ esetben értelmetlen válaszokat kapunk.
Egyébként a printf nem része a C nyelvnek: a C nyelven belül a be- és kivitel nincs definiálva. A printf-ben nincs semmi rendkívüli: csupán egy hasznos függvény, amely része a C programok által közönségesen elérhetQ szabványos rutin-könyvtárnak. Azért, hogy magára a C nyelvre koncentrálhassunk, nem foglalkozunk túl sokat a be- és kivitellel, egészen a 7. fejezetig. ElsQsorban a formátumozott adatbevitel kérdését halasztjuk el addig. Ha számokat kell beolvasnunk, olvassuk el a 7. fejezet 7.4. szakaszának a scanf függvényrQl szóló részét. A scanf - az adatmozgás irányától eltekintve - nagyon hasonló a printf függvényhez.

1.3. Gyakorlat. Módosítsuk úgy a hQmérséklet-átszámító programot, hogy az a táblázat fölé fejlécet is nyomtasson!

1.4. Gyakorlat. Irjunk programot a korábbi példának megfelelQ Celsius-Fahrenheit táblázat kinyomtatására!

1.3. A for utasítás

Az olvasó is nyilván tudja, hogy egy programot sokféleképpen meg lehet írni: próbálkozzunk meg a hQmérséklet-átszámító program egy másik változatával :

main ()
/ * Fahrenheit-Celsius táblázat*/
{
int fahr;
for (fahr = 0; fahr <= 300; fahr = fahr + 20)
printf ("%4d %6.1f \n", fahr, (5.0 / 9.0) * (fahr - 32));
}

Ez ugyanazokat az eredményeket adja, de láthatóan másképp néz ki. Az egyik fQ eltérés, hogy a legtöbb változó szükségtelenné vált: csak a fahr maradt meg int változóként (azért, hogy mutassa a printf-ben a %d konverziót). Az alsó és a felsQ határ, valamint a lépésköz csak állandóként jelenik meg a for utasításban, amely maga is új számunkra. A Celsius-hQmérsékletet számító kifejezés most nem külön értékadó utasításként, hanem mint a printf harmadik argumentuma szerepel.
Ez az utóbbi módosítás egy egészen általános C-beli szabályon alapul, amely kimondja, hogy minden olyan összefüggésben, ahol valamely adott típusú változó értékét használhatjuk, ugyanolyan típusú kifejezést is használhatunk. Minthogy a printf harmadik argumentumának a %6.1f-re való illeszkedés érdekében lebegQpontosnak kell lennie, tetszQleges lebegQpontos kifejezés is elQfordulhat ezen a helyen.
Maga a for egy ciklusutasítás, a while általánosítása. Ha az elQbbi while-lal összehasonlítjuk, mqködése rögtön világossá válik. Három részt tartalmaz, amelyeket pontosvesszQk választanak el. Az elsQ rész, vagyis

fahr = 0

egyszer hajtódik végre a ciklusba való belépés elQtt. A második rész a ciklust vezérlQ ellenQrzés vagy feltétel:

fahr <= 300

A gép megvizsgálja a feltételt; ha igaz, akkor végrehajtja a ciklus törzsét (itt egyetlen printf), amit az újrainicializáló lépés, azaz

fahr = fahr + 20

és újabb feltételvizsgálat követ. A ciklus akkor ér véget, amikor a feltétel hamissá válik. Csakúgy, mint a while esetében, a törzs vagy egyetlen utasítás, vagy pedig kapcsos zárójelek közé zárt utasítások csoportja. Az inicializáló és újrainicializáló rész egy-egy tetszQleges kifejezés lehet.
A while és a for között szabadon választhatunk aszerint, hogy mi tqnik világosabbnak. A for alkalmazása általában olyan ciklusok esetében célszerq, amelyekben az inicializálás és újrainicializálás egy-egy logikailag összefüggQ utasítás, mivel a for sokkal tömörebb, mint a while és egy helyen tartja a ciklusvezérlQ utasításokat.

1.5. Gyakorlat. Módosítsuk úgy a hQmérséklet-átszámító programot, hogy az a táblázatot fordított sorrendben, tehát 300 foktól 0 fokig nyomtassa ki!

1.4. Szimbolikus állandók

Még egy megjegyzés, mielQtt elbúcsúznánk a hQmérséklet-átszámítástól. Nem jó gyakorlat, ha "bqvös számokat", például 300-at vagy 20-at építünk be a programba: ezek nem sokat mondanak annak, aki késQbb olvassa majd a programot, és megváltoztatásuk is nagyon nehéz. Szerencsére a C nyelv lehetQséget ad az ilyen bqvös számok elhagyására. A #define szerkezet segítségével a program elején szimbolikus nevet vagy szimbolikus állandót rendelhetünk egy-egy megadott karakterlánchoz. Ezekután a fordító a név mindennem idézQjelezett elQfordulását a megfelelQ karakterlánccal helyettesíti. A név nemcsak számot, hanem tetszQleges szöveget is helyettesíthet, pl. :

#define LOWER 0 /* A táblázat alsó határa*/
#define UPPER 300 / * A táblázat felsQ határa*/
#define STEP 20 /* Lépésnagyság*/
main ()
/*Fahrenheit-Celsius táblázat*/
{
int fahr;
for (fahr = LOWER; fahr <= UPPER; fahr = fahr + STEP)
printf("%4d %6.1f\n", fahr, (5.0/9.0) * (fahr-32));
}

A LOWER, UPPER és STEP mennyiségek állandók, így deklarációban nem jelennek meg. A szimbolikus neveket nagybetqkkel szokás írni, így azonnal megkülönböztethetQk a kisbetqs változónevektQl. Ügyeljünk arra, hogy a definíciók után nincs pontosvesszQ! Mivel a nevet követQ teljes sor behelyettesítQdik, a for-ban túl sok pontosvesszQ lenne.

1.5. Néhány hasznos program

A következQkben áttekintünk néhány egymással összefüggQ programot, amelyek karakteradatokon végeznek egyszerq mqveleteket. Ki fog derülni, hogy sok program csupán az itt közölt prototípusok bQvített változata.

Karakterek be- és kivitele
A szabványos könyvtárban rendelkezésre állnak olyan függvények, amelyekkel egyszerre egy karaktert lehet írni vagy olvasni. A getchar() minden egyes hívásakor beolvassa a következQ bemeneti karaktert és a visszatérési értéke ez a karakter lesz. Tehát

c = getchar()

után a c változó a következQ bemeneti karaktert tartalmazza. A karakterek közönséges esetben a terminálról érkeznek, de ezzel a 7. fejezetig nem kell törQdnünk. A putchar(c) függvény a getchar ellentéte:

putchar(c)

a c változó tartalmát valamilyen kimeneti perifériára írja ki, ami általában ismét a terminál. A putchar és a printf hívásai keverhetQk: a kivitt karakterek a hívás sorrendjében fognak megjelenni.
Hasonlóan a printf-hez, a getchar és putchar függvényekben sincs semmi rendkívüli. Ezek nem részei a C nyelvnek, de mindenütt rendelkezésre állnak. 9llománymásolás getchar és putchar birtokában meglepQen sok hasznos programot írhatunk anélkül, hogy ezen kívül bármi egyebet tudnánk a be- és kivitelrQl. A legegyszerqbb példa az a program, amely a bemenetet karakterenként a kimenetre másolja. A program váza:

egy karakter beolvasása
while (a beolvasott karakter nem az állomány vége jel)
az éppen beolvasott karakter kimenetre írása
egy új karakter beolvasása

Mindezt C nyelven kifejezve:

main()
/* A bemenet átmásolása a kimenetre. 1. változat*/
{
int c ;
c = getchar();
while (c != EOF) {
putchar(c);
c = getchar();
}
}

A != relációs operátor jelentése : "nem egyenlQ ".
A fQ probléma a bemenet végének az érzékelése. Megállapodás szerint a getchar az állomány végének megtalálásakor olyan értékkel tér vissza, amely nem valamely érvényes karakter kódja: íly módon a program észlelni tudja, hogy mikor fogytak el a bemeneten a karakterek. Az egyetlen probléma - ami azonban igen bosszantó -, hogy kétféle megállapodás is közforgalomban van arra nézve, hogy valójában mi az állomány vége érték. Ezt a problémát egyelQre azzal kerültük ki, hogy a számszerq érték helyett az EOF szimbolikus nevet használtuk, függetlenül a tényleges értéktQl. A gyakorlatban EOF vagy -1 , vagy 0, a programot ennek megfelelQen vagy

#define EOF -1

vagy

#define EOF 0

kell, hogy megelQzze ahhoz, hogy helyes mqködést kapjunk. Azáltal, hogy az EOF szimbolikus állandót használtuk annak az értéknek a jelölésére, amit állomány vége esetén a getchar visszaad, elértük, hogy az egész programban csupán egyetlen dolog függ az állomány vége tényleges értékétQl.
A c változót int-nek és nem char-nak deklaráltuk, így abban tárolható a getchar által visszaadott érték. Mint azt a 2. fejezetben látni fogjuk, ez a változó azért int típusú, mert alkalmasnak kell lennie arra, hogy az összes lehetséges karakteren kívül az EOF értéket is felvegye.
Gyakorlott C programozók a másolóprogramot tömörebben írnák le. A C nyelvben az olyan értékadások, mint

c = getchar()

kifejezésekben is használhatók; a kifejezés értéke egyszerqen a bal oldalhoz hozzárendelt érték. Ha a c-re vonatkozó értékadás egy while feltételvizsgáló részének belsejébe kerül, akkor az állománymásoló program a következQképpen írható :

main()
/* A bemenet átmásolása a kimenetre - 2. változat*/
{
int c;
while ((c = getchar()) != EOF)
putchar (c);
}

A program beolvas egy karaktert, hozzárendeli c-hez, majd ellenQrzi, hogy a karakter azonos-e az állomány vége jellel. Ha nem, akkor a programfutás a while törzsének végrehajtásával, azaz a karakter kinyomtatásával folytatódik. Ezután a while ciklus ismétlQdik. Ha a program végül eléri a bemeneti karaktersorozat végét, akkor a while és vele együtt a main is befejezQdik.
Ez a változat egy helyre vonja össze a beolvasást - most csak egy getchar hívás van -, és egyben le is rövidíti a programot. Az értékadás behelyezése a feltételvizsgálatba az egyik olyan eset, amikor a C nyelv hasznos tömörítést tesz lehetQvé. (Megvan persze a lehetQsége annak, hogy ezt túlzásba vigyük és áttekinthetetlen programkódot hozzunk létre, de ezt igyekszünk elkerülni.)
Lényeges látnunk, hogy az értékadás körüli zárójelek a feltételen belül tényleg szükségesek. A != precedenciája magasabb, mint az = szimbólumé ami azt jelenti, hogy zárójelek hiányában a != relációvizsgálat megelQzné az = értékadási mqvelet végrehajtását. Igy a

c = getchar() != EOF

utasítás egyenértékq a

c = (getchar() != EOF)

utasítással. Ez azzal a nemkívánatos eredménnyel jár, hogy c 0 vagy 1 lesz, attól függQen, hogy a getchar hívásakor állomány vége jel érkezett-e vagy sem. (ErrQl részletesebben a 2. fejezetben szólunk.)

Karakterszámlálás
A következQ program, amelyet a másolóprogram kis módosításával kaptunk, megszámlálja a beolvasott karaktereket:

main()
/* Megszámlálja a bemeneten érkezQ karaktereket*/
{
long nc;
nc = 0;
while (getchar () != EOF)
++nc;
printf("%ld\n", nc);
}

A

++nc;

utasítás egy új operátort mutat be, amelynek jele ++, és a jelentése: inkrementálj eggyel. Irhatnánk azt is, hogy nc = nc + 1, de ++nc tömörebb és gyakran hatékonyabb is. Létezik egy ennek megfelelQ -- operátor, amely 1-gyel dekrementál. A ++ és a -- egyaránt lehet prefix (elQtag) operátor (++nc) vagy postfix (utótag) operátor (nc++)- e két alakhoz kifejezésekben különbözQ értékek tartoznak, amint azt a 2. fejezetben látni fogjuk, de ++nc és nc++ egyaránt inkrementálja nc-t. EgyelQre megmaradunk a prefix operátornál.
A karakterszámláló program a karakterek számát int helyett egy long típusú változóban tárolja. A PDP-11-en egy int mennyiség maximális értéke 32767, így a számláló viszonylag kevés bemenQ érték esetén is túlcsordulna, ha int-nek deklarálnánk. A Honeywell és IBM C-ben a long és az int ugyanaz, de a maximális érték sokkal nagyobb. A %ld konverziómegadás azt jelzi printf-nek, hogy a megfelelQ argumentum egy hosszú egész (long integer).
Ennél is nagyobb számok esetén a double típus (duplahosszúságú lebegQpontos szám) használható. A while helyett for utasítást fogunk használni,hogy bemutathassuk a ciklusszervezés egy másik lehetQségét.

main()
/*Megszámlálja a bemeneten érkezQ karaktereket*/
{
double nc;
for (nc = 0; getchar() != EOF; ++nc)
;
printf ("%.0f \n", nc);
}

A printf mind float, mind double esetén %f-et használ; a %.0f elnyomja a nemlétezQ tört rész kiírását.
A for ciklus törzse üres, mivel az egész feladat a feltételvizsgáló és újrainicializáló részben hajtódik végre. A C nyelvtani szabályai azonban megkívánják, hogy a for utasításnak legyen törzse. Az egymagában álló pontosvesszQ, vagyis a nulla (üres)utasítás e követelmény kielégítése miatt szerepel. Külön sorba írtuk, hogy feltqnQbb legyen.
MielQtt befejeznénk a karakterszámláló program elemzését, felhívjuk a figyelmet, hogy ha a bemeneten nincsenek karakterek, akkor getchar legelsQ hívásakor a while vagy a for feltételvizsgálata hamis értéket eredményez, és így a program eredménye elvárásunknak megfelelQen 0 lesz. Ez lényeges megfigyelés. A while és a for egyik elQnyös tulajdonsága, hogy a feltételvizsgálat a ciklus fejében van, megelQzi a törzset. Ha tehát semmit sem kell csinálni, akkor tényleg semmi sem történik, még akkor sem, ha emiatt a program sohasem halad át a ciklus törzsén. A programoknak akkor is értelmesen kell mqködniük, ha a bemenet "üres". A while és a for segítségével a programok határesetekben is ésszerqen viselkednek.

Sorok számlálása
A következQ program megszámlálja a bemenetére érkezQ sorokat. Feltételezzük, hogy a bemenQ sorok a \n újsor karakterrel fejezQdnek be, amely szigorúan minden kiírt sor végén megjelenik.

main()
/*A bemenetre érkezQ sorok számlálása*/
{
int c, nl ;
nl = 0;
while ((c = getchar()) != EOF)
if (c == '\n')
++nl;
printf ("%d\n", nl );
}

A while törzse most egy if-et tartalmaz, amely pedig a ++ nl inkrementáló mqveletet vezérli. Az if utasítás elvégzi a zárójelezett feltétel vizsgálatát, ha ennek eredménye igaz, akkor végrehajtja a rákövetkezQ utasítást (vagy kapcsos zárójelek közötti utasításcsoportot). A sorokat ismét úgy rendeztük el, hogy világos legyen, mit mi vezérel.
A C nyelv jelölésmódjában az == (kettQs egyenlQségjel) jelentése: egyenlQ . . .-vel (hasonlóan a FORTRAN-beli .EO.-hoz). Ezzel a szimbólummal különböztetjük meg az egyenlQség vizsgálatát a szimpla = jeltQl, amit értékadásra használunk. Minthogy tipikus C programokban az értékadás körülbelül kétszer olyan gyakran fordul elQ, mint az egyenlQségvizsgálat, ésszerq, hogy az értékadó operátor fele olyan hosszú legyen.
Bármely egymagában álló karakter aposztrófok közé írva az illetQ karakternek a gép karakterkészletében szereplQ numerikus értékét jelenti: ezt karakterállandónak nevezzük. Igy például 'A' karakterállandó; az ASCII karakterkészletben ennek értéke 65, vagyis az A karakter belsQ ábrázolása. Természetesen kényelmesebb 'A'-t írni, mint 65-öt: 'A' jelentése világos és független az adott karakterkészlettQl.
A karakterállandókban a karakterláncokban használt escape jelsorozatok is megengedettek, így a feltételvizsgálatokban és aritmetikai kifejezésekben '\n' az újsor karakter kódértékét jelenti. Ne feledjük, hogy '\n' egyetlen karakter, amely kifejezésekben egy egész számmal egyenértékq, \n viszont karakterlánc, amely az adott esetben egyetlen karaktert tartalmaz! A karakterek és karakterláncok témáját a 2. fejezetben folytatjuk.

1.6. Gyakorlat. îrjunk olyan programot, amely megszámlálja a szóközöket, tab és újsor karaktereket!

1.7. Gyakorlat. îrjunk olyan programot, amely a bemenetet átmásolja a kimenetre, miközben az egy vagy több szóközbQl álló karakterláncokat egyetlen szóközzel helyettesíti!

1.8. Gyakorlat. îrjunk olyan programot, amely minden egyes tab karaktert a > , visszaléptetés (backspace), - háromkarakteres sorozattal helyettesít, ami áthúzott > -ként fog megjelenni, továbbá, amely a visszaléptetés karaktereket a hasonlóan áthúzott < szimbólummal helyettesíti! Ezáltal a tab karakterek és visszaléptetések láthatóvá válnak.

Szavak számlálása
Negyedik hasznos programunk sorokat, szavakat és karaktereket számlál annak a laza definíciónak az alapján, amely szónak tekint minden olyan karaktersorozatot, amely nem tartalmaz szóközt, tab vagy újsor karaktert. (Az alábbi program az UNIX wc segédprogramjának a váza.)

#define YES 1
#define NO 0
main ()
/*A bemenet sorainak, szavainak, karaktereinek számlálása*/
{
int c, nl, nw, nc, inword;
inword = NO;
nl = nw = nc = 0;
while ((c = getchar()) != EOF) {
++nc;
if (c == '\n')
++nl;
if (c == ' ' ||c == '\n' ||c == '\t')
inword = NO;
else if (inword == NO) {
inword = YES;
++nw;
}
}
printf ("%d %d %d\n", nl, nw, nc);
}

Ahányszor a program egy szó elsQ karakterével találkozik, növeli a számlálót. Az inword változó jelzi, hogy a program pillanatnyilag egy szón belül van-e vagy sem; kezdetben nincs szón belül, miáltal a NO érték rendelQdik hozzá. ElQnyben részesítjük a YES és NO szimbolikus állandókat az 1 és 0 számértékekkel szemben, mivel olvashatóbbá teszik a programot. Természetesen egy ilyen kis programban, mint ez, ennek nemigen van jelentQsége, de nagyobb programokban az érthetQség javulása sokszorosan megéri azt a szerény plusz fáradságot, ami, az ilyen stílusú programíráshoz szükséges. Módosítani is könnyebb az olyan programot, ahol a számok csupán szimbolikus állandóként jelennek meg. Az

nl =nw=nc=0;

sor mindhárom változót kinullázza. Ez nem speciális eset, hanem annak a ténynek a következménye, hogy az értékadások jobbról balra mennek végbe. Ez valójában ugyanaz, mintha azt írtuk volna, hogy

nc = (nl = (nw = 0));

A || operátor jelentése VAGY, tehát az

if (c == ' ' ||c == '\n' ||c == '\t')

sor azt jelenti, hogy ha "c szóköz vagy c újsor vagy c tab karakter . . . ". (Mint mondottuk, a \t escape szekvencia a tab karakter látható megjelenési formája.) Létezik az ennek megfelelQ && operátor is az ÉS kifejezésére. Az && vagy || operátorokkal összekapcsolt kifejezések kiértékelése balról jobbra történik, és a kiértékelés rögtön abbamarad, amint az egész kifejezés igaz vagy hamis volta nyilvánvalóvá válik. Ha tehát c szóköz karakter, nincs szükség annak megállapítására, hogy c újsort vagy tabot tartalmaz-e, tehát ezek a vizsgálatok nem mennek végbe. Itt most ez nem különösen lényeges, de bonyolultabb esetekben nagyonis fontos lehet, amint azt nemsokára látni fogjuk.
A példában a C nyelv else utasítása is szerepel, amely megadja azt az alternatív tevékenységet, amit akkor kell elvégezni, ha az if feltételrésze hamis értékq. 9ltalános alakja:

if (kifejezés)
1.utasítás
else
2.utasítás

Az if-else-hez tartozó két utasítás közül egy és csakis egy, mégpedig ha a kifejezés értéke igaz, akkor az 1. utasítás, ha hamis, akkor a 2. utasítás hajtódik végre. Mindkét utasítás valójában egészen bonyolult is lehet. A szavakat számláló programban pl. az else utáni utasítás egy újabb if, amely a kapcsos zárójelek közötti két utasítást vezérli.

1.9. Gyakorlat. Hogyan ellenQrizhetjük a szavakat számláló programot? Mik lehetnek szóhatárok.

1.10. Gyakorlat. Irjunk olyan programot, amely külön-külön sorokban nyomtatja ki a bemenetére érkezQ szavakat!

1.11. Gyakorlat. Módosítsuk a szavakat számláló programot úgy, hogy jobban definiáljuk a szó fogalmát, például a szó legyen betqk, számjegyek és aposztrófok olyan sorozata, amely betqvel kezdQdik!

1.6. Tömbök

îrjunk olyan programot, amely megszámlálja, hogy hányszor fordulnak elQ az egyes számjegyek, hány üres helyet adó karakter (szóköz, tab, újsor) és hány egyéb karakter van a beolvasott állományban! Ez a feladat nyilván mesterkélt, de lehetQvé teszi, hogy egyetlen programban szemléltessük a C több jellegzetességét.
Tizenkétféle bemeneti karaktert kell megkülönböztetnünk, így érdemes az egyes számjegyek elQfordulásainak számát egy tömbben nyilvántartani ahelyett, hogy tíz külön változónk lenne. A program egyik lehetséges változata:

main()
/*Számjegyek, üres helyek és egyéb karakterek számlálása*/
{
int c, i, nwhite, nother;
int ndigit [10];
nwhite = nother = 0;
for (i = 0; i < 10; ++i)
ndigit [i] = 0;
while ((c = getchar()) != EOF)
if (c >= '0' && c <= '9')
++ ndigit [c - '0'];
else if (c == ' ' || c == '\n' || c == '\t')
++ nwhite;
else
++nother;
printf ("számjegyek=");
for (i = 0; i < 10; ++i)
printf ("%d", ndigit [i]);
printf ("\n üres hely = %d, egyéb = %d\n", nwhite, nother);
}

Az

int ndigit [10];

deklaráció azt fejezi ki, hogy az ndigit egy 10 egészbQl álló tömb. A tömbindexek a C nyelvben mindig 0-val kezdQdnek (és nem 1-gyel, mint a FORTRAN-ban és a PL/1-ben), így a tömb elemei: ndigit[0], ndigit [1], . . ., ndigit[9]. Ezt tükrözi a két for ciklus: az egyik inicializálja, a másik kiíratja a tömböt.
Az index tetszQleges egész típusú kifejezés. îgy természetesen lehet az index egész típusú változó, mint pl. i, valamint egész értékq állandó is.
Az adott program lényeges módon kihasználja a számjegyek karakterábrázolásának tulajdonságait. Például az

if (c >= '0' && c <= '9')
. . .

vizsgálat eldönti, hogy a c-ben levQ karakter számjegy-e. Ha az, akkor az illetQ számjegy numerikus értéke

c - '0'

Ez a módszer csak akkor alkalmazható, ha '0', '1', . . . növekedQ sorrendq pozitív számok és '0' és '9' között csak számjegyek vannak. Szerencsére ez minden szokásos karakterkészlet esetében így van.
A char-okat és int-eket tartalmazó kifejezésekben definíció szerint kiértékelés elQtt minden int-té konvertálódik, így a char változók és állandók aritmetikai szempontból lényegében az int mennyiségekkel azonosak. Ez egészen természetes és kényelmes megoldás: például c - '0' egész típusú kifejezés, amelynek értéke 0 és 9 között van a c-ben tárolt '0' és '9' közötti karaktereknek megfelelQen, és így érvényes indexe az ndigit tömbnek.
Annak eldöntése, hogy a karakter számjegy, üres hely vagy valami más, az

if (c >= '0' && c <= '9')
++ndigit [c - '0'];
else if (c == ' ' || c == '\n' || c == '\t')
++nwhite;
else
++nother;

programrész segítségével történik. Az

if (feltétel)
utasítás
else if (feltétel)_
utasítás
else
utasítás

programszerkezetet gyakran alkalmazzák többutas elágazások leírására. A programszöveg beolvasása felülrQl kezdve mindaddig folytatódik, amíg a gép igaz feltételt nem talál. Ekkor végrehajtja az odatartozó utasítás részt, és az egész mqvelet végetér. (Az utasítás természetesen több, kapcsos zárójelek közé zárt utasítás is lehet.) Ha egyik feltétel sem igaz, a gép az utolsó else utáni utasítást hajtja végre, amennyiben van ilyen. Ha az utolsó else és a hozzátartozó utasítás hiányzik (mint a szavakat számláló programban), semmi sem történik. A kezdQ if, valamint a záró else között tetszQleges számú

else if (feltétel)
utasítás

csoport fordulhat elQ. Stiláris szempontból célszerq a bemutatott módon megszerkeszteni ezt a programrészt, hogy a hosszú döntési láncok ne nyúljanak túl a papír jobb szélén.
A 3. fejezetben fogunk szólni a switch utasításról, amely szintén többutas programelágaztatások leírására ad lehetQséget. A switch alkalmazása különösen akkor elQnyös, amikor azt vizsgáljuk, hogy egy adott egész vagy karakter típusú kifejezés értéke egyenlQ-e egy állandókból álló halmaz valamelyik elemével. Az összehasonlítás céljából a 3. fejezetben bemutatjuk az elQbbi a program switch utasítással megírt változatát.

1.12. Gyakorlat. îrjunk olyan programot, amely kinyomtatja a bemenetén elQforduló szavak hosszúságának hisztogramját! A legegyszerqbb, ha a hisztogramot vízszintesen rajzoljuk; a függQleges irányú rajzolás nehezebb feladat.

1.7. Függvények

A C nyelvben a függvény ugyanaz, mint a FORTRAN-ban a szubrutin, ill. függvény, vagy a PL/1-ben, a PASCAL-ban és más nyelvekben az eljárás. A függvény kényelmes lehetQséget nyújt számunkra, hogy valamely számítási részt "fekete dobozba" zárjunk, amelyet azután használhatunk anélkül, hogy tartalmával törQdnünk kellene. Valójában csak a függvények segítségével bírkózhatunk meg nagy és bonyolult programokkal. Helyesen tervezett függvények esetében teljesen figyelmen kívül hagyhatjuk, hogyan keletkezik a függvény értéke (eredménye); elegendQ a feladat és az eredmény ismerete. A C nyelv egyszerq, kényelmes és hatékony függvényhasználatot tesz lehetQvé. Gyakran fogunk olyan függvényekkel találkozni, amelyek csupán néhány sorból állnak és amelyeket csak egyszer hívunk meg: ezeket kizárólag a program világosabbá tétele érdekében használjuk.
Ezidáig csak olyan függvényeket használtunk, mint a printf, a getchar vagy a putchar, amelyeket készen kaptunk; itt az ideje, hogy magunk is írjunk néhányat. Mivel a C nyelvnek nincs olyan hatványozó operátora, mint a ** a FORTRAN-ban vagy a PL/1-ben, szemléltessük a függvénydefiniálás technikáját a power(m, n) függvény megírásával, amely az m egész típusú változót a pozitív egész n hatványra emeli. Tehát a power(2,5) függvény értéke 32. Ez a függvény nyilvánvalóan nem tudja mindazt, amit ** tud, mivel csak kis egész számok pozitív hatványait tudja kezelni, de legjobb, ha egyszerre csak egy problémára összpontosítunk.
Az alábbiakban a power függvényt egy fQprogramba ágyazva mutatjuk be. Ne feledjük, hogy a main() maga is függvény!

main ()
/*Hatványozó függvény tesztelése*/
{
int i;
for (i = 0; i < 10; ++i)
printf ("%d %d %d\n", i, power (2,i), power (-3,i));
}

power (x,n)
/*x n-dik hatványra emelése; n >0*/
int x, n;
{
int i, p;
p = 1;
for (i = 1; i <= n; ++i)
p = p * x;
return (p);
}

Mindkét függvény az alábbi alakú:

név (opcionális argumentumlista)
opcionális argumentumdeklarációk
{
deklarációk
utasítások
}

A függvények tetszQleges sorrendben szerepelhetnek, és egy vagy két forrásállományban egyaránt állhatnak. Természetesen, ha a forrás két állományban található, bonyolultabb a fordítás és a töltés, mintha minden egyetlen állományban van, de ez az operációs rendszer kérdése és nem a nyelvjellegzetessége. Pillanatnyilag feltesszük, hogy a két függvény ugyanabban az állományban van, tehát mindaz, amit a C programok futtatásáról megtanultunk, nemváltozik.
A power függvényt a

printf ("%d %d %d\n", i, power(2,i), power(-3,i));

sorban kétszer hívtuk meg. Mindkét hívás két argumentumot ad át a power függvénynek, amely mindkét alkalommal visszaad egy-egy egész számot, amit a hívó program formátumoz és megjelenít. Kifejezésen belül power(2, i) ugyanolyan egész, mint 2 és i. (Nem minden függvény eredményez egész értéket: ezt a témát a 4. fejezetben folytatjuk.)
A power argumentumait megfelelQképpen deklarálni kell ahhoz, hogy a típusuk ismert legyen. Ez a függvény nevét követQ

int x, n;

sorban történik. Az argumentumdeklarációk az argumentumlista és a nyitó bal kapcsos zárójel között vannak; minden deklarációt pontosvesszQ zár le. A power függvény által a saját argumentumai számára használt nevek teljes mértékben lokálisak a power függvényre nézve, azokhoz semmilyen más függvény sem férhet hozzá: más rutinok veszélytelenül használhatják ugyanezeket a neveket. Ez a p és az i változóra is vonatkozik: a power-beli i változónak semmi köze a main-ben használt i-hez.
A power függvény által kiszámított értéket - mint a PL/1-ben - a return utasítás adja vissza a main-nek. A zárójelek között tetszQleges kifejezés elQfordulhat. Egy függvénynek nem feltétlenül szükséges értéket visszaadnia: egy kifejezés nélküli return utasítás átadja a vezérlést, de nem ad át hasznos értéket a hívónak - ez történik olyankor, amikor a vezérlés a függvény végét átlépi azáltal, hogy eléri a jobb oldali záró kapcsos zárójelet.

1.13. Gyakorlat. îrjunk olyan programot, amely a beolvasott szöveget kisbetqssé alakítja át egy olyan lower(c) függvény segítségével, amely c-vel tér vissza, ha c nem betq, és c kisbetqs megfelelQjét adja vissza, ha c betq! D

1.8. Argumentumok; érték szerinti hívás

A C függvények egyik tulajdonságát más nyelvekben - különösen a FORTRAN-ban vagy PL/1-ben - járatos programozók szokatlannak találhatják: a C-ben mindig érték szerinti függvényargumentum-átadás történik. Ez azt jelenti, hogy a hívott függvény az argumentumainak nem a címét, hanem - ideiglenes változóban (valójában egy veremben) - az értékét kapja meg. Ez bizonyos eltérQ tulajdonságokhoz vezet az olyan név szerint hívó nyelvekhez képest, mint amilyen a FORTRAN és a PL/1, amelyekben a hívott rutin az argumentum címét, nem pedig az értékét kapja meg.
A fQ különbség az, hogy a C-ben a hívott függvény nem tudja megváltoztatni a hívó függvény változóinak értékét, csak a saját, ideiglenes változópéldányainak tud új értéket adni.
Az érték szerinti hívás azonban elQny, nem pedig hátrány. 9ltala legtöbbször tömörebb programokat állíthatunk elQ, kevesebb segédváltozót kell használnunk, mivel a hívott rutinban az argumentumok ugyanolyan módon kezelhetQk, mint a hagyományosan inicializált változók. Nézzük például a power következQ változatát, amely kihasználja ezt a tényt:

power (x,n)
/*x n-edik hatványra emelése; n > 0; 2. változat*/
int x, n;
{
int p;
for (p = 1; n > 0; --n)
p = p * x;
return (p);
}

Az n argumentumot ideiglenes változóként használtuk és addig dekrementáltuk, amíg el nem érte a 0-t; így nincs szükség az i változóra. Mindannak, ami az n-nel a power-en belül történik, nincs befolyása arra az argumentumra, amellyel eredetileg a függvényt meghívtuk.
Szükség esetén megoldható, hogy a függvény módosítani tudja az Qt hívó rutin valamelyik változóját. A hívónak meg kell adnia a módosítandó változó címét (gyakorlatilag egy, a változót megcímzQ mutatót), és a hívott függvénynek az argumentumot mutatóként kell deklarálnia, a tényleges változóra ezen keresztül, indirekt módon kell hivatkoznia. Ezzel az 5. fejezetben foglalkozunk.
Ha egy tömb nevét használjuk argumentumként, akkor a függvénynek átadott érték ténylegesen a tömb kezdetének helye vagy címe. (A tömbelemek nem másolódnak át). Ezt az értéket indexelve a függvény a tömb tetszQleges elemét elérheti és megváltoztathatja. Ezzel a következQ fejezet foglalkozik.

1.9. Karaktertömbök

A C nyelvben leggyakoribb tömbtípus valószínqleg a karaktertömb. A karaktertömbök és az Qket kezelQ függvények használatát egy olyan programmal szemléltetjük, amely sorokat olvas be és közülük a leghosszabbat megjeleníti. Az alapstruktúra meglehetQsen egyszerq :

while (van még sor)
if (hosszabb, mint az eddigi leghosszabb sor)
tárold a sort és a hosszát
nyomtasd ki a leghosszabb sort

Ez a struktúra világossá teszi a program természetes tagozódását. Az egyik rész beolvassa és megvizsgálja az újsort, a másik tárolja, a harmadik vezérli a folyamatot.
Minthogy a feladatok ilyen szépen elkülöníthetQk, helyes, ha a programot is eszerint írjuk meg. Ennek megfelelQen elQször írjunk egy külön getline függvényt, amely beolvassa a bemenetrQl a következQ sort,_ ez a getchar függvény általánosítása. Szeretnénk ha a függvény más környezetben is használható lenne, ezért igyekszünk a lehetQ legrugalmasabbá tenni. A minimális igény, hogy a getline jelezze vissza az esetleges állományvéget; 9ltalánosabban használható lesz a függvény, ha a sor hosszát adja vissza, vagy pedig nullát, ha elérte az állomány végét. A nulla bizonyosan nem valódi sorhossz, mivel minden sor legalább egy karaktert kell, hogy tartalmazzon, még a csupán egyetlen soremelést tartalmazó sor hossza is 1.
Ha azt találjuk, hogy egy sor hosszabb, mint az addigi leghosszabb sor, valahová el kell mentenünk. Logikus, hogy ez egy második függvény, a copy feladata legyen, amely az új sort biztos helyre menti.
Végezetül szükségünk van egy fQprogramra, amely vezérli a getline-t és a copy-t. Ime az egész program:

#define MAXLINE 1000 /*A beolvasott sor maximális mérete*/
main ()
/*A leghosszabb sor kiválasztása*/
{
int len; /*A pillanatnyi sor hossza*/
int max; / *Az eddigi maximális hossz*/
char line [MAXLINE]; /*A pillanatnyilag olvasott sor*/
char save [MAXLINE]; /*A leghosszabb sor mentésére*/
max = 0;
while ((len = getline (line,MAXLINE)) > 0)
if (len > max) {
max = len;
copy (line,save);
}
if (max > 0) /*Volt sor*/
printf ("%s", save);
}

getline (s,lim)
/*Sor beolvasása s-be, a hosszát adja vissza*/
char s [];
int lim;
{
int c, i;
for (i = 0; i < lim - 1 && (c =getchar ()) != EOF && c != '\n'; ++i)
s [i] = c;
if (c == '\n') {
s [i] = c;
++i;
}
s [i] = '\0';
return (i);
}

copy (s1,s2)
/*s1 másolása s2-be; s2-t elég nagynak feltételezi*/
char s1 [], s2 [];
{
int i;
i = 0;
while ((s2 [i] = s1 [i]) != '\0')
++i;
}

main csakúgy, mint getline, két argumentumon és egy visszaadott értéken keresztül kommunikál. A getline-ban az argumentumokat a

char s [];
int lim;

sorok deklarálják, amelyek elQírják, hogy az elsQ argumentum tömb, a második pedig egész típusú legyen. Az s tömb hossza getline-ban nincs megadva, mivel azt a main-ben határozzuk meg. A getline a return utasítás segítségével küld vissza értéket a hívónak úgy, ahogy azt a power függvénynél láttuk. Egyes függvényekhasznos értéket szolgáltatnak, míg másokkal, így a copy-val is valamely adott feladatot végeztetünk el, és nem adnak vissza hasznos értéket.
A getline az általa létrehozott tömb végére, a karakterlánc végének jelzésére egy \0 karaktert (egy nulla karaktert, amelynek értéke 0) helyez el. îgy mqködik a C fordító is: amikor egy karakterlánc állandó, mint például

"hello\n"

van a C programban, a fordító a lánc karaktereit tartalmazó karaktertömböt hoz létre, amelyet egy \0-val zár le. A függvények, pl. a printf, így képesek a karakterlánc végének az érzékelésére:

h e l l o \n \0

A printf %s formátumspecifikációja egy ilyen formában ábrázolt karakterláncot vár. Ha megvizsgáljuk a copy függvényt, észrevehetjük, hogy az is arra támaszkodik, hogy az s1 bemeneti argumentumot \0 zárja le, és ezt a karaktert is átmásolja az s2 kimeneti argumentumra. (Mindez azt feltételezi, hogy \0 nem része a normál szövegnek.)
Futólag érdemes megjegyeznünk, hogy még egy ilyen kis program is felvet néhány kényes tervezési problémát. Például mit csináljon main, ha olyan sorral találkozik, amely hosszabb, mint a megadott korlát? A getline helyesen mqködik: amikor a tömb megtelt, leáll, még akkor is, ha nem talált újsort. A hosszat és az utolsónak visszaadott karaktert ellenQrizve a main el tudja dönteni, hogy a sor túl hosszú volt-e, majd tetszés szerint cselekedhet. A rövidség kedvéért ezt a problémát figyelmen kívül hagytuk.
Aki a getline függvényt használja, nem tudhatja elQre, hogy milyen hosszú lehet egy beolvasott sor, így a getline ellenQrzi a túlcsordulást. MásfelQl a copy használója már tudja (vagy kiderítheti), hogy mekkorák a karakterláncok, ezért úgy döntöttünk, hogy ezt a függvényt nem egészítjük ki hibaellenQrzéssel.

1.14. Gyakorlat. Módosítsuk a leghosszabb sort keresQ program fQ rutinját oly módon, hogy helyesen írja ki tetszQlegesen hosszú bemeneti sorok hosszát és a szövegbQl annyit, amennyi csak lehetséges!

1.15. Gyakorlat. Irjunk programot, amely minden olyan sort megjelenít, amely 80 karakternél hosszabb!

1.16. Gyakorlat. Irjunk olyan programot, amely eltávolítja a sorvégi szóközöket és tab karaktereket a bemenet minden sorából és törli a teljesen üres sorokat!

1.17. Gyakorlat.Irjunk olyan reverse(s) függvényt, amely megfordítja az s karakterláncot! Használjuk fel ezt a függvényt olyan program megírásához, amely soronként megfordítja a beolvasott szöveget!

1.10. Érvényességi tartomány; külsQ változók

A main-en belüli változók (line, save stb.) a main saját változói, vagyis a main-re nézve lokálisak. Mivel ezeket a main-en belül deklaráltuk, egyetlen más függvény sem tud közvetlenül hozzájuk férni. Ugyanez mondható más függvények változóiról; például a getline függvényen belüli i változó független a copy i változójától. A függvények lokális változói csak meghívásukkor jönnek létre, és megsemmisülnek, amikor a vezérlés a függvénybQl kilép. Az ilyen dinamikus lokális változókat ezért - más nyelvek szóhasználatához hasonlóan - automatikus változóknak nevezzük. A 4. fejezetben tárgyaljuk az ún. static tárolási osztályt, amelyben a lokális változók megtartják az értéküket két függvényhívás között.
Minthogy az automatikus változók élettartama arra az idQre korlátozódik, amíg a vezérlés a függvényen van, értéküket nem Qrzik meg egyik hívástól a másikig, így minden belépéskor explicit módon értéket kell adni nekik. Ha ezt elmulasztjuk, tartalmuk bizonytalan.
Az automatikus változók mellett olyan változókat is definiálhatunk, amelyek az összes függvényre nézve külsQk, így értékük függvényhívásoktól függetlenül fennmarad. Ezeket a globális változókat bármelyik függvény név szerint elérheti (hasonlóan a FORTRAN nyelv common vagy a PL/1 external mechanizmusához). Globális hozzáférhetQségük miatt a függvények közötti adatátadást argumentumlisták helyett külsQ változókon keresztül is megoldhatjuk.
A külsQ változókat az összes függvényen kívül kell definiálni: ezzel tárolóhelyet foglalunk le számukra. A változókat minden olyan függvényben, ahol használni akarjuk, vagy explicit módon az extern alapszóval, vagy implicit módon értelemszerqen, de deklarálnunk is kell. Mindez bizonyára érthetQbb lesz, ha példaként újra megírjuk a leghosszabb sort keresQ programot úgy, hogy a line, a save és a max külsQ változó legyen. Ehhez mindháromfüggvényben meg kell változtatnunk a hívásokat, a deklarációkat és a függvények törzseit.

#define MAXLINE 1000 /*A beolvasott sor maximális mérete*/
char line [MAXLINE]; /* A beolvasott sor*/
char save [MAXLINE]; /*A leghosszabb sor mentésére*/
int max; /*Az eddigi maximális hossz*/
main ()
/*A leghosszabb sor kiválasztása; speciális változat*/
{
int len; /*A pillanatnyi sor hossza*/
extern int max;
extern char save [ ];
max = 0;
while ((len = getline ()) > 0)
if (len > max) {
max = len;
copy ();
}
if (max > 0) /*Volt sor*/
printf ("%s", save);
}

getline ()
/* Speciális változat*/
{
int c, i;
extern char line [];
for (i = 0; i < MAXLINE - 1 && (c=getchar ()) != EOF && c != '\n'; ++i)
line (i] = c;
if (c == '\n') {
line [i] = c;
++i;
}
line [i] = '\0';
return (i);
}

copy ()
/*Speciális változat*/
{
int i;
extern char line [], save [];
i = 0;
while ((save [i] = line [i]) != '\0')
++i;
}

Példánkban a main, a getline és a copy függvényben elQforduló külsQ változókat az elsQ sorokban definiáltuk, itt határoztuk meg típusukat és foglaltuk le a szükséges tárterületet. Ezek a külsQ definíciók ugyanolyan felépítésqek, mint a korábban látott deklarációk, de mivel függvényeken kívül fordulnak elQ; külsQ változókat adnak meg. Függvényben külsQ változót csak akkor használhatunk, ha elQzQleg közöljük a függvénnyel a változó nevét. Ennek egyik módja, hogy a függvényben egy extern deklarációt helyezünk el, amely mindössze abban különbözik az eddigi deklarációktól, hogy az extern alapszóval kezdQdik.
Bizonyos körülmények között az extern deklaráció elhagyható; ha a forrásszövegben egy változó külsQ definíciója megelQzi a változó használatát valamely függvényben, akkor e függvényben nincs szükség extern deklarációra. Igy a main, a getline és a copy függvényben az extern deklarációk feleslegesek. Gyakorlott C-programozók általában a forrásszöveg elején definiálják az összes külsQ változót, és nem használnak extern deklarációkat.
KötelezQ azonban az extern deklaráció, ha forrásprogramunk több állományra tagolódik, és egy változót, mondjuk az A állományban definiálunk, de B-ben használunk, hiszen ilyenkor a változó két elQfordulása között csak a B-ben elhelyezett extern deklarációval teremthetünk kapcsolatot. Ezt a témát bQvebben a 4. fejezetben fejtjük ki.
Nem szabad összetévesztenünk a külsQ változók deklarációját és definícióját! A definíció az a programsor, ahol a változót ténylegesen létrehozzuk, számára tárhelyet foglalunk le; a deklaráció viszont olyan programrész, ahol csupán leírjuk a változó tulajdonságait, de tárhelyfoglalás nem történik.
Megjegyezzük, hogy az ember hajlamos az égvilágon mindent külsQ változóként megadni, mivel az látszólag egyszerqsíti az adatátadást - az argumentumlisták rövidek, és a változók mindig rendelkezésre állnak, amikor csak akarjuk. Csakhogy a külsQ változók akkor is ott vannak, ha nem akarjuk! Ez a programozási stílus súlyos veszélyeket hord magában. Az így írt programokban az adatátadások áttekinthetetlenek - a változók váratlanul, sQt a programozó szándékától teljesen eltérQ módon megváltozhatnak, és a program igen nehezen módosítható. Emiatt a leghosszabb sort keresQ program második változata gyengébb az elsQnél, de hibája az is, hogy a változók nevének rögzítésével két hasznos függvény elveszti általános jellegét.

1.18. Gyakorlat. Az elQbbi getline függvény for utasításában a feltételvizsgálat meglehetQsen ügyetlen. Javítsunk rajta, de úgy, hogy az állomány végén vagy puffertúlcsorduláskor a program az eddigi módon mqködjön! Biztos, hogy ez a legjobb szervezés?

1.11. Összefoglalás

Az 1. fejezetben áttekintettük a C nyelv legfontosabb elemeit. EbbQl a néhány építQelembQl is tekintélyes méretq, hasznos programokat írhatunk, és valószínqleg jó gondolat, ha ennek érdekében az olvasó most megfelelQ szünetet tart a könyv olvasásában. Az alább következQ gyakorlatokban programötleteket szeretnénk adni olyan programokra, amelyek bonyolultabbak mint azok, amelyeket ez a fejezet bemutatott.
Ha az olvasó már elsajátította a C nyelv eddig ismertetett elemeit, folytassa az olvasást, mivel a következQ néhány fejezetben olyan jellegzetességekrQl szólunk, amelyek nagyban hozzájárulnak a nyelv erejéhez és kifejezQképességéhez.

1.19. Gyakorlat. Irjunk detab néven programot, amely a bemeneten talált tab karakterek mindegyikét annyi szóközzel helyettesíti, amennyi a következQ tabulátorstop-ig hátravan! Tételezzünk fel egy rögzített tabulátorstopkészletet, a stop-ok mondjuk minden n-edik pozíción találhatók.

1.20. Gyakorlat. Irjuk meg az entab programot, amely a szóközökbQl álló karakterláncok helyébe a minimális számú tab karaktert és szóközt írja úgy, hogy a távolság ne változzon! Használjuk ugyanazokat a tab stop-okat, mint a detab-nál!

1.21. Gyakorlat. Irjunk olyan programot, amely a sor n-edik pozíciója elQtt elQforduló utolsó, nem szóköz karakter után "összehajtja"a hosszú bemeneti sorokat (n paraméter)! GyQzQdjünk meg róla, hogy a program tényleg értelmesen mqködik nagyon hosszú sorok esetén, de akkor is, ha a megadott pozíció elQtt egyáltalán nem szerepel szóköz vagy tab!

1.22. Gyakorlat. Irjunk olyan programot, amely egy C programból az összes megjegyzést eltünteti! Ne felejtkezzünk meg az idézQjelezett karakterláncok és karakterállandók helyes kezelésérQl!

1.23. Gyakorlat. Irjunk olyan programot, amely a C programban megtalálja az olyan alapvetQ szintaktikai hibákat, mint a nem azonos számú nyitó és záró kerek, szögletes, ill. kapcsos zárójelek! Ne felejtkezzünk meg az aposztrófokról, idézQjelekrQl, valamint a megjegyzésekrQl sem ! (Ezt a programot teljes általánosságban nehéz elkészíteni.)

2. fejezet: Típusok, operátorok és kifejezések

A programok alapvetQ adatobjektumai a változók és az állandók. A deklarációk felsorolják a használni kívánt változókat, közlik a típusukat, valamint az esetleges kezdeti értéküket. Az operátorok azt határozzák meg, hogy mit kell tenni a változókkal. A kifejezések a változókból és állandókból új értékeket hoznak létre. Fejezetünkben ezekkel foglalkozunk.

2.1. Változónevek

Bár eddig errQl nem beszéltünk, a változók és szimbolikus állandók neveire nézve vannak bizonyos megkötések. A nevek betqkbQl és számjegyekbQl állnak: az elsQ karakter betq kell, hogy legyen. Az aláhúzás karakter (_) betqnek számít: ezzel javíthatjuk a hosszú változónevek olvashatóságát. A nagy- és a kisbetq különbözQnek számít; a hagyományos C programozási gyakorlat szerint a változónevek kisbetqsek, a szimbolikus állandók csupa nagybetqbQl állnak.
A belsQ nevekben csupán az elsQ nyolc karakter értékes, bár ennél hoszszabb nevek is használhatók. KülsQ nevek esetén, így függvényneveknél és külsQ változóknál ez a szám nyolcnál kevesebb is lehet, mivel a külsQ neveket különféle assemblerek és töltQprogramok (loaderek) is használják. Ennek részleteit az A. függelék ismerteti. Ezenkívül az olyan kulcsszavak, mint if, else, int, float stb. fenntartott szavak; változónévként nem használhatók. (Kisbetqseknek kell lenniük.)
Természetesen ésszerq olyan változóneveket választani, amelyek jelentenek valamit, kapcsolódnak a változó funkciójához, és tipográfiailag nem zavarók.

2.2. Adattípusok és méretek

A C-ben csak néhány alapvetQ adattípus van:

char egyetlen byte, amely az érvényes karakterkészlet egy elemét tartalmazhatja.
int egész szám, amely tipikusan a befogadó gépre jellemzQ egész szám ábrázolási méretet tükrözi.
float egyszeres pontosságú lebegQpontos szám.
double kétszeres pontosságú lebegQpontos szám.

Ezen kívül van néhány minQsítQ szimbólum, amely az int mennyiségekre alkalmazható: short, long, valamint unsigned. short (rövid), ill. long (hosszú) különbözQ méretq egész számot jelöl. Az unsigned (elQjel nélküli) számokra a modulo 2n aritmetika szabályai vonatkoznak, ahol n az int típust ábrázoló bit-ek száma; az unsigned számok mindig pozitívak. A minQsítQk deklarációjának alakja:

short int x;
long int y;
unsigned int z;

Ilyen esetekben az int szó elhagyható, és el is szokás hagyni.
Ezeknek az objektumoknak a pontossága a rendelkezésre álló géptQl függ; a következQ táblázat néhány - bitekben megadott - jellemzQ értéketmutat.

DEC PDP-11 Honeywell6000 IBM 370 Interdata 8/32
ASCII ASCII EBCDIC ASCII
char 8 8 8 8
int 16 36 32 32
short 16 36 16 16
long 32 36 32 32
float 32 36 32 32
double 64 72 64 64

A cél az, hogy ahol kívánatos, a short, ill. a long különbözQ hosszúságú egészeket hozzon létre; int általában az adott gépnek megfelelQ legtermészetesebb méret. Látható, hogy minden fordító a saját hardverjének megfelelQen szabadon értelmezheti a short, ill. long minQsítQket, az azonban bizonyos, hogy a short nem hosszabb, mint a long.

2.3. 9llandók

Az int és float állandókkal már végeztünk, csupán azt tesszük még hozzá, hogy a szokásos

123.456e-7

vagy a

0. 123E3

jelölésmód a float számok esetében egyaránt megengedett. Minden lebegQpontos állandó double-nak számít, ezért az "e" jelölés a float és a double számokra egyaránt megfelelQ.
A long állandók írásmódja: 123L. Azok a közönséges egész állandók, amelyek hosszabbak annál, hogy egy int-be beleférjenek, ugyancsak long-nak számítanak.
Külön jelölésmódja van az oktális és a hexadecimális állandóknak:ha egy int típusú állandó 0-val (nullával) kezdQdik, a szám nyolcas (oktális) számrendszerben értendQ; a vezetQ 0x vagy 0X pedig azt jelenti, hogy hexadecimális (tizenhatos számrendszerbeli) számról van szó. Például a decimális 31 ugyanannyi, mint az oktális 037 vagy a hexadecimális 0x1F, ill. 0X1F. A hexadecimális és oktális állandókból az utánuk írt L-lel szintén képezhetünk long mennyiséget.
A karakterállandó egyetlen, aposztrófok közé írt karakter, például 'x'. A karakterállandó értéke a karakternek a gép karakterkészletén belüli numerikus értéke. Például a nulla karakter, vagyis '0' értéke az ASCII karakterkészletben 48, az EBCDIC-ben pedig 240, mindkét érték teljesen különbözQ a 0 numerikus értéktQl. Ha számértékek, mint 48 vagy 240 helyett '0'-t írunk, akkor a program függetlenné válik a karakter adott értékétQl. A karakterállandók ugyanúgy vesznek részt a numerikus mqveletekben, mint bármilyen más szám, bár leggyakrabban más karakterekkel való összehasonlításra használjuk Qket. A konverziós szabályokkal egy késQbbi fejezet foglalkozik.
Bizonyos nemgrafikus karakterek escape szekvenciák segítségével ábrázolhatók karakterállandóként, mint például \n (újsor), \t (tab), \0 (nulla), \\(fordított törtvonal), \' (aposztróf) stb., amelyek két karakternek látszanak, de valójában mindegyik csak egy karakter. Ezenkívül tetszQleges, egy byte méretq bit-minta hozható létre a

'\ddd'

alak segítségével, ahol ddd egy, kettQ vagy három oktális számjegy, pl.:

#define FORMFEED '\014' /* ASCII lapdobás karakter*/

A '\0' karakterállandó a nulla értékq karaktert jelöli. 0 helyett gyakran írunk '\0'-át, amivel valamely kifejezés karakter jellegét hangsúlyozzuk.
Az állandó kifejezés olyan kifejezés, amely csak állandókat tartalmaz. Az ilyen kifejezések kiértékelése fordítási idQben történik, nem pedig futási idQben, és így egyszerq állandónak felelnek meg. Például:

#define MAXLINE 1000
char line [MAXLINE + 1];

vagy

seconds = 60 * 60 * hours;

A karakterlánc-állandó (stringkonstans) idézQjelek közé zárt, nulla vagy több karakterbQl álló sorozat, pl.

"ez itt egy karakterlánc"

vagy

"" /* Üres karakterlánc*/

Az idézQjelek nem részei a karakterláncnak, csupán annak határolására szolgálnak. A karakterláncokban ugyanazok az escape szekvenciák használhatók, mint amelyeket a karakterállandóknál láttunk; \" az idézQjel karaktert jelöli.
Gyakorlatilag a karakterlánc olyan tömb, amelynek minden eleme egy-egy karakter. A fordító automatikusan elhelyezi a \0 nullakaraktert minden ilyen karakterlánc végére, így a programok kényelmesen megtalálhatják a karakterlánc végét. Ez a fajta ábrázolás azt jelenti, hogy nincs tényleges határa a karakterlánc hosszának, de egy adott karakterlánc hosszának megállapításához a programnak végig kell mennie az illetQ karakterláncon. A szükséges fizikai tárhely nagysága egy tárhellyel több, mint az idézQjelek közé írt karakterek száma. Az alábbi strlen(s) függvény az s karakterlánc hosszát adja vissza, kizárva ebbQl a záró \0-t.

strlen (s)
/* s hosszának kiszámítása*/
char s [];
{
int i;
i = 0;
while (s [i] != '\0')
++i;
return (i);
}

Vigyázat! A karakterállandó és az egyetlen karaktert tartalmazó karakterlánc két különbözQ dolog: 'x' nem ugyanaz, mint "x". Az elQbbi egyetlen karakter, amely az x betqnek a gép karakterkészlete szerint megfelelQ számérték elQállítására szolgál, az utóbbi egy karakterlánc, amely egy karaktert (az x betqt) és egy \0-át tartalmaz.

2.4. Deklarációk

Használat elQtt minden változót deklarálni kell, bár bizonyos deklarációk implicit módon, értelemszerqen keletkeznek. A deklaráció meghatároz egy típust, amelyet az illetQ típusú változó(ka)t megadó lista követ, mint például:

int lower, upper, step;
char c, line [1000];

A változók tetszQleges módon oszthatók szét a deklarációk között; az elQzQ listákat így is írhattuk volna:

int lower;
int upper;
int step;
char c;
char line [1000];

az utóbbi forma több helyet igényel, de így pl. minden deklarációhoz vagy az azt követQ módosításokhoz megjegyzést fqzhetünk.
A változók saját deklarációikban inicializálhatók is, bár ezzel kapcsolatban vannak megkötések. Ha a nevet egy egyenlQségjel és egy állandó követi, akkor az az illetQ változó kezdeti értékének megadását (inicializálását) jelenti:

char backslash = '\\';
int i = 0;
float eps = 1.0e-5;

KülsQ vagy statikus változó esetén az inicializálás csak egyszer - értelemszerqen a program végrehajtásának megkezdése elQtt - történik meg. Az explicit módon inicializált automatikus változók minden alkalommal inicializálódnak, amikor az Qket tartalmazó függvényt egy program meghívja. Az explicit inicializálás nélküli automatikus változók értéke határozatlan. A külsQ és statikus változók kezdeti értéke alapértelmezés szerint nulla, de stilárisan helyesebb, ha minden esetben megadjuk a kezdeti értéket.
Az inicializálás témáját akkor folytatjuk, amikor a további adattípusokról lesz szó.

2.5. Aritmetikai operátorok

Az aritmetikai operátorok a +, -, *, / és a % (moduló) operátor. Van egyoperandusú -, de nincs egyoperandusú +.
Az egész típusú (integer) osztás levágja a tört részt. Az

x % y

kifejezés az x-nek y-nal történQ osztásakor keletkezQ maradékot jelenti, tehát értéke nulla, ha x pontosan osztható y-nal. Például egy év általában akkor szökQév, ha az évszám 4-gyel osztható, de nem osztható 100-zal. Kivételt jelentenek a 400-zal osztható évek, amelyek szintén szökQévek. îgy

if (year % 4 == 0 && year % 100 != 0 || year % 400 == 0)
szökQév van
else
nincs szökQév

A % operátor float és double mennyiségekre nem alkalmazható.
A + és - operátorok precedenciája azonos és alacsonyabb, mint a * , / és % (egymással szintén azonos) precedenciája, amely viszont alacsonyabb, mint az egyoperandusú mínuszé. Az aritmetikai operátorok balról jobbra kötnek. (A 2. fejezet végén közölt táblázat összefoglalja az összes operátor precedenciáját és kötési módját.) A kiértékelés sorrendje olyan asszociatív és kommutatív operátoroknál, mint a * és +, nincs meghatározva; a fordító átrendezheti az olyan zárójelezett számításokat, amelyek ezek valamelyikét tartalmazzák. îgy a+(b+c) azonos (a+b)+c-vel. Ennek ritkán van jelentQsége, de ha adott sorrendre van szükség, akkor explicit ideiglenes változókat használhatunk.
A túlcsordulás és alulcsordulás esetének kezelése az adott géptQl függ.

2.6. Relációs és logikai operátorok

A relációs operátorok:

> >= < <= =

Ezek mindegyikének azonos a precedenciája. Eggyel alacsonyabb - és egymás közt egyezQ - precedenciájúak az egyenlQségoperátorok:

== !=

A relációs operátorok precedenciája alacsonyabb, mint az aritmetikaiaké, így a várakozásnak megfelelQen i < lim - 1 ugyanaz, mint i < (lim - 1).
Még érdekesebbek a && és || logikai összekapcsoló mqveletek. A && vagy || szimbólumokkal összekapcsolt kifejezések kiértékelése balról jobbra történik, és a kiértékelés azonnal megáll, amint az eredmény igaz vagy hamis volta kiderül. Ezek a tulajdonságok lényegbevágóak, ha jól mqködQ programokat akarunk írni. Itt van például az 1. fejezetben írt getline sorbeolvasó függvény egyik ciklusa:

for (i = 0; i < lim - 1 && (c = getchar()) != '\n' && c != EOF; ++i)
s [i] = c;

oj karakter beolvasása elQtt nyilvánvalóan ellenQriznünk kell, hogy a beolvasandó karakter tárolásához van-e elég hely az s tömbben, így az i < lim - 1 vizsgálatot kell elsQként végrehajtani! SQt, ha a feltétel nem áll fenn, újabb karaktert már nem szabad beolvasni!
Ugyancsak nem volna szerencsés, ha a c-nek az EOF-fal történQ összehasonlítása a getchar hívása elQtt történne meg_ a hívásnak meg kell elQznie a c-ben található karakter vizsgálatát! && magasabb precedenciájú ||-nél, de mindketten alacsonyabb precedenciájúak, mint a relációs és egyenlQségoperátorok, így az olyan kifejezések, mint

i < lim - 1 && (c = getchar()) != '\n' && c != EOF

külön zárójeleket nem igényelnek. De mivel a != precedenciája magasabb, mint az értékadásé, a kívánt eredmény elérése érdekében a

(c = getchar()) != '\n'

kifejezésben zárójelekre van szükség.
A ! egyoperandusú negáló operátor a nemnulla, másszóval igaz operandusból 0-t, a nulla, azaz hamis operandusból pedig 1-et csinál. A ! operátort általában olyan szerkezetekben használják, mint pl.

if (! inword),

s ezzel helyettesítjük az

if (inword == 0)

formát. Nehéz általánosságban megmondani, hogy melyik alak a jobb. Az elQbbi általában jól olvasható ("ha nem szó belsejében vagyunk"), bonyolultabb esetben azonban nehezen érthetQ.

2.1. Gyakorlat. îrjunk az elQzQ, for ciklussal egyenértékq ciklust, amely a &&-et használja!

2.7. Típuskonverziók

Ha egy kifejezésben különbözQ típusú operandusok fordulnak elQ, a kifejezés kiértékeléséhez az operandusokat azonos típusúakká kell alakítani. 9ltalában csak az értelmes konverziók történnek meg automatikusan, például egész típusú mennyiségek átalakítása lebegQpontossá olyan kifejezésekben, mint f + i, ahol f float, i pedig int típusú. Az értelmetlen kifejezések, mint például a float indexként való használata, nem megengedettek.
A char és int típusú mennyiségek aritmetikai kifejezésekben szabadon keveredhetnek: a kifejezésekben elQforduló minden char automatikusan int-té alakul át. Ez nagymérvq rugalmasságot tesz lehetQvé bizonyos karaktertranszformációkban. Példa erre az atoi függvény, amely egy számjegyekbQl álló karakterláncot a megfelelQ numerikus értékké alakít át:

atoi (s)
/ * s egésszé alakítása*/
char s [];
{
int i, n;
n = 0;
for (i = 0; s [i] >= '0' && s [i] <= '9'; ++i)
n = 10 * n + s [i] - '0';
return (n);
}

Amint az 1. fejezetben említettük, az

s [i] - '0'

kifejezés elQállítja az s[i] -ben tárolt karakter numerikus értékét, mivel a '0', '1' stb. értékek folytonosan növekvQ pozitív sorozatot alkotnak.
A char-ból int-té történQ átalakítás másik példája az alábbi lower függvény, amely egyetlen karaktert alakít át kisbetqssé, kizárólag ASCII karakterkészlet esetén. Ha a karakter nem nagybetq, a lower változatlanul adja vissza.

lower (c)
/*c konvertálása kisbetqssé; csak ASCII*/
int c;
{
if (c >= 'A' && c <= 'Z')
return (c + 'a' - 'A');
else
return (c);
}

Ez a program csak az ASCII kódkészlet használata esetén mqködik helyesen, mivel abban a megfelelQ kis- és nagybetqk távolsága rögzített, mind a kisbetqs, mind a nagybetqs ábécé numerikus értékei folytonosan követik egymást - A és Z között csak betqk vannak. Az EBCDIC karakterkészletre (IBM 360/370) ez az utóbbi tulajdonság nem érvényes, így lower nem mqködne helyesen - nem csak betqket konvertálna.
A karaktereknek egész számokká történQ átalakításával kapcsolatban megemlítjük a nyelv egy finomságát. A C nyelv nem határozza meg, hogy a char típusú változók elQjeles vagy elQjel nélküli mennyiségek-e. Kérdés tehát, hogy egy char mennyiség int típusúvá alakításakor létrejöhet-e negatív egész is? Sajnos ez az architektúrától függQen géprQl gépre változik. Bizonyos gépeken (például a PDP-11-en) az olyan char, amelynek legbaloldalibb bitje 1,negatív egésszé alakul át (elQjel-kiterjesztés: sign extension).Más gépeken a char oly módon válik int mennyiséggé, hogy a számítógép a szó bal oldalához nullákat illeszt, és így a keletkezQ érték mindig pozitív.
A C nyelv definíciója garantálja, hogy a gép szabványos karakterkészletében található egyetlen karakter sem lesz negatív, így ezeket a karaktereket szabadon használhatjuk kifejezésekben pozitív mennyiségekként. Ha azonban más, tetszQleges bit-mintákat tárolunk karakter típusú változókban, azok egyes gépeken pozitív számként, másokon negatív számként jelenhetnek meg.
Tipikus példája ennek, amikor EOF-nak a -1 értéket használjuk. Tekintsük a

char c;
c = getchar();
if (c == EOF)
...

programrészt! Olyan gépen, amely nem végez elQjel-kiterjesztést, c mindig pozitív, mivel char-nak deklaráltuk, EOF viszont negatív. îgy a feltétel sohasem teljesül. Ennek elkerülése érdekében ügyeltünk arra, hogy minden olyan változót int-nek és ne char-nak deklaráljunk, amely a getchar függvény által visszaadott értéket tartalmaz.
Valójában persze nem csak az esetleges elQjel-kiterjesztés miatt használunk int-et char helyett. Egyszerqen arról van szó, hogy a getchar függvénynek minden lehetséges karaktert vissza kell adnia (oly módon, hogy az bármilyen újabb programbemenethez felhasználható legyen), de vissza kell adnia az ezektQl különbözQ EOF értéket is! îgy a getchar függvény értéke nem jelenhet meg char-ként, hanem azt int-ként kell tárolni.
Az automatikus típuskonvertáló másik hasznos formája, hogy a relációs kifejezések (pl. i > j) és az &&, ill. || szimbólumokkal összekapcsolt logikai kifejezések értéke definíciószerqen 1 , ha a kifejezés igaz, ill. 0, ha hamis. îgy az

isdigit = c >= '0' && c <= '9';

értékadás az isdigit változónak az 1 értéket adja, ha c számjegy és a 0 értéket ha nem az. (Az if, while, for stb. feltételvizsgálatában az igaz jelentése egyszerqen: nemnulla.)
Az implicit aritmetikai konverziók mqködése teljesen értelemszerq. 9ltalában, ha egy kétoperandusú operátor, mint a + vagy a * operandusai különbözQ típusúak, a program a mqvelet elvégzése elQtt az alacsonyabb típusú változót magasabb típusúvá alakítja át. Az eredmény a magasabb típusú. Pontosabban szólva az aritmetikai operátorok az alábbi konverziós szabályok szerint hatnak:
A char és short mennyiségek int típusúvá, a float mennyiségek double típusúvá alakulnak át.
Ezután ha az egyik operandus double, a másik is double típusúvá alakul át, és az eredmény is double.
Egyébként ha az egyik operandus long, a másik is long típusúvá alakul át, és az eredmény is long lesz.
Egyébként ha az egyik operandus unsigned, a másik is unsigned típusúvá alakul át, és az eredmény is unsigned lesz.
Egyébként az operandusoknak int típusúaknak kell lenniük, és az eredmény int.
Jegyezzük meg, hogy egy kifejezésben elQforduló minden float mennyiség double-lá alakul át: a C-ben minden lebegQpontos mqvelet kétszeres pontosságú!
Az értékadás is típuskonverzióval jár: a jobb oldal értéke átalakul bal oldali típusúvá, és ez lesz egyben az eredmény típusa is. A karakterek egésszé alakulnak át - akár elQjel-kiterjesztéssel, akár anélkül -, amint azt az elQbbiekben ismertettük. Az ellentétes irányú mqvelet, az int-bQl char-ba történQ konverzió egyértelmq - a felesleges magas helyiértékq bit-ek egyszerqen elmaradnak. îgy

int i;
char c;
i = c;
c = i;

esetében c értéke nem változik. Ez mindig igaz, függetlenül attól, hogy van-e elQjel-kiterjesztés vagy sem.
Ha x float és i int, akkor: x=i valamint i= x egyaránt konverzióhoz vezet; a float-ból int-be történQ konverzió a tört rész levágását eredményezi. A double kerekítéssel alakul át float-tá.
A hosszabb int-ek rövidebbekké vagy char-okká úgy alakulnak át, hogy a program a felesleges magas helyiértékq bite-ket levágja.
Mivel a függvényargumentumok kifejezések, a függvényeknek történQ argumentumátadás ugyancsak típuskonverziókkal jár. Konkrétan a char és a short int-té válik, a float pedig double mennyiséggé. Ezért deklaráltuk a függvényargumentumokat int-nek és double-nak még akkor is, amikor a függvényt char-ral és float-tal hívtuk meg.
Végezetül tetszQleges kifejezésben is kiválthatunk, kikényszerithetünk típuskonverziót, ha ún. típusmódosító (cast) szerkezetet használunk. A (tipusnév) kifejezés szerkezetben a kifejezés az elQzQ szabályok alkalmazásával az elQírt típusúvá alakul át, úgy mintha a kifejezés hozzá lenne rendelve egy, a megadott típusú változóhoz, amelyet azután az egész szerkezet helyett használunk. Például az sqrt (gyökvonó) könyvtári rutin double típusú argumentumot vár, és értelmetlen eredményt ad, ha véletlenül valami mást kap. Ha tehát n egész típusú, akkor

sqrt ((double) n)

az n-et a sqrt-nek történQ átadás elQtt double-lá konvertálja. (Jegyezzük meg, hogy a típusmódosító szerkezet n értékét a kívánt típusban szolgáltatja; n tényleges tartalma azonban nem változik.) A típusmódosító operátor precedenciája ugyanaz, mint a többi egyoperandusú operátoré, amint azt a fejezet végén közölt összefoglaló táblázat is mutatja.

2.2. Gyakorlat. îrjuk meg a htoi(s) függvényt, amely egy hexadecimális számjegyekbQl álló karakterláncot a neki megfelelQ egész értékké alakít át! A megengedett számjegyek; 0...9, a...f és A...F.

2.8. Inkrementáló és dekrementáló operátorok

A C nyelv tartalmaz két szokatlan operátort, amelyekkel változók inkrementálhatók és dekrementálhatók. A ++ inkrementáló operátor operandusához 1 -et ad hozzá, a -- dekrementáló operátor pedig 1 -et von le belQle. A ++-t gyakran használjuk változók inkrementálására, például:

if (c == '\n')
++nl;

A szokatlanság abban rejlik, hogy a ++ és a -- egyaránt használható prefix operátorként (a változó elQtt, mint a ++n esetében) vagy postfix operátorként (a változó mögé írva: n++). Az eredmény mindkét esetben n inkrementálása. De míg a ++n kifejezés n-et az elQtt növeli, hogy felhasználná annak értékét, n++ csak azt követQen inkrementál. Eszerint olyan esetekben amikor nemcsak az inkrementáló tulajdonságot, hanem n értékét is felhasználjuk, ++n és n++ különböznek egymástól. Ha n értéke 5, akkor

x = n++;

az x-et 5-re állítja, de

x = ++n;

x-et 6-ra állítja. n mindkét esetben 6 lesz. Az inkrementáló és dekrementáló operátorok csak változókra alkalmazhatók; az olyan kifejezés, mint

x = (i + j)++

nem megengedett !
Ha az értékre nincs szükség, csak az inkrementáló hatásra, pl.

if (c == '\n')
nl++;

esetében, a prefix vagy a postfix operátort tetszés szerint választhatjuk meg. Vannak azonban olyan feladatok, amikor speciálisan az egyikre vagy a másikra van szükség. Tekintsük például a squeeze(s, c) függvényt, amely az összes elQforduló c karaktert törli az s karakterláncból:

squeeze (s, c) /*Valamennyi c karakter törlése s-bQl*/
char s [];
int c;
{
int i, j;
for (i = j = 0; s [i] != '\0'; i++)
if (s [i] != c)
s [j++] = s [i];
s [j] = '\0';
}

Minden alkalommal, amikor a program az s karakterláncban c-vel nem azonos karaktert talál, bemásolja azt a pillanatnyi j pozícióba, és csak ezután inkrementálja j-t, hogy fogadhassa a következQ karaktert. Hatása pontosan azonos az

if (s [i] != c) {
s [j] = s [i];
j++;
}

alakéval. Hasonló példa fordult elQ az 1. fejezetben látott getline függvényben, ahol az

if (c == '\n') {
s [i] = c;
++i;
}

sorokat az ennél tömörebb

if (c == '\n')
s [i++] = c;

alakkal helyettesíthetjük.
Harmadik példánk az strcat(s, t) függvény, amely a t karakterláncot az s karakterlánc végéhez illeszti (konkatenálja). strcat feltételezi, hogy s-ben elég hely van ahhoz, hogy ott az összeillesztett karakterlánc elférjen.

strcat (s,t) /*t illesztése s végéhez*/
char s[], t []; / * s-nek elég nagynak kell lennie * /
{
int i, j;
i = j = 0;
while (s[i] != '\0') /*Keresi s végét*/ while (s [i] = '\ ) / eresi s vég /
i++;
while ((s [i++] = t[j++]) != '\0') /*t átmásolása*/
;
}

Miközben a program az egyes karaktereket t-bQl s-be másolja, a ++ postfix operátor mind i, mind pedig j értékét növeli, hogy azok a következQ ciklusban a megfelelQ pozícióra mutassanak.

2.3. Gyakorlat. îrjuk meg az squeeze(s1 , s2) egy másik változatát, amely s1-bQl minden olyan karaktert töröl, amely megegyezik bármelyik s2 beli karakterrel!

2.4. Gyakorlat. îrjuk meg az any(s1, s2) függvényt, amely megadja az s1 karakterláncnak azt a legelsQ pozícióját, ahol bármelyik, s2 karakterláncbeli karakter elQfordul, és -1 értéket szolgáltat, ha s1 egyetlen s2-beli karaktert sem tartalmaz!

2.9. Bitenkénti logikai operátorok

A C nyelvben több bitmanipulációs operátor van; ezek a float és double típusú változókra nem alkalmazhatók.

& bitenkénti ÉS,
| bitenkénti megengedQ (inkluzív) VAGY,
^ bitenkénti kizáró (exkluzív) VAGY,
<< bitléptetés (shift) balra,
>> bitléptetés (shift) jobbra,
~ egyes komplemens (egyoperandusú).

A bitenkénti ÉS operátort gyakran használjuk valamely bithalmaz maszkolására. Például:

c = n & 0177;

mindent nulláz, az n kis helyiértékq bitjeinek kivételével. A | bitenkénti VAGY operátorral lehet biteket 1 -re állítani.

x = x | MASK;

ugyanazokat a biteket állítja 1-be x-ben, mint amelyek 1-be vannak állítva MASK-ban.
Gondosan meg kell különböztetnünk az & és | bitenkénti operátorokat az && és || logikai mqveletektQl, amelyek egy igazságérték balról jobbra történQ kiértékelését írják elQ. Ha például x értéke 1 és y értéke 2, akkor x & y értéke 0, x&&y értéke pedig 1 . (Miért?)
A << és >> léptetQ (shift) operátorok bal oldali operandusukon annyi bitléptetést hajtanak végre, ahány bitpozíciót a jobb oldali operandusuk elQír. Igy x <<2 az x-et két pozícióval balra lépteti, a megürült biteket pedig 0-val tölti fel; ez 4-gyel való szorzással egyenértékq. unsigned mennyiség jobbra léptetése esetén a felszabaduló bitekre nullák kerülnek. ElQjeles mennyiség jobbra léptetése esetén bizonyos gépeken, így a PDP-11-en a felszabaduló bitekre az elQjel kerül (aritmetikai léptetés), más gépeken 0 bitek (logikai léptetés).
A ~ bináris operátor egész típusú mennyiség 1 -es komplemensét képezi, vagyis minden 1 -es bitet 0-ra állít és viszont. Ezt az operátort leggyakrabban olyan kifejezésekben használjuk, mint

x & ~077

amely x utolsó 6 bitjét 0-ra maszkolja. Vegyük észre, hogy x & ~077 független a szóhosszúságtól, és így elQnyösebb, mint például x & 0177700, amely feltételezi, hogy x 16 bites mennyiség. A gépfüggetlen alak nem növeli a futási idQt, mivel ~077 állandó kifejezés, és mint ilyen, fordítási idQben értékelQdik ki.
KövetkezQ programpéldánkban néhány bitoperátor mqködését szemléltetjük. A getbits(x, p, n) függvény x-nek a p-edik pozíción kezdQdQ n-bites mezQjét adja vissza (jobbra igazítva). Feltételezzük, hogy a 0 bitpozíció a jobb szélen van és hogy n és p értelmes pozitív értékek. Például getbits (x,4,3) a 4, 3 és 2 pozíción levQ három bitet szolgáltatja, jobbra igazítva.

getbits (x, p, n) /*n bit a p pozíciótól kezdve*/
unsigned x, p, n;
{
return ((x >> (p + 1 - n)) & ~(~0 << n));
}

x >> (p + 1 - n) a kívánt mezQt a szó jobb szélére mozgatja. Az x argumentumot unsigned mennyiségnek deklarálva biztosítjuk, hogy a jobbra léptetéskor a felszabaduló bitek ne elQjelbitekkel, hanem nullákkal töltQdjenek fel, függetlenül attól, hogy a program éppen milyen gépen fut. ~0 csupa 1 bitet jelent, amelyet az ~0 << n utasítás segítségével n bitpozícióval balra léptetve a jobb oldali n biten csupa nullákból álló, a többi pozíción egyesekbQl álló maszk jön létre. Ezt a ~ operátorral komplementálva olyan maszk keletkezik, amelyben a jobb oldali biteken állnak egyesek.

2.5. Gyakorlat. Módosítsuk a getbits függvényt úgy, hogy a bitpozíciók sorszáma balról jobbra nQjön!

2.6. Gyakorlat. Irjunk olyan wordlength() függvényt, amely kiszámítja a befogadó gép szóhosszúságát, azaz meghatározza, hogy egy int mennyiségben hány bit van! A függvény legyen gépfüggetlen, vagyis a forráskód minden gépen mqködjön!

2.7. Gyakorlat. Irjunk olyan rightrot(n, b) függvényt, amely b számú bitpozícióval jobbra történQ bitrotációt végez az n egész típusú mennyiségen!

2.8. Gyakorlat. Irjunk olyan invert(x, p, n) függvényt, amely az x-ben a p pozíciótól kezdve n bitet invertál(vagyis az 1-eseket 0-ra, a 0-kat 1-esekre cseréli fel), miközben a többi bit változatlan marad!

2.10. Értékadó operátorok és kifejezések

Az olyan kifejezések, mint

i = i + 2

amelyekben a bal oldal a jobb oldalon megismétlQdik, a += értékadó operátor segítségével az

i += 2

tömörített alakban is írhatók.
A C-ben a legtöbb kétoperandusú operátornak megvan az op= alakú értékadó megfelelQje, ahol op a

+ - * / % << >> & |

szimbólumok egyike. Ha e1 és e2 kifejezés, akkor

e1 op= e2

jelentése:

e1 = (e1) op (e2).

Az egyetlen eltérés, hogy az elQbbi esetben a gép e1-et csak egyszer számítja ki. Ügyeljünk az e2 körüli zárójelekre:

x *= y + 1

jelentése

x = x * (y + 1)

nem pedig

x = x * y + 1

Az alábbi példában a bitcount függvény megszámlálja az egész típusú argumentumában található 1 -es bitek számát.

bitcount (n) /*1-es bitek megszámlálása n-ben*/
unsigned n;
{
int b;
for (b = 0; n != 0; n >>= 1)
if (n & 01)
b++;
return (b);
}

Tömörségük mellett az értékadó operátoroknak elQnye az is, hogy jobban megfelelnek az emberi gondolkodásmódnak. Azt mondjuk: "adj 2-t i-hez" vagy "növeld i-t 2-vel" (tehát: i += 2), nem pedig: "vedd i-t, adj hozzá 2-t majd tedd vissza az eredményt i-be" (i = i + 2). Bonyolult kifejezésekben mint

yyval [yypv [p3 + p4] + yypv [p1 + p2]] += 2

az értékadó_operátor érthetQbbé teszi a kódot, mivel az olvasónak nem kell körülményesen ellenQriznie, hogy két hosszú kifejezés tényleg megegyezik-e; ha pedig nem egyezik meg, nem kell azon tqnQdnie, hogy miért nem. Ezenkívül az értékadó operátor még a fordítónak is segíthet a hatékonyabb kód elQállításában. Korábban már kihasználtuk azt a tényt, hogy az értékadó utasításnak értéke van és kifejezésekben is elQfordulhat; a legközönségesebb példa:

while ((c = getchar()) != EOF)
. . .

Ugyanúgy, a többi értékadó operátort használó értékadások is szerepelhetnek kifejezésekben, bár ezek ritkábban fordulnak elQ.
Az értékadó kifejezés típusa megegyezik bal oldali operandusának típusával.

2.9. Gyakorlat. 2-es komplemensq aritmetikában x & (x-1 ) törli x legjobboldalibb 1-es bitjét. (Miért?) Kihasználva ezt a megfigyelést, írjuk meg a bitcount egy gyorsabb változatát!

2.11. Feltételes kifejezések

Az

if (a > b)
z = a;
else
z = b;

feltételes utasítás eredményeként z a és b közül a nagyobbik értékét veszi fel.
A C-ben a háromoperandusú ?: operátor segítségével az ilyen szerkezeteket sokkal rövidebben leírhatjuk. Legyen e1, e2, e3 három kifejezés. Az

e1 ? e2 : e3

feltételes kifejezésben a gép elQször e1-et értékeli ki. Ha értéke nem nulla (igaz), akkor e2, egyébként e3 kiértékelése következik, és a kapott érték lesz a feltételes kifejezés értéke. A program e2 és e3 közül tehát csak az egyiket értékeli ki. îgy z-be a és b közül a nagyobbat az alábbi feltételes kifejezéssel tölthetjük:

z = (a > b) ? a : b; _/* z = max(a,b) */

Megjegyezzük, hogy a feltételes kifejezés is igazi kifejezés, és ugyanúgy használható, mint bármilyen más kifejezés. Ha e2 és e3 különbözQ típusú, az eredmény típusát a fejezetünkben korábban ismertetett konverziós szabályok határozzák meg. Ha például f float és n int, akkor az

(n > 0) ? f : n

kifejezés double lesz, függetlenül attól, hogy n pozitív-e vagy sem.
A feltételes kifejezésben az elsQ kifejezést nem kötelezQ zárójelbe tenni, mivel ?: precedenciája igen alacsony (pontosan az értékadás fölötti). Zárójelezéssel azonban érthetQbbé tehetjük a kifejezés feltételrészét.
A feltételes kifejezések használata gyakran tömör és világos kódot eredményez. Az alábbi ciklus például soronként tízesével kinyomtatja egy tömb N elemét oly módon, hogy az egyes oszlopokat egy-egy szóköz választja el, és minden sort (az utolsót is beleértve) pontosan egy újsor karakter zár le.

for (i = 0; i < N; i++)
printf ("%6d %c", a[i],
(i % 10_== 9 || i == n - 1)_? '\n' : ' ');

Minden tizedik és az N-edik elem után egy újsor karaktert ad ki a program. Minden más elemet egy-egy szóköz követ. Gyakorlásképpen próbálja meg az olvasó ugyanezt feltételes kifejezés használata nélkül leírni!

2.10. Gyakorlat. îrjuk át a lower függvényt, amely a nagybetqs karaktereket kisbetqsekké konvertálja! Az if-else helyett használjunk feltételes kifejezést!

2.12. Precedencia; a kiértékelés sorrendje

A következQ táblázat összefoglalja valamennyi operátor precedencia- és kötési szabályait, azokét is, amelyekrQl idáig nem volt szó. Az egy sorba írt operátorok precedenciája azonos; a táblázatban lefelé haladva a precedencia csökken, így például * , / és % precedenciája azonos és magasabb + és - precedenciájánál.

Operátor Asszociativitás
() [] balról jobbra
! ~ ++ -- - (tipus) * & . -> sizeof jobbról balra
* / % balról jobbra
+ - balról jobbra
<< >> balról jobbra
< <= > >= balról jobbra
== != balról jobbra
& balról jobbra
^ balról jobbra
| balról jobbra
&& balról jobbra
|| balról jobbra
? : jobbról balra
= += -= stb. jobbról balra
, (3. fejezet) balról jobbra

A -> és . operátorok segítségével struktúrák elemeihez férhetünk hozzá, ezekkel, valamint a sizeof (objektum mérete) operátorral a 6. fejezetben foglalkozunk. A * (indirekció) és az & (valaminek a címe) operátorral az 5. fejezetben találkozunk.
Ügyeljünk arra, hogy az &, ^ és | bitenkénti logikai operátorok precedenciája kisebb, mint az == és != precedenciája. Emiatt az olyan bitvizsgáló kifejezések, mint

if ((x & MASK) == 0)
. . .

a zárójelezés nélkül nem mqködnek helyesen.
Mint említettük, az asszociatív és kommutatív operátorokkal (*, +, &, ^, |) felépített kifejezéseket a fordítóprogram átrendezheti, még akkor is, ha zárójele(ket)t tartalmaznak. Az esetek többségében ennek nincs jelentQsége; azokban az esetekben, ahol mégis van, explicit ideiglenes változók használatával gondoskodhatunk a kívánt kiértékelési sorrendrQl.
A legtöbb nyelvhez hasonlóan a C sem határozza meg egy-egy operátor operandusainak kiértékelési sorrendjét. Az

x = f () + g ();

utasításban pl. nem tudjuk, hogy f-et g elQtt vagy g után számítja ki a gép. îgy, ha akár f, akár g olyan külsQ változót módosít, amelytQl a másik függ, x értéke függhet a mqveletek végrehajtásának sorrendjétQl. Ha adott sorrendre van szükségünk, ezt megint csak úgy biztosíthatjuk, hogy a részeredményeket ideiglenes változókban tároljuk.
Ugyancsak határozatlan a függvényargumentumok kiértékelési sorrendje, így a

printf ("%d %d \n", ++n, power (2,n)); /* ROSSZ */

utasítás különbözQ gépeken különbözQ eredményeket adhat (és ad is) attól függQen, hogy a gép n-et a power hívása elQtt vagy után inkrementálja. A helyes megoldás természetesen:

++n;
printf ("%d %d \n", n, power(2,n));

A függvényhívások, egymásba skatulyázott értékadó utasítások, az inkrementáló és dekrementáló operátorok mellékhatásokat okozhatnak. Ez azt jelenti, hogy egy kifejezés kiszámításának - nem szándékos - melléktermékeként megváltozhat egy változó értéke. A mellékhatásokkal járó kifejezésekben sok függhet attól, milyen sorrendben tárolja a gép a kifejezésben szereplQ változókat. Szerencsétlen, de elég gyakori esetet példáz az

a[i] = i++;

utasítás. Kérdés, hogy az index i régi vagy új értékével azonos. A válasz különbözQ lehet, aszerint, hogy a fordító hogyan értelmezi, kezeli ezt az utasítást. Mindig a fordító dönti el tehát, lesz-e mellékhatás (módosul-e a változók értéke) vagy sem, hiszen az optimális sorrend erQsen függ a gép architektúrájától.
A tanulság: egy nyelven sem szabad olyan programot írni, amelynek eredménye függ a konkrét kiértékelési sorrendtQl! Természetesen jó, ha tudjuk, mire vigyázzunk, ugyanakkor, ha nem tudjuk, hogy valami hogyan mqködik különbözQ gépeken, ez a tudatlanság meg is védhet bennünket. (A lint nevq C helyességvizsgáló program a legtöbb esetben felfedezi a kiértékelési sorrendtQl való függést.)

3. fejezet: Vezérlési szerkezetek

A nyelv vezérlésátadó utasításai a számítások végrehajtásának sorrendjét határozzák meg. A korábbi példákban már találkoztunk a C leggyakoribb vezérlésátadó utasításaival. Ebben a fejezetben teljessé tesszük a készletet és részletesen ismertetjük a már korábban említett utasításokat is.

3.1. Utasítások és blokkok

A kifejezések, pl. x = 0, i++ vagy printf(. . .) utasítássá válnak, ha pontosvesszQ követi Qket:

x = 0;
i++;
printf (. . .);

A C-ben a pontosvesszQ utasításlezáró jel (terminátor) és nem elválasztó szimbólum, mint az ALGOL-szerq nyelvekben.
A { és } kapcsos zárójelek felhasználásával deklarációkat és utasításokat egyetlen összetett utasításba vagy blokkba foghatunk össze. Ez szintaktikailag egyetlen utasítással egyenértékq. Nyilvánvaló példái ennek a függvények utasításait határoló kapcsos zárójelek, vagy azok a zárójelek, amelyek egy if, else, while vagy for szimbólumot követQ utasítássort vesznek körül. (Változók bármely blokkon belül deklarálhatók, errQl a 4. fejezetben lesz szó.) A blokkot lezáró jobb oldali kapcsos zárójel után soha nincs pontosvesszQ.

3.2. Az if-else utasítás

Az if-else utasítással döntést, választást írunk le. Az utasítás szintaxisa formálisan :

if (kifejezés)
1.utasítás
else
2.utasítás

ahol az else rész nem kötelezQ. A gép a kifejezés kiértékelése után, ha annak értéke igaz (vagyis nemnulla), az 1. utasítást, ha értéke hamis (nulla), és ha van else rész, akkor a 2. utasítást hajtja végre.
Mivel az if egyszerqen a kifejezés numerikus értékét vizsgálja, lehetQség van bizonyos programozási rövidítésre. A legnyilvánvalóbb, ha

if (kifejezés)

-t írunk

if (kifejezés != 0)

helyett. Ez néha természetes és világos, máskor viszont nehezen megfejthetQ.
Minthogy az if-else konstrukció else része elhagyható, sokszor nem egyértelmq, hogy az egymásba skatulyázott if utasítások melyikéhez tartozik else ág. A kétértelmqséget a C más nyelvekhez hasonlóan azzal oldja fel, hogy az else a hozzá legközelebbi else nélküli if-hez kapcsolódik. Például az

if (n > 0)
if (a > b)
z = a;
else
z = b;

esetben az else a belsQ if hez tartozik, amint azt a sorbetolással szemléltettük. Ha nem ezt akarjuk, zárójelekkel érhetjük el a helyes összerendelést:

if (n > 0) {
if (a > b)
z = a;
}
else
z = b;

A kétértelmqség különösen veszélyes az olyan esetekben, mint:

if (n > 0)
for (i = 0; i < n; i++)
if (s[i] > 0 ) {
printf (". . .");
return (i);
}
else /*ROSSZ*/
printf("hiba, n értéke nulla \n");

A sorbetolás ugyan félreérthetetlenül mutatja, hogy mit akarunk, de ezt a számítógép nem érzékeli, és az else-t a belsQ if-hez kapcsolja. Az ilyen típusú hibákat igen nehéz felfedezni.
Egyébként vegyük észre, hogy a z = a után pontosvesszQ van az

if (a > b ) ;
z = a;
else
z = b;

programrészben. Ennek az az oka, hogy nyelvtanilag egy utasítás követi az if-et, márpedig az olyan kifejezés jellegq utasításokat is, mint z = a mindig pontosvesszQ zárja le.

3.3. Az else-if utasítás

Az

if (kifejezés)
utasítás
else if (kifejezés)
utasítás
else if (kifejezés)
utasítás
else
utasítás

szerkezet olyan gyakran fordul elQ, hogy megér némi külön fejtegetést. Többszörös elágazást (döntést) általában ilyen if-sorozattal valósítunk meg. A gép sorban kiértékeli a kifejezéseket. Ha valamelyik kifejezés igaz, akkor a hozzá tartozó utasítást a gép végrehajtja, és ezzel az egész lánc lezárul. Az egyes utasítások helyén egyetlen utasítás vagy kapcsos zárójelek közé zárt utasításcsoport egyaránt állhat.
Az utolsó else a "fentiek közül egyik sem" (alapértelmezés szerinti) esetet kezeli. Ha a vezérlés ide kerül, egyetlen korábbi feltétel sem teljesült. Néha ilyenkor semmit sem kell csinálni, így a záró

else
utasítás

elhagyható, vagy - valamilyen tiltott feltétel figyelésével - hibaellenQrzésre használható.
KövetkezQ példánkban egy háromutas döntést láthat az olvasó. Olyan bináris keresQ függvényt mutatunk be, amely egy rendezett v tömbben egy bizonyos x értéket keres. v elemeinek növekvQ sorrendben kell követniük egymást. Ha x elQfordul v-ben, akkor a függvény x v-beli (0 és n-1 közötti) sorszámát szolgáltatja, ellenkezQ esetben értéke -1 lesz:

binary (x, v, n) /*x keresése v[0] . . .
v[n - 1]-ben*/
int x, v[], n;
{
int low, high, mid;
low = 0;
high = n - 1;
while (low <= high) {
mid = (low + high) / 2;
if (x < v[mid])
high = mid - 1;
else if (x > v[mid])
low = mid + 1;
else / *Megtalálta*/
return (mid);
}
return (-1);
}

Minden lépésben meg kell vizsgálni, hogy x kisebb, mint a v[mid] középsQ elem, nagyobb nála vagy egyenlQ vele, ami egészen természetes módon írható le else-if szerkezettel.

3.4. A switch utasítás

A switch utasítás a többirányú programelágaztatás egyik eszköze. Megvizsgálja, hogy valamely kifejezés értéke megegyezik-e több állandó érték valamelyikével, és ennek megfelelQ ugrást hajt végre. Az 1. fejezetben olyan programot láttunk, amellyel az egyes számjegyek, üres és egyéb karakterek elQfordulásait számláltuk meg. Ugyanazt a programot most az if ... else if... ...else szerkezet helyett a switch utasítással írtuk meg:

main () /*Számjegyek, üres és egyéb karakterek
számlálása*/
{
int c, i, nwhite, nother, ndigit[10];
nwhite = nother = 0;
for (i = 0; i < 10; i++)
ndigit [i] = 0;
while ((c = getchar()) != EOF)
switch (c) {
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
ndigit [c - '0'] ++;
break;
case ' ':
case '\n':
case '\t':
nwhite++;
break;
default :
nother++;
break;
}
printf ("számjegyek=");
for (i = 0; i < 10; i++)
printf ("%d", ndigit[i]);
printf ("\n üres hely = %d, egyéb = %d \n",
nwhite, nother);
}

A switch kiértékeli a zárójelek közötti kifejezést (ebben a programban ez a c karakter), és összehasonlítja az összes case (eset) értékével. Minden case-t egész értékkel, karakterállandóval vagy állandó kifejezéssel meg kell cimkézni. Ha valamelyik case azonos a kifejezés értékével, a végrehajtás ennél a case-nél kezdQdik. A default cimkéjq case-re akkor kerül a vezérlés, ha a többi case egyike sem teljesül. A default elhagyható : ha nem szerepel és a case-ek egyike sem teljesül, semmi nem történik. A case-ek és a default tetszQleges sorrendben követhetik egymást. A case utasítások címkéinek különbözniük kell egymástól.
A break utasítás hatására a vezérlés azonnal kilép a switch-bQl. Mivel a case-ek címkeként mqködnek, miután valamelyik case-hez tartozó programrész végrehajtása befejezQdött, a vezérlés a következQ case-re kerül, hacsak explicit módon nem intézkedünk a kilépésrQl. A switch-bQl való kilépés legközönségesebb módja a break és a return. Ugyancsak break utasítással lehet kilépni a while, for és do ciklusokból, errQl e fejezet késQbbi részében lesz szó.
Az egymást követQ case-ekbe való belépés nem egyértelmqen elQnyös. A dolog pozitív oldala, hogy mint példánkban a szóköznél, az újsor és a tab karakternél is láttuk, egyetlen tevékenység számára több esetet enged meg. De ebbQl az is következik, hogy általában minden case-t break-nek kell lezárnia, nehogy a vezérlés a következQ case-re lépjen. A case-ken történQ lépkedés azért is veszélyes, mert a vezérlés széteshet, ha a programot módosítjuk. Azokat az eseteket kivéve, amikor ugyanahhoz a számításhoz több címke tartozik, a case-ek közötti átmenetek használatával célszerq takarékoskodni.
A jó külalak érdekében még akkor is helyezzünk el break-et az utolsó eset után (az elQzQ példánkban a default után), ha az logikailag szükségtelennek látszik. Ha valamikor késQbb a szekvencia végéhez újabb case-t illesztünk, ez a fajta defenzív programozási taktika fog megmenteni minket.

3.1. Gyakorlat. Irjuk meg azt az expand(s, t) függvényt, amely - miközben az s karakterláncot a t karakterláncba másolja - a láthatatlan karaktereket (pl.újsor és a tab) látható escape szekvenciákká (pl.\n és\t) alakítja át! Használjunk switch utasítást!

3.5. A while és a for utasítás

Már találkoztunk a while és for ciklusokkal. A

while (kifejezés)
utasítás

szerkezetben a gép kiértékeli a kifejezést. Ha értéke nem nulla, akkor végrehajtja az utasítást és ismét kiértékeli a kifejezést. Ez a ciklus mindaddig folytatódik, amíg a kifejezés 0 nem lesz, amikor is az utasítás után a végrehajtás végetér. A

for (kif1; kif2; kif3)
utasítás

alakú for utasítás egyenértékq a

kif1;
while (kif2) {
utasítás
kif3;
}

alakkal. Nyelvtanilag a for mindhárom összetevQje kifejezés. Többnyire kif1 és kif3 értékadás vagy függvényhívás, kif2 pedig relációs kifejezés. A három kifejezés bármelyike elhagyható, de a pontosvesszQknek meg kell maradniuk. Ha kif1 vagy kif3 marad el, akkor a ; egyszerqen elmarad a kifejtésbQl. Ha a kif2 vizsgálat nem szerepel, akkor állandóan igaznak tekintjük, így

for( ; ; ) {
. . .
}

végtelen ciklus, amelybQl valószínqleg más módon kell kiugrani (pl. return vagy break révén).
A while és a for között lényegében ízlésünk szerint választhatunk. Például a

while ((c = getchar()) == ' ' || c == '\n' || c == '\t')
; /*9tugorja a láthatatlan karaktereket*/

programrészben nincs inicializálás, sem újrainicializálás, így a while használata a lehetQ legtermészetesebbnek tqnik.
A for nyilvánvalóan elQnyösebb olyankor, amikor egyszerq inicializálás és újrainicializálás fordul elQ, mivel a ciklust vezérlQ utasítások egymás közelében, a ciklus tetején jelennek meg. Ez a legszembetqnQbb a

for (i = 0; i < N; i++)

esetben, amely egy tömb elsQ N eleme feldolgozásának C nyelvq megfogalmazása, a FORTRAN és PL/1 DO ciklusának megfelelQje. Az analógia azonban nem teljes, mivel a for határai a cikluson belülrQl változtathatók, és az i vezérlQváltozó megtartja értékét, amikor valamilyen oknál fogva a ciklus végetér. Minthogy a for összetevQi tetszQleges kifejezések, a for ciklus nem korlátozódik aritmetikai léptetésekre. Stiláris szempontból mégis helyesebb, ha a for-ban nem helyezünk el tQle független számításokat; a for-t inkább ciklusvezérlQ mqveletekre tartsuk fenn.
Nagyobb példaként bemutatjuk az atoi függvény másik változatát. Az atoi függvény egy karakterláncot a neki megfelelQ numerikus értékké alakít át. Az itt következQ változat a korábbinál általánosabb: kezeli az esetleges vezetQ szóközöket és az esetleges - vagy + elQjelet. (A 4. fejezet tartalmazza az atof függvényt, amely ugyanezt a konverziót lebegQpontos számokra végzi el. )
A program alapstruktúrája a bemenet alakját tükrözi:

ugord át az üres közöket, ha vannak
olvasd be az elQjelet, ha van
olvasd be az egész részt és konvertáld

Minden lépés elvégzi a maga feladatát, és a dolgokat tiszta állapotban adja át a következQ lépésnek. Az egész folyamat az elsQ olyan karakter elQfordulásakor ér véget, amely nem lehet része számnak.

atoi (s) /*s konvertálása egésszé*/
char s [];
{
int i, n, sign;
for (i = 0; s [i] == ' ' || s [i] == '\n'
|| s [i] == '\t'; i++)
; /*Ugord át az üres helyet*/
sign = 1;
if (s [i] == '+' || s [i] == '-') /*ElQjelvizsgálat*/
sign = (s [i++] == '+') ? 1 : -1;
for (n = 0; s [i] >= '0' && s [i] <= '9'; i++)
n = 10 * n + s [i] - '0';
return (sign * n);
}

A ciklusvezérlés tömöritésének elQnyei még jobban kiütköznek, ha több, egymásba skatulyázott hurok van. A következQ függvény az UNIX Shell sort funkcióját valósitja meg:feladata egy egész tipusú tömb rendezése. A Shell sort alapgondolata, hogy kezdetben inkább az egymástol távoli elemek kerüljenek összehasonlításra, nem pedig szomszédosak, mint az egyszerq cserélgetQs rendezQprogramokban. Ezáltal a nagyfokú kezdeti rendezetlenség várhatóan gyorsan eltqnik, így a késQbbi lépéseknek kevesebb dolga akad. Az összehasonlított elemek közötti távolság fokozatosan 1-re csökken, amikor is a rendezés szomszédcserélgetési módszerré alakul át.

shell (v,n) /*v[0]...v[n-1]-et növekvQ sorba rendezi*/
int v[], n;
{
int gap, i , j, temp;
for (gap = n / 2; gap > 0; gap /= 2)
for (i = gap; i < n; i++)
for (j = i - gap; j >= 0
&& v [j] > v[j + gap]; j -= gap) {
temp = v [j];
v [j] = v [j + gap];
v [j + gap] = temp;
}
}

Három egymásba skatulyázott ciklus van. A legkülsQ ciklus az összehasonlított elemek közötti távolságot vezérli, amit n/2-rQl minden ciklusban felére csökkent, amíg a távolság 0 nem lesz. A középsQ ciklus minden olyan elempárt összehasonlít, amelyek egymástól gap-nyire vannak. A legbelsQ ciklus minden, nem megfelelQ sorrendben levQ összehasonlított elempárt megfordít. Mivel gap az utolsó ciklusban 1-re csökken, végül minden elem helyes sorrendbe rendezQdik. Vegyük észre, hogy a for utasítás általános jellegénél fogva a külsQ ciklus ugyanolyan alakú, mint a többi, bár nem végez aritmetikai léptetést.
Az egyik utolsó C operátor a "," (vesszQ), amelyet legtöbbször a for utasításban használunk. A vesszQvel elválasztott kifelyezéspárok kiértékelése balról jobbra történik, és az eredmény típusa, ill. értéke megegyezik a jobb oldali operandus típusával, ill. értékével. îgy a for utasítás egyes részeiben több kifejezést is elhelyezhetünk például azért, hogy párhuzamosan két indexet dolgozzunk fel. Ezt mutatjuk be a reverse(s) függvényben, amely az s karakterláncot helyben megfordítja:

reverse (s) /*Az s karakterlánc helyben megfordítása*/
char s [];
{
int c, i, j;
for (i = 0 , j = strlen (s) - 1; i < j; i++ , j--) {
c = s [i];
s [i] = s [j];
s [j] = c;
}
}

A függvényargumentumokat, a deklarációkban elQforduló változókat stb. elválasztó vesszQk nem vesszQoperátorok, és nem garantálják a balról jobbra történQ kiértékelést.

3.2. Gyakorlat. îrjuk meg az expand(s1, s2) függvényt, amely az s1 karakterláncban található rövidítéseket s2-ben teljes listává bQvíti ki (pl. a-z helyett abc. . .xyz-t ír)! Engedjük meg a kis- és a nagybetqket, ill. a számjegyeket is, és készüljünk fel az olyan esetek kezelésére is, mint a-b-c és a-z 0-9 és -a-z! (Hasznos megállapodás, ha a vezetQ vagy záró - karaktert betq szerint vesszük.)

3.6. A do-while utasítás

Mint az 1. fejezetben mondottuk, mind a while, mind a for ciklus rendelkezik azzal a kívánatos tulajdonsággal, hogy a kiugrási feltétel teljesülését nem a ciklus végén, hanem a ciklus elején vizsgálja. A harmadik C-beli ciklusfajta, a do-while a vizsgálatot a ciklus végén, a ciklustörzs végrehajtása után végzi el; a törzs tehát legalább egyszer mindenképpen végrehajtódik. A szintaxis:

do
utasítás
while (kifejezés);

A gép elQbb végrehajtja az utasítást, majd kiértékeli a kifelyezést. Ha az értéke igaz, ismét végrehajtja az utasítást, és így tovább. Ha a kifelyezés értéke hamissá válik, a ciklus végetér.
Mint várható, a do-while-t sokkal ritkábban szokás használni, mint a while-t és a for-t, talán az összes ciklusok öt százalékában. IdQnként azonban mégiscsak érdemes elQvenni, mint például az itt következQ itoa függvényben, amely egy számot karakterlánccá alakít át (atoi inverze).A feladat kicsit bonyolultabb, mint gondolnánk, mivel az egyszerq számjegygeneráló módszerek a számjegyeket rossz sorrendben hozzák létre.ogy döntöttünk, hogy a karakterláncot visszafelé generáljuk, majd megfordítjuk.

itoa (n,s) /*n karakterré konvertálása s-be*/
char s [];
int n;
{
int i, sign;
if ((sign = n) < 0) /*elQjelvizsgálat és tárolás*/
n = -n; /*n pozitív legyen*/
i = 0;
do { /*számjegyek generálása fordított
sorrendben */
s [i++] = n % 10 + '0'; /*megkapja a következQ
számjegyet*/
} while ((n /= 10) > 0); /*törli*/
if (sign < 0)
s [i++] = '-';
s [i] = '\0';
reverse (s);
}

Példánkban a do-while használata tényleg kényelmes, mivel n értékétQl függetlenül legalább egy karaktert el kell helyezni az s tömbben. A do-while törzsét alkotó egyetlen utasítást - bár itt szükségtelen - kapcsos zárójelek közé zártuk, hogy a sietQs olvasó se higgye azt, hogy a while egy while ciklus kezdete.

3.3. Gyakorlat. Kettes komplemensq számábrázolásban az itoa függmény általunk írt változata nem kezeli a legnagyobb negatív számot, tehát a (2 szóméret-1) értékq n-et. Magyarázzuk meg, hogy miért! Módosítsuk úgy a programot, hogy ezt az értéket is helyesen írja ki, függetlenül attól, hogy milyen gépen fut!

3.4. Gyakorlat. îrjuk meg azt a hasonló itob (n,s) függvényt, amely az n unsigned egész számot bináris karakterábrázolásban az s karakterláncba konvertálja! îrjuk meg az itoh függvényt is, amely egy egész számot hexadecimális ábrázolásmódba alakít át!

3.5. Gyakorlat. îrjuk meg az itoa függvénynek azt a változatát, amely kettQ helyett három argumentumot fogad! A harmadik argumentum a minimális mezQszélesség; az átkonvertált számot szükség esetén balról üres közökkel kell kitölteni, hogy elég széles legyen.

3.7. A break utasítás

Néha kényelmes, ha a ciklusból való kilépést nem a ciklus elején vagy végén való feltételvizsgálattal vezéreljük. A break utasítással a vizsgálat elQtt is ki lehet ugrani a for, while és do ciklusokból, csakúgy, mint a switch-bQl. A break utasítás hatására a vezérlés azonnal kilép a legbelsQ zárt ciklusból utasítás hatására a vezérlés azonnal kilép a legbelsQ zárt ciklusból (vagy switch-bQl).
A következQ program az összes sor végérQl eltávolítja a szóközöket és tab karaktereket oly módon, hogy break utasítás segítségével kilép a ciklusból, amikor a legjobboldalibb nem - szóköz és nem - tab karaktert megtalálja.

#define MAXLINE 1000
main () /*Sorvégi szóközök és tabok eltávolítása*/
{
int n;
char line [MAXLINE];
while ((n = getline (line,MAXLINE)) > 0) {
while (--n >= 0)
if (line [n] != ' ' && line [n] != '\t'
&& line [n] != '\n')
break;
line [n + 1] = '\0';
printf_("%s \n", line);
}
}

A getline a sor hosszát adja vissza. A belsQ while ciklus a line utolsó karakterén kezdQdik (ne felejtsük el, hogy --n elQbb dekrementálja n-et és csak azután használja az értékét), és visszafelé haladva keresi az elsQ olyan karaktert, amely nem szóköz, tab vagy újsor. Ha ilyen karaktert talál, vagy ha n negatívvá válik (vagyis, ha az egész sort megvizsgálta), akkor a ciklus megszakad. Igazolja az olvasó, hogy ez akkor is helyes mqködés, ha az egész sor csupa üres helyeket megjelenítQ karakterekbQl áll!
A break alkalmazása helyett választhatjuk azt a megoldást is, hogy a vizsgálatot magába a ciklusba tesszük:

while ((n = getline (line,MAXLINE)) > 0) {
while (--n >= 0 && (line [n] == ' '
|| line [n] == '\t' || line [n] == '\n'))
;
. . .
}

Ez a változat gyengébb, mint az elQzQ, mivel a vizsgálat nehezebben érthetQ. Általában kerüljük az olyan vizsgálatokat, amelyekben keverednek az &&, ||, ! szimbólumok és a zárójelek.

3.8. A continue utasítás

A continue utasítás a break-hez kapcsolódik, de a break-nél ritkábban használjuk; a continue-t tartalmazó ciklus (for, while, do) következQ iterációjának megkezdését idézi elQ. A while és a do esetében ez azt jelenti, hogy azonnal végrehajtódik a feltételvizsgálat, a for esetében pedig a vezérlés azonnal az újrainicializálási lépésre kerül. (A continue csak ciklusokra alkalmazható, switch-re nem. Az olyan, switch-en belüli continue, ahol a switch egy cikluson belül van, a következQ ciklusiteráció végrehajtását váltja ki.)
Például a következQ programrész az a tömbnek csak a pozitív elemeit dolgozza fel; a negatív értékeket átugorja:

for (i = 0; i < n; i++) {
if (a [i] < 0) /*Ugord át a negatív elemeket*/
continue;
. . . /*Dolgozd fel a pozitív elemeket*/
}

A continue utasítást gyakran használjuk olyan esetekben, amikor a ciklus további része nagyon bonyolult és ezért a feltételvizsgálat megfordítása és egy újabb programszint (sorbetolás) túl mélyen skatulyázná a programot.

3.6. Gyakorlat. îrjunk olyan programot, amely a bemenetét a kimenetére másolja, de ha a bemenetre egymás után többször érkezik ugyanaz a sor, azt csak egyszer nyomtatja ki! (Ez egyszerq változata az UNIX uniq szolgáltatásának.)

3.9. A goto utasítás; címkék

A C-ben is használhatjuk a sokat szidott goto utasítást, ugrathatunk címkékre. Elméletileg a goto-ra sohasincs szükség, és gyakorlatilag majdnem mindig egyszerqen programozhatunk nélküle is. Ebben a könyvben nem használtunk goto-t.
Mindazonáltal bemutatunk néhány olyan esetet, ahol a goto-knak meg lehet a maguk helye. A leggyakoribb eset, amikor a feldolgozást valamilyen mélyen skatulyázott szerkezet belsejében akarjuk abbahagyni oly módon, hogy egyszerre két, egymásba ágyazott ciklusból lépünk ki. A break utasítást közvetlenül nem használhatjuk, mivel az egyszerre csak a legbelsQ ciklusból ugratja ki a vezérlést. îgy például :

for ( . . . )
for ( . . . ) {
. . .
if (zavar)
goto hiba;
. . . }
hiba:
számold fel a zavart

Ez a fajta szervezés célszerq, ha a hibakezelQ program nem triviális és ha a hibák különbözQ helyeken fordulhatnak elQ. A címkék alakja ugyanaz, mint a változóneveké, csak kettQspont követi Qket. Ugyanazon a függvényen belül, mint ahol a goto elQfordul, bármelyik utasítást megcímkézhetjük.
Másik példaként tekintsük azt a problémát, amikor egy kétdimenziós tömb elsQ negatív elemét akarjuk megtalálni. (A többdimenziós tömbökrQl az 5. fejezetben lesz szó.) Az egyik lehetQség:

for (i = 0; i < n; i++)
for (j = 0; j < m; j ++)
if (v [i][j] < 0)
goto found;
/*Nem talált*/
. . .
found:
/*Az i, j pozíción megtalálta*/
. . .

Bármely goto-t tartalmazó program megírható goto nélkül, de esetleg csak megismételt vizsgálatok vagy külön bevezetett változó árán. Például a tömbben való keresés goto nélkül :

found = 0;
for (i = 0; i < N && !found; i++)
for (j = 0;j < M && !found; j++)
found = v[i][j] < 0;
if (found)
/*i-1, j-1-nél volt*/
. . .
else
/*Nem talált*/
. . .

Bár nem kívánunk az ügyben dogmatikusak lenni, kimondjuk : minél kevesebbet használjuk a goto-t, annál jobb.

4. fejezet: Függvények és programstruktúra

A függvények a nagy számítási feladatokat kisebbekre bontják. îgy a programozó építhet arra, amit mások már megcsináltak, és nem kell mindent elölrQl kezdenie. A jól megírt függvények gyakran elrejtik a mqveletek részleteit a program azon részei elQl, amelyeknek nem is kell tudniuk róluk. Ezáltal az egész program világosabbá válik, és a változtatások is könnyebben elvégezhetQk.
A C nyelvet úgy tervezték meg, hogy a függvények hatékonyak és könnyen használhatók legyenek. A C programok általában sok kis méretq függvényt tartalmaznak. Egy program több forrásállományra is tagolódhat. Az állományok külön-külön is fordíthatók, és a könyvtárakban található, már korábban lefordított függvényekkel együtt betölthetQk. Ezt a folyamatot most nem tárgyaljuk, mivel a részletek a helyi operációs rendszertQl függenek.
A legtöbb programozó már ismeri a be- és kivitel céljára szolgáló könyvtári függvényeket (getchar, putchar) és a numerikus számítások könyvtári függvényeit (sin, cos, sqrt). Ebben a fejezetben részletesebben szólunk arról, hogyan írhatunk új függvényeket.


4.1. Alapfogalmak

Kezdetként tervezzünk és írjunk olyan programot, amely a bemenetének minden olyan sorát kinyomtatja, amely adott karakterláncból álló mintát tartalmaz! (Ez speciális esete az UNIX grep segédprogramjának.) Például a "the" minta keresése a

Now is the time /Ideje, hogy
for all good minden jó
men to come to the aid ember segítségére siessen
of their party. embertársainak./
sorokban a
Now is the time
men to come to the aid
of their party.

kimeneti szöveget fogja eredményezni. A feladat alapstruktúrája könnyen felbontható három különálló részre:

while (van még sor)
if (a sor tartalmazza a mintát)
nyomtatás

Bár nyilván elhelyezhetjük az egész programkódot a fQ rutinban, mégis az a jobb megoldás, hogy kihasználjuk az elQzQ természetes struktúrát és minden részbQl egy-egy külön függvényt készítünk. Három kis program könnyebben kezelhetQ, mint egy nagy, mivel az egymásra nem tartozó részletek a függvényekbe rejthetQk és a nem kívánatos kölcsönhatások lehetQsége minimális lesz. Mi több, az egyes részek a késQbbiekben önmagukban is hasznosak lehetnek.
A while (van még sor) feladatot az 1. fejezetben írt getline függvény, a nyomtatás feladatát pedig a szabványos könyvtárban rendelkezésünkre álló printf függvény végzi el. Eszerint csupán azt a rutint kell megírnunk, amely eldönti, hogy a sor tartalmazza-e a kérdéses mintát. A probléma megoldásának tervét a PL/1-bQl "lophatjuk el": az index(s, t) függvény azt az s karakterláncbeli pozíciót vagy indexet adja vissza, ahol a t karakterlánc kezdQdik, vagy pedig -1-gyel tér vissza, ha s nem tartalmazza t-t. s-beli kezdQpozícióként 0-t használunk, nem 1-et, mivel a tömbök a C nyelvben a 0 indexszel kezdQdnek. Ha a késQbbiekben bonyolultabb minta-összehasonlítási feladatot akarunk megoldani, csak az index függvényt kell kicserélnünk; a programkód többi része változatlan marad.
Ennyi tervezés után már gyorsan megírhatjuk a programot. Jól látható, hogyan illeszkednek egymáshoz az egyes részek. Ne dolgozzunk a legáltalánosabb esettel: a keresett minta egyelQre legyen csupa betqbQl álló karakterlánc. Nemsokára szó lesz a karaktertömbök inicializálásáról, és az 5. fejezetben megmutatjuk, hogyan tehetjük a mintát olyan paraméterré, amelyet a program futása során állítunk be. Példánk egyben a getline függvény újabb változata is: tanulságos lesz, ha összehasonlítjuk az 1. fejezetbeli változattal!

#define MAXLINE 1000
main () /* Adott mintára illeszkedQ összes sor megkeresése*/
{
char line [MAXLINE];
while (getline (line, MAXLINE) > 0)
if (index (line, "the") >= 0)
printf("%s", line);
}

getline (s, lim) /*Sor beolvasása s-be,
visszatérési érték a
sorhosszúság*/
char s [];
int lim;
{
int c, i;
i = 0;
while (--lim > 0 && (c = getchar ()) != EOF && c != '\n')
s [i++] = c;
if (c == '\n')
s [i++] = c;
s [i] = '\0';
return (i);
}

index (s, t) /*Visszaadja t s-beli indexét;
-1 , ha t nincs s-ben*/
char s [], t [];
{
int i, j, k;
for (i = 0; s [i] != '\0'; i++) {
for (j = i , k = 0; t [k] != '\0'
&& s [j] == t [k];j++ , k++)
;
if (t [k] == '\0')
return (i);
}
return (-1);
}

Minden függvény az alábbi alakú :

név (argumentumlista, ha van)
argumentumdeklarációk, ha vannak
{
deklarációk és utasítások, ha vannak
}

Mint látható, a különféle részek hiányozhatnak; a legrövidebb függvény :

dummy () { }

ami semmit sem csinál. (Az ilyen semmit sem csináló függvény gyakran hasznos, ha a programfejlesztés során le akarjuk foglalni egy késQbb megírandó programrész helyét.) A függvénynevet típusnév is megelQzheti, amennyiben a függvény nem egész típusú értékkel tér vissza; errQl a következQ szakaszban lesz szó.
A program lényegében egyedi függvénydefiníciók halmaza. A függvények közötti kommunikáció (ebben az esetben) argumentumokkal és a függvények által visszaadott értékekkel történik, de történhet külsQ változókon keresztül is. A függvények a forrásállományon belül tetszQleges sorrendben fordulhatnak elQ, és a forrásprogram több állományra bontható, csak függvényeket nem szabad kettévágni.
A hívott függvény meghívójának a return utasítás segítségével adhat vissza értéket. A return utasítást tetszQleges kifejezés követheti:

return (kifejezés)

A hívó függvénynek jogában áll a visszaadott értéket figyelmen kívül hagyni. Nem szükséges továbbá, hogy a return után kifejezés álljon, ez esetben a hívó nem kap vissza semmit. A vezérlés akkor is érték átadása nélkül tér vissza a hívóhoz, ha a végrehajtás a függvény végén eléri a záró jobb oldali kapcsos zárójelet. Ez a megoldás megengedett, de valószínqleg valamilyen bajt jelez, ha a függvény értéket ad vissza az egyik helyrQl és nem ad értéket egy másikról. Mindenesetre az olyan függvény értéke, amely nem ad vissza értéket, bizonyosan értelmetlen (határozatlan, hulladék). Az ilyen jellegq hibákat a C nyelv lint nevq helyességvizsgáló programja jelzi.
A több állományra tagolódó C programok fordításának és betöltésének mechanizmusa rendszerrQl rendszerre változik. Az UNIX operációs rendszerben pl. az 1. fejezetben említett cc parancs végzi el ezt a feladatot. Tegyük fel, hogy a három függvény három állományban található, amelyeknek a neve main.c, getline.c és index.c. Ekkor a

cc main.c getline.c index.c

parancs lefordítja a három állományt, az eredményül kapott áthelyezhetQ formátumú tárgykódot a main.o, getline.o és index.o nevq állományokba helyezi, és betölti Qket az a.out nevq végrehajtható állományba.
Ha hiba fordul elQ, mondjuk a main.c-ben, akkor az illetQ állomány önmagában újrafordítható és az eredmény betölthetQ a korábban kapott állományokkal együtt a

cc main.c getline.o index.o

paranccsal. A cc parancs a ".c", ill. az ".o" névadási konvenciók segítségével különbözteti meg a forrásállományokat (source) a tárgykódot tartalmazó (object) állományoktól.

4.1. Gyakorlat. îrjunk egy rindex(s, t) nevq függvényt, amely t s-beli legjobboldalibb elQfordulásának pozícióját adja vissza, ill. -1-et ad, ha t nem fordul elQ s-ben!

4.2. Nemegész típusú értékekkel visszatérQ függvények

Idáig egyetlen programunk sem tartalmazott a függvény típusára vonatkozó deklarációt. Ennek az az oka, hogy alapértelmezés szerint a függvények implicit módon deklaráltak azáltal, hogy megjelennek valamely utasításban vagy kifejezésben, mint pl.:

while (getline (line, MAXLINE) > 0)

Ha valamely kifejezésben korábban még nem deklarált név fordul elQ, amelyet bal oldali kerek zárójel követ, akkor ezt a gép a szövegkörnyezet alapján függvénynévként deklarálja. Ezenkívül alapértelmezés szerint a függvényrQl azt feltételezzük, hogy int típusú értéket ad vissza. Mivel a char kifejezésekben int mennyiséggé alakul át, a char típussal visszatérQ függvényeket sem kell deklarálni. Ezzel az esetek többségét lefedtük, beleértve összes eddigi példánkat is.
Mi történik azonban, ha a függvénynek valamilyen más típusú értéket kell visszaadnia? Sok numerikus függvény - mint pl. az sqrt, sin és cos - double típusú értéket ad vissza; más speciális függvények más típusokat. Ezek alkalmazását az atof(s) függvénnyel szemléltetjük, amely az s karakterláncot a neki megfelelQ duplapontosságú lebegQpontos számmá alakítja. Az atof az atoi kiterjesztése, amelynek több változatát is megírtuk a 2. és 3. fejezetben. Az atof kezeli az esetleges elQjelet és tizedespontot, valamint a jelenlevQ vagy hiányzó egész, ill. tört részt. (Ez azonban nem nevezhetQ jó minQségq bemeneti konverziós rutinnak; ilyen rutin megírása több helyet igényelne, mint amit most erre a célra szánunk.)
ElQször is az atof maga kell, hogy deklarálja az általa visszaadott érték típusát, mivel az nem int. Tekintve, hogy kifejezésekben a float double mennyiséggé alakul át, nincs értelme azt mondanunk, hogy az atof float értéket ad vissza; jól kihasználhatjuk a kétszeres pontosságot, és a függvényt double értékkel visszatérQnek deklaráljuk. A típus neve megelQzi a függvény nevét:

double atof (s) /*Az s karakterlánc átalakítása double-lá*/
char s [];
{
double val, power;
int i, sign;
for (i = 0; s [i] == ' ' || s [i] == '\n'
|| s [i] == '\t'; i++)
; /* Üres hely átugrása*/
sign = 1;
if (s [i] == '+' || s [i] == '-') /*ElQjel*/
sign = (s [i++] == '+') ? 1 : -1;
for (val = 0; s [i] >= '0' && s [i] <= '9'; i++)
val = 10 * val + s [i] - '0';
if (s [i] == '.')
i++;
for (power = 1 ; s [i] >= '0' && s [i] <= '9'; i++) {
val = 10 * val + s [i] - '0';
power *= 10;
}
return (sign * val / power);
}

Másodszor is, ugyanilyen fontos, hogy a hívó rutinnak közölnie kell, hogy az atof nemegész értéket ad vissza. A deklarációt a következQ primitív kalkulátor-program mutatja (a program épphogy elegendQ pl. egy csekkönyv egyenlegének kiszámításához). A program soronként egy-egy számot olvas be, amelyet elQjel elQzhet meg, a számokat összeadja és az összeget minden beolvasás után kinyomtatja:

#define MAXLINE 100
main () /*Primitív kalkulátor*/
{
double sum, atof();
char line [MAXLINE];
sum = 0;
while (getline (line, MAXLINE) > 0)
printf ("\t %.2f \n", sum += atof (line));
}

A

double sum, atof ();

deklaráció értelmében sum double típusú változó, és atof olyan függvény, amely double értékkel tér vissza.
Amennyiben atof nincs mindkét helyen explicit módon deklarálva a C feltételezi, hogy egész típusú értékkel tér vissza, és így értelmetlen válaszokat kapunk. Ha maga az atof és main-beli hívása következetlen módon fordul elQ ugyanabban a forrásállományban, ezt a fordító észreveszi. Ha azonban az atof függvényt külön fordítottuk (ami valószínq), az eltérést a gép nem veszi észre, az atof double értéket ad vissza, amit a main int értékként kezel, és értelmetlen válaszokat kapunk. (A lint kimutatja az ilyen hibát!)
Az atof birtokában elvileg így is megírhatjuk az atoi függvényt (karakterlánc konvertálása int-té):

atoi (s) /*Az s karakterlánc átalakítása
egész számmá*/
char s [];
{
double atof ();
return (atof (s));
}

Figyeljük meg a deklarációk és a return utasítás struktúráját. A kifejezés értéke a

return (kifejezés)

-ben mindig olyan típusúvá alakul át, mint amilyen a függvény típusa, még mielQtt a hívóhoz való visszatérés megtörténne. îgy atof értéke, ami double, automatikusan int típusúvá alakul át a return-ben való megjelenéskor, mivel az atoi függvény int értékkel tér vissza. (A lebegQpontos érték int típusúvá történQ konverziója levágja az,.0 "$ê+¬-®-..0.Œ/Ž/”3–3Z4\4ü8þ8p9r9N;P;8=:=¤=¦=¤?¦?þAB¤CäC¶E¸EþFGJHLH”J–JöKøKíÞ̺°ºžºqh;„CJ OJQJ^JaJ h%'CJ OJQJ^JaJ h%'h%'CJ OJQJ^JaJ #h;„h;„6CJ OJQJ^JaJ jh;„0JU#h;„h%'6CJ OJQJ^JaJ #h;„h;„5CJ$OJQJ^JaJ$h%'5CJ$OJQJ^JaJ$#h;„h%'5CJ$OJQJ^JaJ$,.0dfª
L8:JLZ\2$¸"ø%à'ž+ +Æ+ê+ì+,,¾/ó048÷ïïïïïïïïïïïïïïïïïïïïïïïïïï $a$gd;„ $a$gd;„²Q
¾Q
ýý48\:VDÀH(PfQŒSôW¬[Œ^Ðc8fdküm´s b
c

cÜdÞdLeNe¾eÀebfdfàgâg>h@hj
jôjöjþlmÆmÈm6n8nŒnŽnŠoŒo

prrpsrshtjt„u†uw
wzz*z,z{{~~~~¼~¾~È~ïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïh%'CJ OJQJ^JaJ h%'h%'CJ OJQJ^JaJ YÈ~Ê~"$8:ˆŠ*€,€„„b„d„ž„ „Ö„Ø„pˆrˆÎˆÐˆôˆöˆ ‰‰4‹6‹î‹ð‹ŒŒ¼Œ¾ŒøŒúŒ0‘2‘|‘~‘Ü’Þ’&“(“F“H“ƔȔ––——

˜ ˜F™H™ä›æ›¾œÀœÆŸÈŸd¡f¡Œ¡Ž¡4¢6¢f¢h¢œ¢ž¢Ö¢Ø¢ä¢æ¢ø¢ú¢££4£6£8£:£&¤(¤†¤ˆ¤è¤ê¤òáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáò h%'h%'CJ OJQJ^JaJ h%'CJ OJQJ^JaJ Yz‰ž‰ ‰žŠ ŠÐŠÒŠ8:´ŽfŽÂŽÄŽæŽèŽ€‚¬âBr¦ÚÜl’n’Š’÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„Š’Œ’“’“¾“À“z™|™¸™º™Z›\›Œ›Ž›æœüþ žJžrž˜ž¶žèžŸ*Ÿ,Ÿ¢¢V¢÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„V¢X¢*¥,¥T¥V¥ºª¼ªèªêªà¬â¬D­v­­°­Ô­ö­®@®T®ª®¬®4¯6¯f¯h¯”±–±÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„ê¤|¥~¥x¦z¦$©&©V©X©hªjªâªäª’«”«Ö¬Ø¬â®ä® ¯"¯L²N²”µ–µš·œ·à·â·öºøºª»¬»8¼:¼ª¼¬¼Î¼Ð¼z¾|¾ÁÁNÁPÁöÁøÁÖÂØÂÜÃÞÃvÄxÄÊÄÌĬŮÅ|Æ~Æ.Ç0DŽdžÇ*È,ÈæÈèÈ6Ì8ÌðÌòÌÐÎÒÎÄÏÆÏôÐöÐÒÑÔÑÔÔ0Ô2Ô~ԀÔÀÔÂÔ$Õ&Õ²Öïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïh%'CJ OJQJ^JaJ h%'h%'CJ OJQJ^JaJ Y–±¸±º±’²”²Î²Ð² µÊºF»€¾šÂ<Ä8ÅpÇzÈ|ÈîÈbÉÈÉ,ÊvÊwÊÍÊÎÊêÊëʮаÐ÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„°ÐÌÐÎÐþÒÿÒÓÓöØ

Ú Ú8Ú:ڐܒÜêÜìÜÆÞ¾ßÀßðßòß@ábâdâœâžâFåºæ¼æ÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„²Ö´ÖNØPØjÚlÚúÚüÚÜÜ0Ü2ܦܨÜúÜü܆݈ÝÞ ÞöÞøÞ°ß²ßÆàÈà|á~árâtâ¨âªâLãNã¨ãªãøãúãJäLäVåXåææ€æ‚æªæ¬æ0è2èìì

í í’ð”ð°ñ²ñ*ò,ò^ò`òNóPó‚ó„ó´ó¶óŒôŽô¸ôºôjõlõ"ö$ö ö¢ö"÷$÷d÷f÷¶÷¸÷ò÷ô÷¶ø¸ødúfúòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáò h%'h%'CJ OJQJ^JaJ h%'CJ OJQJ^JaJ Y¼æôæöæ,èøèÖéJê¸êëë&ë'ë/ïÊòXõZõ’õ”õ˜öšöÆöÚöêö÷÷^÷°÷ì÷,ø÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„,øLø`ø”ø–ø¬üàý¼
ó

dºxS\]{|רëö
!,÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„fúŒúŽú¦ü¨üÈüÊüý ý¬ý®ýþþTÿVÿ|ÿ~ÿ^`ÈÊ


prÈÊâäLN4 6 „ † ¢¤îðè
ê
t v 46:@œž†ˆÄÆ:<˜šòô¬®df""”$–$°$²$œ&ž&Ú(Ü(**Þ+à+”,ïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïh%'CJ OJQJ^JaJ h%'h%'CJ OJQJ^JaJ Y,7BMbs…†”¼"-S ô D$%V'€)D+20>1
2 2,2-2¢2£2¼2Ò2è2÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„”,–,ì-î-V/X/„1†1œ4ž4F5H5@6B6„6†6Ø6Ú6 99Z:\:&<(<À<ÂMvMxMÞOàO®P°PøPúP(Q*Q¾QÀQæQèQ¾SÀSÖTØT˜VšV

Y Y¦[¨[^^^^f^h^òáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáò h%'h%'CJ OJQJ^JaJ h%'CJ OJQJ^JaJ Yè2þ2ÿ2 3Ž55¸5º5þ5š7œ7Ì7Î79 9F9r9ž9 9î=2>§>®?RCTC„C†CG G÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„ GFGtG¢G¤GœJžJÐJÒJâKäK

L8LdL’LÀLÂLàPâPQQHQvQ¤Q¦QºR¼RÚRÜR÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„ÜRLS‚U„U¼U¾UÚUVVØWÚW$X&XTX€X‚XfZhZ¸ZºZìZ[[‚\ƒ\\ž\´\Ë\÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„Ë\Ì\_R`T`Œ`Ž`¾`ì`î`àb*d,dddfd’dÖdØd¦i¨iÜiÞi…k†kšk¯kÅkÛkñk÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„h^Ú^Ü^þ^_ÜaÞa0b2b¤b¦bÈbÊbTeVe~e€epfrf¤g¦g”i–iÚmÜmìnînHpJpt
tØuÚuv v4v6vVvXv¬v®v ww~x€xÂyÄy||€€P€R€º€¼€†ˆð‚ò‚f…h…<ˆ>ˆrˆtˆ´‹¶‹ZŒ\Œ¢Œ¤ŒÜ‘Þ‘’’”’ð“ò“˜˜š˜0™2™òšôšF›H›Ä›Æ›Ò›ïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïh%'CJ OJQJ^JaJ h%'h%'CJ OJQJ^JaJ Yñk l:lhl–lÂlîlmm8o:oNoPo¸oºoÚoÜonrvvJvLvpvœvžv zz/z0z÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„0zqzrzz‚zL{M{Y{‚{ƒ{"}$}P}¤}ø}ú}?~@~d~e~“~”~§~¬~³~º~Ã~Ë~Ì~÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„Ì~£’L†C‡D‡^‡_‡„‡…‡™‡ž‡¤‡¨‡­‡¶‡¼‡Ã‡ã‡ï‡ð‡äˆæˆúˆ

‰&‰<‰>‰‹÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„‹ŒŒ(Œ*ŒNPr’ÖøŽŽêŽëŽ÷Ž (JK~œ¶‘Ä’”÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„”””-” ”P–R–X–Z–Þ–ø–ú–——®—º—¼—æ—è—ò—ô—ü—þ—ž¸ž(¢8¢:¢„¢÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„Ò›Ô›œ œœœbœdœâäVžXžŸŸ„Ÿ†Ÿ| ~ ¬ ® Ê Ì ¡¡¢ ¢Þ¢à¢X£Z£¥¥<¥>¥d¦f¦‚§„§
©

©««ž« «î«ð«4­6­¬­®­ ²²F²H²T²V²"´$´Z´\´|´~´µµ²µ´µ2¶4¶Z¶\¶¦¶¨¶„·†· ¸"¸V¸X¸`¸b¸š¸œ¸2¹4¹J¹L¹T¹V¹z¹|¹òáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáò h%'h%'CJ OJQJ^JaJ h%'CJ OJQJ^JaJ Y„¢†¢<¨\¨^¨Ž¨¨d©f©˜©š©D¬F¬¬’¬J®K®l®–®É®æ®ú®¯ ¯¯g¯h¯}¯¯÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„¯·¯Ë¯÷¯"°H°¤°¦°~²€²ª²À²þ²&³(³B·Ðºf¾ ÀPÀQÀxÀžÀŸÀtÂv˜¸ºÂ÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„|¹¤¹¦¹À¹Â¹Ê¹Ì¹à¹â¹þ¹ºòºôº»»¼¼$¼&¼H¼J¼\¼^¼¸¼º¼Ô¼Ö¼r½t½x½z½¸½º½0¾2¾H¾J¾¿
¿J¿L¿ÂÂà ÃÐÄÒÄVÅXÅÇÇnÇpÇÂÇÄÇÆÇÈÇîÇðÇXÈZȊȌȬȮÈ*É,É.É0ÉÊÊÌÊDËF˘̚ÌpÍrÍæÎèÎàÏâÏþÑÒÔÔÕÕFÙHÙöÙïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïh%'CJ OJQJ^JaJ h%'h%'CJ OJQJ^JaJ YºÂlÅxÆ¢ÉòÉôÉÊ4ÊJÊtʞʤʦʨ˪ËÔËÖËúÌüÌÍͲʹÍÂÍÄÍFÎHÎnÎpÎ÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„pÎ ÏÏ6Ï8Ï~Ð€ÐœÐ´ÐæÐÑBÑVѦÑÔÑÖÑÂÔ
Ö׎Üá%â<â=âQâRâåâæâøâ÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„öÙøÙôÛöÛÎÜÐÜfÝhÝ–Ý˜Ý¦Ý¨Ý ß߲ߴßlànàæáèáää¨äªäêæìæ¢ç¤çÜçÞçÎèÐèìì~ñ€ñæñèñò òBòDòÌòÎòôôvôxôÌôÎôÌõÎõööùù¬ù®ùˆûŠûÎûÐû8þ:þ€‚üþJLÖØÎ Ð ÌΒ ” š
œ
H
J
" 46âäòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáò h%'h%'CJ OJQJ^JaJ h%'CJ OJQJ^JaJ Yøâã ããããæ
æ.æbæhæjæ*è,èPèvè|è~èTéhéjéØéÚé:ê;êKêLê<ì>ì÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„>ìTì¤ìÐìÚì

í8íhíÀíÂíî îFîHîÂðÄðÌðØðêðñ ñ*ñ,ñòòôò óóüôþô÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„þôõ,õ.õF÷H÷‚÷Ô÷Ö÷ä÷æ÷ø6øVøXøÀù2û4ûPûRûüüFüHü®ü°üÆüÈübý÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„býdý°ý²ýÿÿ0ÿ~ÿ ÿ¶ÿìÿ
JLÞÇÈãäóü
"#Ú Ü ÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„
<>btv® ° Ö Ø




B
D
|
~
¦
¨
Ö
Ø
H

Z

\

˜

š



÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„¶

ä

ö




h DF>@fxzr t ¤ ¦ $NPbd Â24÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„ä´¶ÂÄÀ > @ †ˆšœ\^–˜¨ªÀ’îð¸ºHJ :<îðª¬ÚÜìî¢-¤-ˆ Š š œ l$n$6&8&ö(ø(6)8)p*r*\,^,4-6-p-r-Ú-Ü-˜/š/T0V0è0ê0224488<<Ü<Þ
" $ h!j!¨!À!ü!""$"6#8#h#j#&$($:$÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„:$^$`$à&â& ''J'L'l'n'Š(Œ(Â(Ä(

) )$)&)H+J+x+z+¬+®+²+´+"-$-÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„$-N-P-22B2D2~2€2¦233Â5Ä5ì56%6/6J6K6c6d6s6–6—6Þ889:9^9÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„^9x9|9Š9´9è9ì9î9;:<:I:J:©:¸?º?ð?ò?@@(@3@4@®@¯@×@Ø@:CVGXG÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„BBðBòBÌCÎCVEXEÄEÆEbFdF˜FšF´G¶GhHjHöHøH¼J¾JKKîKðK&L(L2M4MˆNŠNÔNÖNOORRRRTRZT\TVVXVZVÄVÆVÚXÜXZZ\Z[[°\²\Ì\Î\ü]þ]<^>^4_6_t_v_x_z_``` `æaèa4b6bVbXbªb¬b dd¼d¾dødúdxezeðfòfòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáò h%'h%'CJ OJQJ^JaJ h%'CJ OJQJ^JaJ YXG¦G¨GÔJJM"OÀRÂRòRþRS(S4S6S@TBT¤T¦Tž\À]Â]
^

^ü`þ`TaVaZa\a÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„\a¼a¾aFcHc¤càcâcFjLlm
m2mZm\mbmdm†mˆm n"nZn\nbndn’n”n„p†p÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„òfLgNgægègrhthÌhÎhpiri`jbjÚjÜjðlòl m¢mîmðm¬n®nÂnÄnþopÎpÐp”r–rªr¬rzs|s ttŠtŒtâtätþtuvuxuÂuÄuvvpvrvŒwŽwìxîx

z z˜|š|Z‚\‚BƒDƒ2„4„@…B…N‡P‡xˆzˆTŠVŠ0‹2‹ä‹æ‹úŒüŒNP˜š¸º••è—ê—ü—ïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïh%'CJ OJQJ^JaJ h%'h%'CJ OJQJ^JaJ Y†p´p¶pþqr4r6rrjrlrs`sbs¢t¤tÈtÊt–uôuöuvvRvTvbvdv4x÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„4x‚x„x¨xªx6y8yhyjy|{~{°{²{&€(€b€d€BD|~¼„îŠ:‹<‹^‹`‹¶÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„¶¸ÌØæè ŽŽ*ŽLŽPŽ\ŽvŽ‚Ž†ŽˆŽ„†ÌÎ¶‘¸‘ȑʑؑڑô‘ö‘¨’÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„¨’ª’º’¼’¨“´–Ö–Ø–ô–ö–nœ¸žºžúžüž~ Þ¢h¤Š§”¨–¨Ä¨Ü¨.©R©T©¢ªú«2¯÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„ü—þ—˜˜š˜ØšÚšZ›\›n›p›‚œ„œ˜š°²zŸ|ŸB D l¡n¡¦¡¨¡\¢^¢b¤d¤¤’¤Æ¤È¤

¦ ¦~¦€¦À¦Â¦^§`§~©€©@­B­J®L®¾¯À¯B°D°ª°¬°

´ ´0·2·Š·Œ·¸¸pºrº¼»¾»þ¼½¾¾¢¾¤¾¿¿Þ¿à¿bÀdÀÁ ÁÀÁÂÁÐÄÒĒŔŨÅÚÅÆÆHÆJÆòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáò h%'h%'CJ OJQJ^JaJ h%'CJ OJQJ^JaJ Y2¯Ž²²À²Â²ú´ü´HµJµbµdµlµnµæµèµðµòµÂ¶*»,»T»V»€¼ÎÀŠÂNÃ@ÄBÆ.Ê÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„Jƨɪɴ˶ËÍÍ2Í4Í`ÎbÎ¼Î¾ÎøÑúÑÒ
ÒdÕfՔՖպռÕÞ×à×dØfغؼØ*Ù,ٖ٘ÙÚښڜÚÜÜÒÝÔݪ߬ßNèPè„è†è¤è¦èÔèÖèìì4ï6ï˜ðšðdñfñ>ò@òZò\òÈóÊóâóäóôô¢ö¤ö(÷*÷v÷x÷ùùzù|ù¢ù¤ùæùèù²û´û~ü€üÜüÞü

þïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïh%'CJ OJQJ^JaJ h%'h%'CJ OJQJ^JaJ Y.ÊÖÌØÌþÌÍÏ,Ð.Ð<Ð>Ð^ÑDÒFÒ\Ò^Ò~Ò€Ò’Ò”Ò ÓÓ(Ó*ÓøÓúÓ8Ô:Ô(Õ*Õ÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„*ÕNÕP՜՞մÕÞÕôÕ
Ö Ö6ÖLÖbÖx֐֨ÖÊÖöÖ*×n׬ררØ(ØBØ^Ø´ØúØ÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„úØ$ÙhÙ|ِÙÖÙÚDÚZÚtÚvÚzÚ|ڒڔڄۆÛÊÛÌÛÞ Þ.Þ:ÞBÞNÞbÞnÞrÞvÞ÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„vÞzހކގސÞ:ß<ßVߚߜß-à à2à3à?àhà~à©àÓàæàëàòàùàá
áá#á)á÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„)á-á2á;áAáHáhátáŠášáÁáÒáíáùáâââ,âOâpââ¸âÉâçâ ãã3ãFãmã÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„mãã®ãÈãÛã ää(äGä[äiäuäŽä¨ä¼äÆäîäåå8åNåSålå‚åšåÇåÔåÞåßå÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„ßåðåñåæ-æ>æIædæsæ|æ“ææ°æ»æÌæÕæææïæýæççç&ç<çqçzçç˜ç°ç÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„°ç¹çÂçËçÒçÜçäçöç

è"è4è8è:èhèjè|èœèÌèîèé.éTéÂéêéê&ê0êKêZê÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„Zê|êê°ê¿êõê+ë,ëììzìÔìí2í`íŽíºíÜíî

îîJîLî€î‚î¦õ<ùŒú,ý÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„,ý.ýHýJý¶þ¸þÞþàþôþTèêúþDJL 

+,?@÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„

þ þÿÿâä¼¾¾ÀLNLN ¬
®
^
`
p
r
Æ
È
F
H
J
L
` b XZhj ¢ Ì Î æ è À€‚ : (L(N(¬(®(ü(þ(F)H)Ö)Ø)

* *òáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáò h%'h%'CJ OJQJ^JaJ h%'CJ OJQJ^JaJ Y@ln¸ º ¦
8:~€Ž ˜ÇÈäè醈˜œÊô „r-÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„r-t-ž Ø Ú Ø!Ú!""""%"-"4"<"D"N"X"b"c"w"x"¤"Ç"Ï"Ñ"é"þ"÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„þ"F$„$¸$Ô$%R%¦%Î%Ô%Ú%Ü%þ%&X&ž& & )‚+„+´+Þ+à+F0Ø0Ú01B1l1÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„ *"*$*è*ê*T+V+¤,¦,à-â-B.D.¦0¨0¨1ª122|2~2–2˜2ü2þ2þ345
5¶5¸5Ä6Æ6H8J8Î:Ð:Ø:Ú:B
G GŒHŽHþHIîIðI|J~JPKRKpKrKÄLÆL0M2MZN\N¤P¦PÎPÐP†SˆSèTêT‚V„V
Wïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïh%'CJ OJQJ^JaJ h%'h%'CJ OJQJ^JaJ Yl1Ä1Æ1ø3l4n4„4ž4´4Ð4Ò4Î57 7D7L7P7R7ž;ú<ü<=.=0=CvCxCÆCÈC÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„ÈCˆGˆJºK¼KØKÚKL
L6L8LöL

P PbPdPjVXX>]@]$^&^ú^ü^$_&_+`,`4`÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„
W

W2W4WXXZZ>[@[t\v\„]†]ž^ ^ê_ì_bbFcHc.d0dŠgŒgÄgÆgÜgÞg
h

h¼h¾h˜išiÊiÌi`jbj¨jªj¶j¸jnnòoôo¤p¦p
q

qVrXr~r€r°s²sltnt.v0vFvHvrvtvôvövêwìwÂzÄzøzúzp{r{œ{ž{Ð{Ò{||x|z|Ð|Ò|â}ä}¦¨òáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáò h%'h%'CJ OJQJ^JaJ h%'CJ OJQJ^JaJ Y4`V`X`b``Ê`Ì`Í`e>hèiêiüiþiàjâjújüjl lll0nÄpÆpîqðq$r&r÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„&rFwHw wx"x*xJxLxVxŒxÀxÂxÃx(|*|b|d|~
~@~6€8€T€V€îð‚‚÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„¨Œ€Ž€ˆŠƒ ƒ"…$…è…ê…š‰œ‰ª‰¬‰ŒŒòŒôŒxzöø ô”ö”|–~–þ—˜Ü˜Þ˜ÆšÈšüšþšŒŽ*Ÿ,Ÿ¼Ÿ¾ŸN P l¡n¡Â£Ä£`¤b¤$¥&¥¦¦Â§Ä§\©^©@ªBª<«>«œ«ž«­­ð®ò®Â¯Ä¯â¯ä¯ø°ú°ò±ô±n²p²þ²³ µ¢µòµôµ>¶@¶†¶ˆ¶6·ïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïh%'CJ OJQJ^JaJ h%'h%'CJ OJQJ^JaJ Y‚’ƒÄ†Æ†ö†d‡¾‡ô‡ö‡0ˆ2ˆ@ˆ¦ˆªˆºˆØˆþˆ‰4‰8‰<‰>‰¤‰ŠŒª¬¶¸÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„¸ÔÖH’““6“8“••&•Ž•’• •Þ•ø•ü•þ•è˜ä›ðòžžLžNž|ž~ž÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„~ž : ¡¡*¡Ž¡’¡¤¡´¡è¡ô¡¢"¢$¢(¢*¢6¢8¢N¦¸©L«N«\«¾«Â«Ø«"¬÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„"¬(¬X¬^¬`¬.­Ú¯hµjµŒµ···`·d·|·Œ·Ê·è·ô· ¸$¸&¸Ôº ¾<ÁLÄMıÄ÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„6·8·Æ¹È¹» »0»2»d»f»(¼*¼j¼l¼Z½\½|½~½’¾”¾ð¾ò¾:À<À4Á6ÁÆÆÌÌtÍvÍÍ’Í Ï"ÏpÏrÏ ÓÓ Ù¢ÙîÚðچ܈Ü:Ý<݊ތÞÚßÜßãã„ã†ã,å.å’ç”çêçìç¶è¸èjêlêÒìÔìðð€ó‚óÎóÐóLôNôföhöÄöÆö‚÷„÷døføbùdù>ú@úüüòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáò h%'h%'CJ OJQJ^JaJ h%'CJ OJQJ^JaJ Y±Ä²ÄtÆvƓȔȦȶɷÉÅÉÒÉÚÉÊÊ3Ê@ÊRÊsÊyʈʎʴÊÁÊÚÊèÊîÊðÊòÊË÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„ËË˒ГРСÐeÑfÑ|Ñ}ѤѥÑËÑÌÑwÕRÖTÖrֈ֪֒֨ÖfÙhÙ&Ú(ÚÛÛ÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„ےܔܬܮÜ$ßnàoàvà±à³àÍàÞàóà

áá;áUájá—á¢á§á±áÉáâáýárâvâxâ÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„xâ~â€â¢â¤âå4æç
ç>çJçLç"è$è4è6èÆé0íÂíÄíøíî=îGîLîVîWî}î~î÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„~îŒî•î©î²î·îÀîÁîèóêóô"ô$ô‚õÎøÐøú’ú²ú´ú§šœ¬òö6÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„ü ýýðþòþÊÿÌÿÌ Î



¬

®

Ú
Ü
&(þ vxê쌎(* ¢ !!$$z$|$„%†%˜%š%Â&Ä&ˆ)Š)*
* **œ*ž*¶*¸*Ä+Æ+0066&6(6Ä?Æ?bAdAìAîA$B&BVCXCÔCÖCêCìCLDND²D´DHH€K‚KNïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïh%'CJ OJQJ^JaJ h%'h%'CJ OJQJ^JaJ Y6 ¤¦¾2@rˆ ¤¦êì. p t Œ ¢ ¦ ¨ › ¯ ° ã ä ÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„ä 4
°

²

Æ

È

Hvx< > Ž  äHúü zŽ’ Ðè-="÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„="¨$ª$Ò$Ô$â&ä&

'h'ž'Ü'Þ',)h.1/’0”0
11`1d1°1272l2u2 2±2¼2÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„¼2Î2Ð2ê2ÿ2333A3L3U3W3a3¦3±3Â3Í3Ò3Õ3ã3ï3ñ3ò3ÿ344G4I4P4W4÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„W4y4~4€4455
555«8°:²:Æ:È:%<&<6<7<„=þ@jClCúDüDÈEÊE„F…F÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„…FLGMGHHÍL O S4W6W¬WX6X]XeXœXžXÄXÔXêXóXY#Y.Y7Y9YSYhYjY÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„NN„N†NvPxP.R0RÄRÆR S"S`UbUÚUÜU>V@Vö[ø[\\6\8\
]

]î]ð]ü]þ]L^N^ ^¢^¤^¦^F`H`aaaPcRcÆcÈce
f

f˜fšf"i$iŠjŒjl
l\l^lmmpmrmzn|nooöpøpqq"q$q r"rŒrŽrvtxtÌtÎtVuXuœvžv.x0xzzòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáò h%'h%'CJ OJQJ^JaJ h%'CJ OJQJ^JaJ YjYkYvYŽYYšY¯Y÷Y
Z,ZHZRZVZxZZ”Z–Z¦ZÔZØZæZ"[0[|[†[Š[Œ[R`¸c÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„¸cRf¦hLnNnFpHppprp stuvu¬w®w†yˆy>|@|¼}¾}h€j€È€Ê€’ƒ”ƒ¸ƒºƒF‡÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„z$z&zèzêzìzîzä{æ{:}<}´}¶}¾}h€ð€ò€ÔƒÖƒ°„²„Ò„Ô„ô„ö„6†8†0‡2‡R‡T‡‚‡„‡ ˆˆlˆnˆìˆîˆ:‰<‰žŒ ŒÔÖfŽhŽÄŽÆŽ tvª‘¬‘²‘´‘:“<“`“b“¾“À“¶•¸•L–N–ö–ø–V—X—^—`—~™€™JšLšïáïáïáïáïáïáïÌïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïá)hÕ*vh%'B*CJ OJQJ^JaJ phÿh%'CJ OJQJ^JaJ h%'h%'CJ OJQJ^JaJ PF‡þŠ,Œ.ŒfŒhŒÀŒÂŒd&Ž‚ŽàŽâŽè‘ê‘’’<’>’¼’Þ“à“@”r”Œ”¬”Дò”•÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„•<•>•ä—æ—˜˜¶˜¸˜Î˜Ð˜Þ˜à˜ò˜ô˜Pš„›‘žä¢~¥€¥Ž¥¥"¦$¦Ž¦¦¸§V©÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„LšdœfœÎœÐœâœäœ , . ¡¡b¢d¢°¢²¢¥¥Ú¥Ü¥¢¨¤¨æ¨è¨RªTª¨ªªªZ«\«ä¬æ¬°°°°²°’±”±@³B³’³”³6´8´R´T´dµfµ ¶¢¶Ž··ü·þ·¸¸h¹j¹¢º¤º»»ü»þ»æ¼è¼Ò½Ô½Ö¾Ø¾ÂÂ*Â,ÂþÂÃøÃúÊČĸĺÄÈÈ:É<ɊʌÊÄÊïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïh%'CJ OJQJ^JaJ h%'h%'CJ OJQJ^JaJ YV©X©‚©´©¶©À©Â©ø©ú©ÒªÔª«
« ««J«L«­²±´±Ê±²²²,²:²f²p²ˆ²÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„ˆ²Œ²Ž²&µ(µJµLµ··B·l·n·D¸F¸\¸r¸†¸–¸º¸¼¸¸¹„»†»´»Ê»ò»ô»ì¿KÀ÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„KÀLÀhÀiÀÙÀÁ ÁÁÁ ÄĂĚĤÄÀÄÂÄ<żÈLÉNÉ–É˜ÉÆÉÈÉàÉâÉäÊæÊòÊ÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„ÄÊÆÊ¼Ë¾ËLÎNÎüÏþÏ*Ð,Ð"Ñ$ÑLÑNÑXÒZÒ¤Ò¦ÒôÒöÒÔÔØØÖØØØÙÙÙÙþÙÚ@ÚBÚÎÛÐÛöÝøÝªÞ¬Þ`ßbߢߤߔá–áfâhâÒâÔâ0æ2æÞèàèØéÚéôéöé,ë.ë<ë>ëHìJìˆíŠíˆïŠïàñâñôô

ö ö"ö$ö û¢ûøûúû’ÿ”ÿ$&ÊÌ46®°òáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáò h%'h%'CJ OJQJ^JaJ h%'CJ OJQJ^JaJ YòÊôÊÌ&Ï(ϲÏÈÏÊÏàÑzÔ|ÔâÔäÔèÕêÕ
Ö Ö4ÖùÖúÖ ×
×$×%×6×7×ÜØÞØ˜Ù÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„˜ÙšÙÄÙÆÙ(Ýàßâßôß(à>àBàVàdàÂàöà áá á`ábázá|á¢âhäjä~äÒäàäää÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„ääåHåRåjånåpå”è¸íšð0ñ2ñBñ`ñ|ñ„ñ†ñóüöÒøÔø ùùzú¹üýzýáý¨þ÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„¨þDÿp„†”¤²Àš ž ö

÷




ˆŠ
78† ˆ ¦ ² ´ ÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„°ÐÒbdbd” – ¼ ¾
"
$
&
d
f
.0z| ~€¼¾:<ÚÜ¢¤æèZ"\"##F#H# $$€%‚%ú%ü%((„,†,æ,è,--ò-ô-ð.ò.¦0¨0t1v144ø4ú4r5t5Š5Œ5¬5®5œ6ž6 6¢6º6¼68 8š9œ9Â:ïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïh%'CJ OJQJ^JaJ h%'h%'CJ OJQJ^JaJ Y´ NOXYrs|}
/no~„…<>¬ÂÐÔè(9÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„9GIJNPt’œ ¢T-V-x-Ž-˜-œ-ž-Ò-Ô-ò- " !!)!`!b!÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„b!l!w!Ã!È!ü!þ!""v#x#¶$¸$P&Q&t&u&á&â&ó&&(p(¬(ê(2)4)â)ä)*÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„**ö*ø* ++À+Ð-2Â2Ã2Ì2Í2œ5x8z8Þ899Z9^9`9æ=è=²>´>ˆ@Š@°A÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„Â:Ä:;;à<â<¦>¨>JALADDLENEŽFFJJˆLŠL MMÖMØM¤N¦NŒOŽOQQTT¶W¸WÀYÂYðYòYÜZÞZ†_ˆ_L`N`

h hiixjzjkk4k6kÄkÆkðmòm nnhnjn´n¶nrrsÊtÌtðuòuÐvÒvòwôwzzz z„z†zî}ð}Ž~~Z\ڀ܀òáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáò h%'h%'CJ OJQJ^JaJ h%'CJ OJQJ^JaJ Y°A²A§B¨BÑBÒBíBîBøBùBZD\DjDlDªDjElE”E–EøEúE FF&F(FNFPFG÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„>GTGVGjGlGŒGŽG¢G¤GÀGÂGRHSHƒHH‘H˜HµHÁHÆHÒHÔHÕHôKöKPLRLØOÚO÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„ÚOP$P&P"QºQ¼Q”R•R²R³R¶R·RÂRÉRÎRÕRÖR&S´SµSÂSÃSNVPV¢V¤VÒXÔX÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„ÔXôXöX€Y0[À\Á\Ù\ñ\]]ï]ð]__p_r_èaêa b"bTbhbzbŽb¦bºbÊbÚb÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„Úbêbûb

cc9cVcWcLdðdñde

e
eXf iþij"j$jÌldmfmÌmÎm0o2o x

x.x/xOyPykylyz z.z8zXzZzB{`b”–H€J€h€÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„h€~€ˆ€ž€ €>‚NƒPƒnƒpƒ‚ƒ„ƒ¬ƒ®ƒH„”†–†¬†Â†Ð†Ú†è†ê†ˆˆ6ˆLˆZˆ^ˆ÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„܀ȂʂB„D„‡‡ðŠòŠà‹â‹~€ØŽÚŽ’’J’L’Ú’Ü’¨“ª“””¸”º”²—´—ò—ô—ššðšò𠬡®¡ä¢æ¢º£¼£ò£ô£t¤v¤¤’¤¢¥¤¥Â¥Ä¥¦ ¦j¦l¦²§´§€¨‚¨Æ¨È¨@«B«D«F«ô«ö«Ð°Ò°²
²Ä²Æ²º³¼³^¶`¶Æ¶È¶d·f·–·˜·4¸6¸ª¸¬¸`¹ïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïh%'CJ OJQJ^JaJ h%'h%'CJ OJQJ^JaJ Y^ˆhˆvˆxˆôˆöˆ

‰<‰^‰‚‰š‰ž‰¼‰ŠŠv‹ò‹ô‹ŒŒ(Œ6Œ8Œ–˜ÊÌÒÔ÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„ԍòŽ,Ž>ŽfŽxނޔޖŽè±‘²‘·‘À‘Á‘:’ì”î”@•^•|•€•¨•º•Ö•–2–R–÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„R–r–œ–º–ä–———"—$—?˜@˜W˜X˜ü›þ›.œ;œ=œcœxœ‘œ¡œÀœÍœ×œáœëœõœ÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„õœÿœ
 (2GNXcnx‰“šœ´Íçž$ž&ž'žj¢ò¥–ªl¬÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„l¬m¬l­m­Œ­­¼­½­Ï­Ø­Ù­Ï®Ð®ç®ð®ñ®$°&°2°P°b°n°r°t°^³`³z³†³Š³÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„гŒ³H´â´ä´Vµªµ¬µš¶@¸B¸r¸t¸Ô¼Ê¿4À6À~ÀºÀÁ
Á˜ÂšÂâÂøÂüÂÃpÞÃ÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„`¹b¹ô¹ö¹6º8ºüºþº

» ».¼0¼œ¼ž¼º½¼½¾¾”¾–¾ä¾æ¾¿’¿œÀžÀ¼Á¾ÁÂÂ*Â,Â:Ä<ĞŠÅ@ÆBÆ<È>ÈNÈPÈÉ
ɨʪÊäÌæÌxÍzÍ
Î

ÎÀÎÂÎúÎüÎÏÏРФЦЎѐÑÒ
ÒÖÖRÖTÖjÖlÖÖÖØÖü×þ×´Ù¶ÙÜÜxÞzÞ¾ÞÀÞßß$â&âfãhãòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáò h%'h%'CJ OJQJ^JaJ h%'CJ OJQJ^JaJ YžÃÚÃîÃZĞÄüÄ0ÅVÅZÅ\Å\Ê^ÊÌÊäÊèÊ Ë^˒ËÄË
Ì&ÌPÌxÌ|̀̂Ì@чԈÔ÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„ˆÔ¿ÔÊÔÌÔÙÔ ÕÕ(Õ3Õ5Õ7Õ8ÕöÖøÖ Ú
Ú#Ú$Ú¨Û©Û¬ÛµÛÈÛÉÛnÝTáVá²áÈá÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„ÈáÖáÚáôáZâ”â¢âðâ

ãjã„ãÊãæãä ä:ä>ä@ä°æ²æèŽèŠé‹érëtë ë¢ëºî÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„hãÆäÈä0æ2æêê’ê”êÊìÌìhíjíèíêírîtî°î²îÎîÐîï
ïòòXòZòœòžò\ó^ózõ|õ”õ–õÊ÷Ì÷Î÷Ð÷øø>ú@ú†úˆúôüöünýpý€‚üþX Z Œ Ž ŒŽ~ € ø ú R
T
Ž  XZZ\üþª¬z|~€ÎÐdfïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïh%'CJ OJQJ^JaJ h%'h%'CJ OJQJ^JaJ Yºî<ð=ðRð…ð‡ðŽð£ðÎðßð ññ#ñ8ñPñRñTñUñ¤õ|ö~öÔö÷p÷t÷€÷„÷†÷ôø÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„ôøöø(ù*ùJýþþ0þ_þiþ’þ”þ•þrÿsÿ®°êì¼äæ".DZjv÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„v ¢è^
`

Â
æ
þ

(
6
v
‚
„
t

u

€

¢

Ã

Ø

ã

ù

ÿ





÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„
‡
ˆ
³
´
bÄã ä ö ÷ åæKlxˆŸ¯°1Q[\÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„XZÀސ’”&-(-Ð-Ò-î-ð-"","."%%**ˆ*Š*++0+2+è,ê,t-v-¶-¸-@/B/ô2ö2x5z588¸8º84969‚9„9ú9ü9œ<ž<*=,==’=@@:AK@K†KˆKœMžMPP€P‚PRQTQòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáòáò h%'h%'CJ OJQJ^JaJ h%'CJ OJQJ^JaJ Y™ ¬$®$Ø$R%V%€%È%&&&&A&V&e&p&y&{&…&Œ&Æ&Ó&â&ï&ý& '
'

'÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„

'8'P'a'c'p'’'´'Ñ'Ó'æ'ò'ô'(((&('(E(f(h((((Ù(Ú(ç(è(^+÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„^+h.x/z/ /¢/Ä4¬6­6É6Ê69(:):E:F:ô:õ:<=>=ª=¬=Í>Î>ò>ó>(C¼H²K÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„²K´K,LBLFLlL†LÚLMT@TXXŽYYÐYÒYZ~ @~ B~ ~ ¢~ x z Î Ð ú ü †„ ˆ„ … … *… ,… .… 0… h‡ j‡ Š Š   hŽ jŽ d f ʏ ̏ \‘ ^‘ ”‘ –‘ ’ ’ (’ *’ :’ <’ l“ n“ ” ” J” L” • • ܕ ޕ L– N– ¤– ¦– — — ‚— „— æ— è— f˜ h˜ ô˜ ö˜ 6™ 8™ tš vš žš š ̚ Κ °› ²› h j ïáïáïáïáïáïßïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáïáUh%'CJ OJQJ^JaJ h%'h%'CJ OJQJ^JaJ X9RMRbRkRRºR¼R½R¿RÀRÕRÖRASFVªV«VÔVãVîVðVWWWWjWkW~WW|~ ÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷÷ $a$gd;„ esetleges tört részt, amint errQl a 2. fejezetben szó volt.)

4.2. Gyakorlat. BQvítsük ki atof-ot oly módon, hogy az
123.45e-6
alakú tudományos jelölésmódot is kezelni tudja, ahol a lebegQpontos számot e vagy E és egy esetleges elQjellel ellátott kitevQ követheti!

4.3. További tudnivalók a függvényargumentumokról

Az 1. fejezetben megtárgyaltuk a nyelvnek azt a tulajdonságát, hogy a függvényargumentumok érték szerint adódnak át, vagyis a hívott függvény az egyes argumentumoknak nem a címét, hanem a külön ideiglenes másolatát kapja meg. Eszerint a függvény nem képes befolyásolni a hívó függvényben található eredeti argumentumot. A függvényen belül valójábanminden argumentum lokális változó, amely azzal az értékkel inicializálódott, amivel a függvényt meghívták.
Ha a függvény argumentumaként tömbnév jelenik meg, a tömb kezdQcíme adódik át; a tömbelemek nem másolódnak át. A függvény az átadott címtQl kezdQdQ indexeléssel megváltoztathatja a tömb elemeit. A tömbök tehát név szerint adódnak át. Az 5. fejezetben elmondjuk, hogyan lehet a mutatókat úgy használni, hogy a hívó függvényekben található nemtömb jellegq változókat is befolyásolni tudjuk.
Megjegyezzük, hogy nincs teljesen kielégítQ módszer olyan gépfüggetlen függvények írására, amelyek változó számú argumentumot fogadnak. Nincs ugyanis olyan gépfüggetlen eljárás, amellyel a hívott függvény meg tudná határozni, hogy adott hívás alkalmával ténylegesen hány argumentumot kapott. îgy nem tudunk például olyan, igazán gépfüggetlen programot írni, amely ki tudná választani tetszQleges számú argumentum közül a legnagyobbat, amint azt a FORTRAN és a PL/1 max nevq beépített függvénye teszi.
Változó számú argumentum általában biztonságosan használható, ha a hívott függvény nem használ olyan argumentumot, amit ténylegesen nem kapott meg, továbbá ha a típusok használata következetes. A printf, amely a legközönségesebb változó-argumentumszámú C függvény, az elsQ argumentumában található információ alapján határozza meg, hogy még hány argumentum következik és azoknak mi a típusa. Súlyos hiba lép fel, ha a hívó nem ad elegendQ számú argumentumot, vagy ha a típusok nem azonosak azzal, amit az 1. argumentum mond. A printf sem gépfüggetlen, és különbözQ környezetek esetében módosítani kell.
Másik lehetQség, hogy amennyiben az argumentumok ismert típusúak, valamilyen megállapodás szerint, pl. egy speciális argumentumértékkel (ami gyakran a nulla) meg lehet jelölni az argumentumlista végét.

4.4. KülsQ változók

A C program külsQ objektumok halmaza. Ezek változók vagy függvények lehetnek. A külsQ jelzQt a belsQ fogalommal való szembeállítás kedvéért használjuk, amely utóbbi a függvényeken belül definiált argumentumokat és automatikus változókat írja le. A külsQ változókat függvényeken kívül definiáljuk, így sok függvény számára elérhetQk. Maguk a függvények mindig külsQk, mivel a C-ben nem lehet függvényeket más függvényeken belül definiálni. Megállapodás szerint a külsQ változók egyben globális változók is tehát minden, az ilyen változóra ugyanazzal a névvel történQ hivatkozás (még a teljesen külön fordított függvényekbQl is) ugyanarra a fizikai objektumra történQ hivatkozást jelent. Ebben az értelemben a külsQ változók a FORTRAN vagy PL/1 externaljainak felelnek meg. KésQbb látni fogjuk, hogyan definiálhatunk olyan külsQ változókat és függvényeket, amelyek globálisan nem hozzáférhetQk, hanem csupán egyetlen forrásállományon belül láthatók.
Mivel a külsQ változók globálisan hozzáférhetQk, helyettesíthetik a függvényargumentumokat és a függvények közötti kommunikáció céljait szolgáló visszatérési értékeket. Bármelyik függvény hozzáférhet külsQ változóhoz az illetQ változó nevére történQ hivatkozással, ha a nevet korábban deklarálták.
Ha függvények között nagy számú változót kell megosztani, a külsQ változók használata kényelmesebb és hatékonyabb, mint a hosszú argumentumlistáké.
Amint az 1. fejezetben rámutattunk, ezt az okoskodást fenntartással kell fogadnunk, mivel az ilyen megoldás rontja a program áttekinthetQségét és olyan programokat eredményez, amelyekben sok a függvények közötti adatkapcsolat.
A külsQ változók használatának második oka az inicializálással kapcsolatos. Különösen lényeges, hogy a külsQ tömbök inicializálhatók, az automatikus tömbök azonban nem. E fejezet vége felé foglalkozunk az inicializálással.
A harmadik ok - ami miatt külsQ változókat használunk - érvényességi tartományuk és fennmaradási idejük. Az automatikus változók valamely függvényre nézve belsQ változók : akkor jönnek létre, amikor a vezérlés belép a rutinba, és megszünnek az onnan való kilépéskor. A külsQ változók viszont állandóan megmaradnak: nem jönnek-mennek, így az egyik függvényhívástól a másikig megtartják értéküket. Ha tehát két függvénynek meg kell osztoznia valamilyen adathalmazon és egyik függvény sem hívja a másikat, gyakran az a legkényelmesebb, ha a közösen használt adatokat külsQ változókban tartjuk és nem adogatjuk át ide-oda argumentumokon keresztül.
Vizsgáljuk tovább ezt a kérdést egy nagyobb példán keresztül! A feladat egy újabb, az elQzQnél jobb kalkulátorprogram írása. Ez a program már megengedi a +, -, * , / és = mqveleteket. A kalkulátor az infix jelölésmód helyett a fordított lengyel (reverse Polish) jelölésmódot használja, mivel az utóbbi kényelmesebb. (îgy mqködnek pl. a Hewlett Packard gyártmányú zsebszámológépek.) Ebben a jelölésmódban minden operátor az operandusai után áll; az olyan infix kifejezést, mint pl.

(1 - 2) * (4 + 5) =

úgy írjuk be, hogy:

1 2 - 4 5 + * =

Zárójelekre nincs szükség.
A megvalósítás egészen egyszerq. Minden operandust egy verembe tolunk; operátor érkezésekor a megfelelQ darabszámú operandus (kétoperandusú operátorok esetében kettQ) kilép a verembQl, elvégezzük rajtuk az operátor által meghatározott mqveletet, majd az eredményt ismét visszaírjuk a verembe. A fenti esetben pl. elQbb 1 és 2 a verembe kerül, majd a helyükbe a kettQ különbségét, vagyis -1-et írjuk. Ezután 4-et és 5-öt toljuk a verembe, amelyeket azután az összegük, vagyis 9 helyettesít. Végül a szorzás után -1 és 9 helyére szorzatuk, vagyis -9 kerül a verembe. Az = operátorral kinyomtatjuk a verem legfelsQ elemét anélkül, hogy onnan elmozdulna (így egy számítás részeredményei is ellenQrizhetQk).
Bár a verembe tolás és az onnan történQ kiléptetés (push és pop) mqveletei egyszerqek, mire a hibafigyelést és javítást is hozzáfqzzük, elég hosszú programot kapunk ahhoz, hogy mindent külön függvénybe tegyünk, ahelyett, hogy ugyanazt a programkódot ismételgetnénk az egész programon keresztül. Szükség van továbbá egy külön függvényre, amely beolvassa a következQ bemenQ operátort vagy operandust. îgy a program felépítése:

while (a következQ operátor vagy operandus
nem az állomány vége)
if (szám)
told a verembe
else if (operátor)
léptesd ki az operandusokat
végezd el a mqveletet
told a verembe az eredményt
else
hiba

Nem döntöttünk még a fQ kérdésben - hol legyen a verem, vagyis mely rutinok férhessenek hozzá közvetlenül. Az egyik lehetQség, hogy a vermet a main rutinban tartjuk, és a vermet és a pillanatnyi verempozíciót átadjuk a verembe írást és az onnan történQ kiléptetést végzQ rutinoknak. A main rutinnak azonban nem kell tudnia a vermet vezérlQ változókról; csupán a verembe történQ írásra és az onnan történQ kiléptetésre kell ügyelnie. Ezért úgy döntöttünk, hogy a vermet és a hozzá kapcsolódó információt olyan külsQváltozókkal ábrázoljuk, amelyekhez a push és pop függvények hozzáférhetnek, a main azonban nem.
Ezt a megoldást egyszerqen lefordíthatjuk a programozás nyelvére. A fQprogram lényegében az operátorok és operandusok típusára vonatkozó nagy switch-bQl áll, ez talán tipikusabb használata a switch utasításnak, mint amit a 3. fejezetben láttunk:

#define MAXOP 20 /*Operandus és operátor max.mérete*/
#define NUMBER '0' /*Szám észlelésének jelzése*/
#define TOOBIG '9' /*Jelzi, hogy a karakterlánc
túl nagy*/
main () /*Fordított lengyel logikájú kalkulátor*/
{
int type;
char s [MAXOP];
double op2, atof(), pop(), push();
while ((type = getop (s, MAXOP)) != EOF)
switch (type) {
case NUMBER:
push (atof(s));
break;
case ' +' :
push (pop() + pop());
break;
case '*' :
push (pop() * pop());
break;
case '-' :
op2 = pop ();
push (pop() - op2);
break;
case '/':
op2 = pop ();
if (op2 != 0.0)
push (pop () / op2);
else
printf ("az osztó nulla\n");
break;
case '=':
printf ("\t %f \n", push(pop()));
break;
case 'c':
clear ();
break;
case TOOBIG:
printf ("%.20s . . .túl hosszú\n", s);
break;
default:
printf ("ismeretlen parancs %c \n", type);
break;
}
}

#define MAXVAL 100 /*Értékverem max. mélysége*/
int sp = 0; /*Veremmutató*/
double val [MAXVAL]; /* Értékverem*/

double push (f) /*f írása az értékverembe*/
double f;
{
if (sp < MAXVAL)
return (val [sp++] = f);
else {
printf ("hiba: a verem megtelt\n");
clear ();
return (0);
}
}

double pop () /*A legfelsQ érték kiemelése a verembQl*/
{
if (sp > 0)
return (val [--sp]);
else {
printf ("hiba: a verem üres\n");
clear ();
return (0);
}
}

clear () /*A verem kiürítése*/
{
sp = 0;
}

A c parancs annak a clear függvénynek a segítségével üríti ki a vermet, amit hiba esetén a push és a pop is használ. A getop függvénnyel rövidesen foglalkozunk.
Amint arról az 1. fejezetben már szó volt, egy változó akkor külsQ, ha az összes függvény törzsén kívül definiáljuk. îgy a push, a pop és a clear által használt vermet és veremmutatót e három függvényen kívül definiáltuk. Maga a main azonban nem hivatkozik a veremre és a veremmutatóra - a verem ábrázolását gondosan elrejtettük. îgy az = operátorra vonatkozó programrésznek a

push (pop ());

utasítást kell használnia ahhoz, hogy a verem tetejét a verem megváltoztatása nélkül meg lehessen vizsgálni.
Figyeljük meg továbbá, hogy mivel a + és a * kommutatív operátorok, a kiléptetett operandusok kombinálásának sorrendje közömbös, a - és a / operátorok esetében azonban meg kell különböztetni a bal oldali és a jobb oldali operandust.

4.3. Gyakorlat. Az alapvetQ programkeret megtartásával egyszerqen kibQvíthetjük a kalkulátorprogramot. Vezessük be a moduló (_%) és az egyoperandusú mínusz operátorokat! Vezessük be továbbá az erase parancsot, amely törli a verem legfelsQ elemét! Vezessünk be változónevek kezelését lehetQvé tevQ parancsokat! (A huszonhat egybetqs változónévre egyszerqen megoldható.)

4.5. Az érvényességi tartomány szabályai

Nem szükséges egyszerre lefordítani a C programot alkotó összes függvényt és külsQ változót: a program forrásszövege több állományban tárolható, és könyvtárakból már elQzQleg lefordított rutinok is betölthetQk. Ezzel kapcsolatban két érdekes kérdés merül fel:
- Hogyan lehet a deklarációkat úgy megírni, hogy a fordítás során a változók helyesen deklarálódjanak?
- Hogyan kell elkészíteni a deklarációkat ahhoz, hogy a program betöltésekor az összes részlet helyesen kapcsolódjon össze?
Egy név érvényességi tartománya a programnak az a része, amelyre vonatkozóan a nevet definiáltuk. A függvény elején definiált automatikus változó érvényességi tartománya az a függvény, amelyben a nevet deklaráltuk, és a más függvényekben ugyanilyen néven létezQ változókat ez nem érinti. Ugyanez igaz a függvény argumentumaira.
A külsQ változó érvényességi tartománya ott kezdQdik, ahol a forrásállományban a változót deklaráltuk és az illetQ állomány végéig tart. Ha pl. a val, sp, push, pop és clear ebben a sorrendben, egyetlen állományban vannak definiálva, vagyis:

int sp = 0;
double val [MAXVAL];
double push (f) { . . . }
double pop () { . . . }
clear () { . . . }

akkor a val és sp változók a push, pop és clear függvényekben egyszerqen megnevezésükkel használhatók, és nincs szükség további deklarációkra.
Ha viszont egy külsQ változóra még annak definiálása elQtt kell hivatkozni, vagy ha egy külsQ változót más forrásállományban definiálunk, mint ahol használunk, akkor kötelezQen extern deklarációt kell alkalmazni.
Lényeges, hogy különbséget tegyünk valamely külsQ változó deklarációja és definíciója között! A deklaráció a változó tulajdonságait írja le (típusát, méretét stb.), míg a definícióval tárterületet is lefoglalunk. Ha az

int sp;
double val [MAXVAL];

sorok minden függvényen kívül jelennek meg, akkor definiálják az sp és val nevq külsQ változókat, tárterületet foglalnak le, és az adott forrásállomány többi része számára deklarációként is szolgálnak. Másrészt az

extern int sp;
extern double val [];

sorok deklarálják, hogy sp int típusú, val pedig double típusú tömb (amelynek méretét máshol határozzuk meg), de ezek a sorok nem hozzák létre a változókat és nem foglalnak le számukra tárterületet.
A forrásprogramot alkotó állományok között csupán egyben kell a külsQ változó definíciójának szerepelnie; a többi állományban extern deklarációval biztosítjuk a változó elérését. (A definíciót tartalmazó állományban is lehet extern deklaráció.) KülsQ változót csak definiáláskor lehet inicializálni. A tömbméreteket a definícióban kell megadni, de opcionálisan extern deklarációban is szerepelhetnek.
Bár az elQbbi programban az ilyenfajta szervezés nem valószínq, elképzelhetQ, hogy a val és sp változókat az egyik állományban definiáljuk és inicializáljuk, míg a push, pop és clear függvényeket egy másikban. Ekkor összekapcsolásukhoz a következQ definíciók és deklarációk szükségesek:
Az 1. állományban:

int sp = 0; /* Veremmutató*/
double val [MAXVAL]; /* Értékverem*/

A 2. állományban:

extern int sp;
extern double val [];
double push (f) { . . . }
double pop () { . . . }
clear () { . . . }

Minthogy a 2. állományban található extern deklarációk a három függvény elQtt és azokon kívül fordulnak elQ, ezért mindegyikükre vonatkoznak, tehát egyetlen deklarációkészlet elegendQ lesz az egész 2. állományhoz.
Fejezetünkben szó lesz még a nagyobb programoknál elQnyös #include szolgáltatásról, amely lehetQvé teszi, hogy csak egyszer írjuk le az extern deklarációkat, amelyek azután fordítás közben minden forrásállományba beillesztQdnek.
Nézzük most a getop megvalósítását, amely a következQ operátort vagy operandust olvassa be. Az alapfeladat egyszerq: a szóközök, tabok és újsorok átugrása. Ha a következQ karakter nem számjegy és nem tizedespont, akkor getop visszaadja az illetQ karaktert. Egyébként összegyqjti a számjegyekbQl álló karakterláncot (amely tizedespontot is tartalmazhat) és NUMBER-rel tér vissza, jelezve, hogy a bemenetre szám érkezett.
A rutin elég bonyolult, mivel arra törekedtünk, hogy azt az esetet is helyesen kezelje, amikor a beolvasott szám túl hosszú. A getop mindaddig számjegyeket olvas be (esetleg közben egy tizedespontot is), amíg azok el nem fogynak, de csupán azokat tárolja, amelyek elférnek. Ha nem volt túlcsordulás, akkor NUMBER-rel és a számjegyek karakterláncával tér vissza. Ha azonban a szám túl hosszú volt, akkor figyelmen kívül hagyja a beolvasott sor hátralevQ részét, és így a felhasználó a hiba helyétQl kezdve egyszerqen újraírhatja a sort. A függvény a túlcsordulást a TOOBIG-gel való visszatéréssel jelzi:

getop (s, lim) /*A köv. operátor vagy operandus beolvasása*/
char s [];
int lim;
{
int i, c;
while ((c = getch()) == ' ' || c == '\t' || c == '\n')
;
if (c != '.' && (c < '0' || c > '9'))
return (c);
s [0] = c;
for (i = 1; (c = getchar()) >= '0' && c <= '9'; i++)
if (i < lim)
s [i] = c;
if (c == '.') { /*A tört rész beolvasása*/
if (i < lim)
s [i] = c;
for (i++; (c = getchar()) >= '0' && c <= '9'; i++)
if (i < lim)
s [i] = c;
}
if (i < lim) { /*A szám rendben van*/
ungetch(c);
s [i] = '\0';
return (NUMBER);
}
else { /*Túl nagy, a sor többi részét átugorja*/
while (c != '\n' && c != EOF)
c = getchar();
s [lim - 1] = '\0';
return (TOOBIG);
}
}

Mit jelent getch és ungetch? Gyakran az a helyzet, hogy a bemenetet olvasó program csak akkor jön rá, hogy eleget olvasott, amikor már a kelleténél több karaktert olvasott be. Ilyen eset pl., amikor egy számot alkotó karaktereket kell beolvasni: amíg a program nem észleli az elsQ nem-számjegyet, a szám nem teljes. Ehhez azonban a programnak a szükségesnél eggyel több karaktert kell beolvasnia, egy olyan karaktert, amelyre nincs felkészülve.
Valahogy tehát nembeolvasottá kellene tenni a nemkívánt karaktert. Amikor a program a szükségesnél eggyel több karaktert olvasott be, vissza kellene helyezni azt a bemenetre, így a program a továbbiakban úgy viselkedhetne, mintha ezt a felesleges karaktert sohasem olvasta volna be. Szerencsére mindezt két, egymással együttmqködQ függvény megírásával könnyen megoldhatjuk. getch szállítja a következQ megvizsgálandó bejövQ karaktert; ungetch visszaír egy karaktert a bemenetre oly módon, hogy a következQ getch hívás ismét ezt a karaktert szolgáltatja.
Az együttmqködés módja egyszerq. Az ungetch a felesleges karaktereket egy megosztott pufferbe - egy karaktertömbbe - írja vissza. A getch kiolvassa a puffert, amennyiben abban van valami, ill. ha üres, meghívja a getchar függvényt. Szükség van egy olyan indexváltozóra is, amely az éppen vizsgált karakter pufferbeli pozícióját mutatja.
Mivel a getch és az ungetch a puffert és az indexet közösen használja, az utóbbiaknak a hívások között meg kell tartaniuk értéküket, mindkét rutinra nézve külsQ változóknak kell lenniük. îgy a getch, ungetch és az általuk megosztva használt változók az alábbi módon írhatók:

#define BUFSIZE 100
char buf [BUFSIZE]; /*Az ungetch puffere*/
int bufp = 0; /*A következQ szabad pozíció buf-ban*/
getch () /*Kiolvas egy (esetleg visszaírt)
karaktert*/
{
return ((bufp > 0) ? buf [--bufp] : getchar());
}

ungetch (c) /* Karakter visszahelyezése a bemenetre*/
int c;
{
if (bufp > BUFSIZE)
printf("ungetch, túlsok karakter\n");
else
buf [bufp++] = c;
}

A pufferbe történQ visszaírásra egyetlen karakter helyett tömböt használtunk, mivel ez az általánosítás a késQbbiekben még jól jöhet.

4.4. Gyakorlat. îrjuk meg az ungets(s) nevq rutint, amely egy teljes karakterláncot ír vissza a bemenetre! Szükséges, hogy az ungets függvénynek tudomása legyen buf-ról és bufp-rQl, vagy csak egyszerqen használja az ungetch függvényt?

4.5. Gyakorlat. Tegyük fel, hogy sohasem helyezünk vissza egynél több karaktert. Módosítsuk a getch és ungetch függvényeket ennek megfelelQen !

4.6. Gyakorlat. getch és ungetch rutinjainak a visszahelyezett EOF-ot gépfüggQ módon kezelik. Határozzuk meg, hogyan viselkedjenek rutinjaink, amikor EOF-ot írunk vissza, majd valósítsuk meg ezt a megoldást!

4.6. Statikus változók

A már korábban megismert extern és automatikus változók mellett a statikus (static) változók jelentik a harmadik tárolási osztályt. A static változók akár belsQk, akár külsQk lehetnek. A belsQ static változók ugyanúgy lokálisak valamely függvényre nézve, mint az automatikus változók, de az automatikusaktól eltérQen állandóan fennmaradnak és nem jönnek létre, ill. szünnek meg a függvény minden egyes aktivizálása alkalmával. Eszerint a belsQ static változók a függvényen belül saját, állandó tárat képeznek. A függvényeken belül megjelenQ karakterláncok, mint pl. a printf argumentumai, belsQ static változók.
A külsQ static változó annak a forrásállománynak a további részében lesz ismert, amelyben deklarálták, de érvényességi tartománya nem terjed ki egyetlen más állományra sem. A külsQ static változók segítségével lehetQségünk van arra, hogy az olyan neveket mint buf és bufp elrejtsük a getch-ungetch kombinációban. A változóknak külsQknek kell lenniük ahhoz, hogy megoszthatók legyenek, ugyanakkor rejtve kell maradniuk a függvények felhasználói elQl, mivel így kizárjuk a konfliktus lehetQségét. Ha a két rutint és a két változót egyetlen állományba szerkesztjük:

static char buf [BUFSIZE]; /*Az ungetch puffere*/
static int bufp = 0 /*A következQ szabad pozíció
buf-ban*/
getch () { . . . }
ungetch (c) { . . . }

akkor egyetlen más rutin sem férhet hozzá a buf és bufp változókhoz; de nem kerülhetnek összeütközésbe a változók az ugyanezen program más állományaiban elQforduló ugyanilyen nevekkel sem.
A statikus tárolást, legyen az akár belsQ, akár külsQ, úgy definiáljuk, hogy a közönséges deklaráció elé a static szót írjuk. A változó külsQ, ha az összes függvényen kívül, ill. belsQ, ha valamelyik függvényen belül definiálják.
A függvények általában külsQ objektumok, a nevük globálisan ismert. Ugyanakkor a függvények static típusúnak is deklarálhatók, az ilyen függvények neve ismeretlen lesz azon az állományon kívül, ahol deklarálták.
A C nyelvben a static deklaráció nem csupán állandóságot rejt magában, hanem bizonyos mértékq elzártságot is. A belsQ static objektumok csupán az adott függvényen belül ismertek; a külsQ static objektumok (változók vagy függvények) pedig csak abból a forrásállományból hozzáférhetQk, amelyben megjelennek, és neveik nem kerülnek összeütközésbe a más állományokban elQforduló ugyanilyen nevq változókkal, ill. függvényekkel.
KülsQ static változók és függvények segítségével elrejthetjük az adatobjektumokat és a velük dolgozó belsQ rutinokat, így más rutinok és adatok ezekkel még véletlenül sem kerülhetnek összeütközésbe. A getch és az ungetch függvény pl. karakterbeolvas_ és -visszaíró modult alkot; buf és bufp pedig static kell, hogy legyen ahhoz, hogy kívülrQl ne legyen elérhetQ. Hasonlóképpen push, pop és clear egy veremkezelQ modult alkot; val-nak és sp-nek ugyancsak külsQ static-nak kell lennie.

4.7. Regiszterváltozók

A negyedik és egyben utolsó tárolási osztály neve register. A register deklaráció közli a fordítóprogrammal, hogy a kérdéses változóra nagyon gyakran történik hivatkozás. Amennyiben lehetséges, a register típusú változók a gép regisztereibe kerülnek, miáltal rövidebb és gyorsabb programokjönnek létre. A register deklaráció alakja:

register int x;
register char c;

és így tovább; az int rész elhagyható. A register deklaráció csupán automatikus változókra, valamint függvények formális paramétereire alkalmazható. Utóbbi esetben a deklaráció alakja:

f (c, n)
register int c, n;
{
register int i;
. . .
}

A gyakorlatban a regiszterváltozókra nézve olyan megszorítások állnak fenn, amelyek az adott hardver tulajdonságait tükrözik. Az egyes függvényeknek csupán néhány változója tárolható regiszterekben és csak bizonyos típusok megengedettek. A fölös számú, ill. meg nem engedett deklarációk esetében a register szót a gép figyelmen kívül hagyja. Nem hivatkozhatunk továbbá valamely regiszterváltozó címére (ezzel a témával az 5. fejezetben foglalkozunk). A speciális megkötések géprQl gépre változnak: például a PDP- 11 számítógépen csupán a függvényen belüli elsQ három regiszterdeklaráció hatásos, a típus pedig int, char vagy mutató lehet.

4.8. Blokkstruktúra

A PL/1, ill. az ALGOL értelmében a C nem blokkstruktúrált nyelv, amennyiben függvények nem definiálhatók más függvények belsejében.
Változókat azonban definiálhatunk blokkstruktúrált módon. Nyitó kapcsos zárójel után - amely nemcsak függvény, hanem mindenfajta összetett utasítás kezdetét jelzi - változódeklarációk és inicializálások egyaránt állhatnak. Az ily módon deklarált változók felülbírálják a külsQbb blokkokban ugyanilyen név alatt elQforduló változókat és a vonatkozó jobb oldali kapcsos zárójelig érvényben maradnak. Pl. az

if (n > 0) {
int i; /* oj i deklarálása*/
for (i = 0; i < n; i++)
. . .
}

programban az i változó érvényességi tartománya az if utasítás igaz ága; ennek az i-nek semmi köze a programban elQforduló bármely egyéb i-hez.
A blokkstruktúra külsQ változókra is alkalmazható. Ha adottak az

int x;
f ()
{
double x;
. . .
}

deklarációk, akkor az f függvényen belül az x elQfordulásai a belsQ, double típusú változóra, f-en kívül a külsQ int változóra vonatkoznak. Ugyanez igaz a formális paraméterek neveire :

int z;
f (z)
double z;
{
. . .
}

Az f függvényen belül z a formális és nem a külsQ paraméterre vonatkozik.

4.9. Inicializálás

Az inicializálásról futólag már többször is szóltunk, de mindig csak mellékesen valamely más téma kapcsán. Ebben a fejezetben összefoglaljuk a szabályok egy részét, miután már megtárgyaltuk a különféle tárolási osztályokat.
Explicit inicializálás hiányában a külsQ és a statikus változók kezdeti értéke garantáltan nulla lesz; az automatikus és a regiszterváltozók értéke határozatlan.
Az egyszerq változók (nem a tömbök és a struktúrák) a deklarálásukkor inicializálhatók oly módon, hogy a nevüket egyenlQségjel és egy állandó kifejezés követi :

int x = 1;
char squote = '\''; /*Aposztróf*/
long day = 60 * 24; /*Percek száma a napban*/

KülsQ és statikus változók esetében az inicializálás egyszer, mégpedig értelemszerqen a fordítási idQben történik meg. Az automatikus és a regiszterváltozók minden alkalommal inicializálódnak, amikor a vezérlés belép a függvénybe vagy blokkba.
Automatikus és regiszterváltozók esetében az inicializálás jobb oldalán nemcsak egy állandó állhat - tetszQleges, korábban definiált értékeket, akár függvényhívásokat tartalmazó kifejezés is megengedett. A 3. fejezetben említett bináris keresQprogram kezdeti érték beállításai pl. a következQ módon írhatók :

binary (x, v, n)
int x, v [], n;
{
int low = 0;
int high = n-1;
int mid;
. . .
}

a már látott:

binary (x, v, n)
int x, v [], n;
{
int low, high, mid;
low = 0;
high = n - 1;
. . .
}

alak helyett.
Az automatikus változók inicializálásai valójában értékadó utasítások rövidített formái. Lényegében csupán ízlés kérdése, hogy valaki melyik alakot részesíti elQnyben. 9ltalában explicit értékadásokat használtunk, mivel a deklarációkban elQforduló inicializálások nehezebben követhetQk.
Automatikus tömbök nem inicializálhatók. KülsQ és statikus tömbök úgy inicializálhatók, hogy a deklarációt a kezdeti értékek kapcsos zárójelek közé zárt és vesszQkkel elválasztott listája követi. Az 1. fejezetben ismertetett karakterszámláló program, amelynek kezdete

main () /*Számjegyek, üres közök és egyebek számlálása*/
{
int c, i, nwhite, nother;
int ndigit [10];
nwhite = nother = 0;
for (i = 0; i < 10; i++)
ndigit [i] = 0;
. . .
}

volt, ehelyett így is írható:

int nwhite = 0;
int nother = 0;
int ndigit [10] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, } ;
main () /*Számjegyek, üres közök és egyebek számlálása*/
{
int c, i;
. . .
}

Az adott esetben ezek az inicializálások szükségtelenek, mivel mindegyik kezdeti érték nulla, ennek ellenére célszerq explicit alakban megadni Qket. Ha a megadott méretnél kevesebb számú kezdeti érték van, akkor a többi kezdeti érték nulla lesz. Túl sok kezdeti érték megadása hibát jelent. Sajnos nincs lehetQség valamely kezdeti érték ismétlQdésének megadására, sem pedig arra, hogy a tömb valamely közbensQ elemét az összes többi kezdeti érték megadása nélkül inicializáljuk.
A karaktertömbök a kezdetiérték-beállítás speciális esetét jelentik: a kapcsos zárójelekkel és vesszQkkel történQ jelölésmód helyett karakterlánc is használható :

char pattern [] = "the";

Ez rövidítése a hosszabb, de ezzel egyenértékq írásmódnak:

char pattern [] = { 't', 'h', 'e', '\0'};

Ha egy - tetszQleges típusú - tömb méretét elhagyjuk, a fordító a tömbhosszúságot a megadott kezdeti értékek darabszámából számítja ki. A fenti esetben a méret négy (három karakter és a záró \0).

4.10. Rekurzió

A C megengedi a függvények rekurzív használatát, vagyis a függvények (közvetlenül vagy közvetve) saját magukat is hívhatják. Ennek hagyományos példája valamely számnak karakterláncként történQ nyomtatása. Amint korábban említettük, a számjegyek rossz sorrendben generálódnak: a kis helyiértékq számjegyek a nagyobb helyiértékq számjegyek elQtt jönnek létre, de a nyomtatás fordított sorrendben kell, hogy történjék.
A problémának két megoldása van. Az egyik megoldás szerint a számjegyeket a generálás sorrendjében tároljuk egy tömbben, majd fordított sorrendben nyomtatjuk ki Qket, ahogy ezt a 3. fejezetben az itoa függvény tette. A printd elsQ változata ezt a megoldást követi.

printd (n) /*n nyomtatása decimális alakban*/
int n;
{
char s [10];
int i;
if (n < 0) {
putchar ('-');
n = -n;
}
i = 0;
do {
s [i++] = n % 10 + '0'; /*Veszi a következQ
karaktert*/
} while ((n /= 10) > 0); /*Elhagyja*/
while (--i >= 0)
putchar (s [i]);
}

A másik lehetQség a rekurzív megoldás, amelyben printd minden hívásakor elQször saját magát hívja meg - feldolgozza az összes vezetQ számjegyet, majd kinyomtatja az utolsó számjegyet.

printd (n) /*n nyomtatása decimális alakban
(rekurzív)*/
int n;
{
int i;
if (n < 0) {
putchar ('-');
n = -n;
}
if ((i = n / 10) != 0)
printd (i);
putchar (n % 10 + '0');
}

Amikor a függvény önmagát rekurzív módon meghívja, minden hívás az automatikus változók friss készletét kapja meg, függetlenül a korábbi készlettQl. îgy printd(123) esetében az elsQ printd függvényben n = 123. 9tadja a 12-t az újabb printd függvénynek, és 3-at nyomtat ki annak visszatérése után. Ugyanígy a második printd 1 -et ad át a harmadiknak (amely kinyomtatja), majd kiírja a 2-t.
A rekurzió általában nem gyorsabb, és tár-megtakarítást sem jelent, mivel valahol létre kell hozni egy vermet a feldolgozott értékek számára. A rekurzív programkód azonban tömörebb és gyakran könnyebben leírható és megérthetQ. Mint a 6. fejezetben látni fogjuk, a rekurzió különösen kényelmes a rekurzív módon definiált adatstruktúrák, pl. a fastruktúrák esetében.

4.7. Gyakorlat. A printd-ben alkalmazott megoldások felhasználásával írjuk meg az itoa rekurzív változatát, vagyis rekurzív rutin segítségével konvertáljunk egy egész számot karakterlánccá!

4.8. Gyakorlat. îrjuk meg az s karakterláncot megfordító reverse(s) függvény rekurzív változatát!

4.11. A C elQfeldolgozó

A C nyelv egy egyszerq makro-elQfeldolgozó (preprocesszor) segítségével bizonyos nyelvi kiterjesztéseket is nyújt számunkra. E kiterjesztések közül a legközönségesebb a már látott #define, de ide tartozik az a nyelvi eszköz is, amely állományok tartalmának a fordítás során történQ beiktatását teszi lehetQvé.

9llományok beiktatása
A #define szimbólumok, deklarációk és más nyelvi objektumok kezelését is megkönnyíti a C állománybeiktatási szolgáltatása. Minden sor, amelynek alakja:

#include "állománynév"

az állománynév nevq állomány tartalmával helyettesítQdik. (Az idézQjelek kötelezQk!) Gyakran minden forrásállomány elején megjelenik egy vagy két ilyen alakú sor, amely a közös #define utasításoknak és a globális változók extern _deklarációinak beiktatására szolgál. A #include parancsok egymásba skatulyázhatók.
Az #include a legelQnyösebb módja annak, hogy egy nagy méretq program deklarációit összegyqjtsük. E megoldás eredményeképpen az összes forrásállomány ugyanazokat a definíciókat és változódeklarációkat kapja meg, és ezáltal különösen kellemetlen hibákat kerülhetünk el. Természetesen olyankor, amikor valamelyik beiktatott állomány megváltozik, az összes ettQl függQ állományt újra le kell fordítani.

Makrohelyettesítés
A

#define YES 1

alakú definícióval a legegyszerqbb típusú makrohelyettesítést valósítjuk meg - egy nevet egy karakterlánccal helyettesítünk. A #define-ban elQforduló nevek alakja azonos a C azonosítókéval; a helyettesítQ szöveg tetszQleges. A helyettesítQ szöveg általában a sor további része; hosszú definíciók úgy folytathatók, hogy a folytatandó sor végére \-t helyezünk el. A #define szimbólummal definiált név érvényességi tartománya a definíció helyétQl az adott forrásállomány végéig tart. A nevek újradefiniálhatók; a definíciók korábbi definíciókat használhatnak. IdézQjelek közé írt karakterláncokra nézve, tehát amikor pl. YES definiált név, a

printf("YES")

-ben nem történik helyettesítés.
Minthogy a #define-t egy makro-elQfeldolgozó, nem pedig maga a fordító dolgozza fel a definíciókra csak nagyon kevés nyelvtani megkötés vonatkozik. Az ALGOL hívei pl. a

#define then
#define begin {
#define end; }

definíciók után azt írhatják, hogy:

if (i > 0) then begin
a = 1;
b = 2; end

LehetQség van argumentumokkal rendelkezQ makrók definiálására, amikor is a helyettesítQ szöveg a makró hívásának módjától függ. Példaként definiáljuk a max nevq makrót az alábbi módon :

#define max(A, B) ((A) > (B) ? (A) : (B))

Ekkor az

x = max(p+q, r+s);

sort az

x =((p + q) > (r + s) ? (p + q) : (r + s));

sor fogja helyettesíteni. Olyan maximum függvényt kaptunk tehát, amelynek nem függvényhívás, hanem soron belüli programkód a kifejtése. Ha az argumentumokat következetesen kezeljük, ez a makro bármilyen adattípusra meg fog felelni: nem szükséges különféle max-okat készítenünk a különbözQ adattípusokra, mint ahogy azt függvényhívások esetében tennénk.
Amennyiben közelebbrQl megvizsgáljuk a max elQzQ kifejtését, találhatunk benne néhány buktatót. A kifejezések kétszer értékelQdnek ki; ez nem jó olyankor, amikor mellékhatásuk pl. függvényhívás vagy inkrementáló operátorok alkalmazása. Ügyelnünk kell továbbá a zárójelek használatára, nehogy megváltozzon a kiértékelés sorrendje! (tekintsük a

#define square(x) x * x

makrót, amikor azt square(z + 1) alakban hívjuk.) Vannak tisztán jelölésbeli problémák is: nem szabad, hogy a makro neve és az argumentumlistáját bevezetQ bal oldali zárójel között szóköz legyen!
Mindezek ellenére a makrók igen hasznosak. Erre nézve gyakorlati példa a 7. fejezetben ismertetendQ szabványos be- és kiviteli (I/O) könyvtár, amelyben a getchar és putchar függvényeket makrókként definiáljuk (putchar nyilván argumentumot igényel). Ezáltal elkerüljük azt a plusz terhelést, amit a minden egyes feldolgozandó karakter esetében történQ függvényhívás jelentene.
A makroprocesszor további szolgáltatásait az "A" függelék ismerteti.

4.9. Gyakorlat. Definiáljuk a swap(x, y) makrót, amely megcseréli a két int típusú argumentumát! (A blokkstruktúra segítségünkre lesz.)

5. fejezet: Mutatók és tömbök

A mutató (pointer) olyan változó, amely egy másik változó címét tartalmazza. A C-ben gyakran használunk mutatókat, részben azért, mert néha csupán így tudunk kifejezni valamilyen számítást, részben pedig azért, mert használatuk általában tömörebb és hatékonyabb kódot eredményez, mint amelyet más módon kaphatnánk.
Azt szokták mondani, hogy a mutató, csakúgy, mint a goto utasítás, csak arra jó, hogy összezavarja és érthetetlenné tegye a programot. Ez biztos így is van, ha ész nélkül használjuk, hiszen könnyqszerrel gyárthatunk olyan mutatókat, amelyek valamilyen nem várt helyre mutatnak. KellQ önfegyelemmel azonban a mutatókat úgy is alkalmazhatjuk, hogy ezáltal programunk világos és egyszerq legyen. Ezt kíséreljük meg bemutatni a következQkben.

5.1. Mutatók és címek

Mivel a mutató valamilyen objektum címét tartalmazza, rajta keresztül az illetQ objektum indirekt módon érhetQ el. Tegyük fel, hogy x - pl. int - változó, px pedig mutató, amelyet valamilyen eddig még nem ismertetett módon hoztunk létre. Az & egyoperandusú operátor valamely objektum címét adja meg, tehát a

px = &x;

utasítás x címét rendeli hozzá a px változóhoz; ilyenkor azt mondjuk, hogy px az x értékre mutat (azt címzi meg). Az & operátor csupán változókra és tömbelemekre alkalmazható; az olyan konstrukciók, mint &(x + 1) vagy &3 nem megengedettek. Ugyancsak tilos valamely register változó címére hivatkozni!
A * egyoperandusú operátor úgy kezeli az operandusát, mint a keresett érték címét, és megkeresi ezt a címet, hogy tartalmát behozza. Ha tehát y is int, akkor

y = *px;

annak a tartalmát rendeli y-hoz, amire px mutat. îgy a

px = &x;
y = *px;

szekvencia ugyanazt az értéket rendeli y-hoz, mint

y = x;

A mqveletekben részt vevQ változókat deklarálnunk is kell :

int x, y;
int *px;

x és y deklarációja ugyanolyan, mint eddig. A px mutató deklarációja azonban új.

int *px;

azt fejezi ki, hogy a *px kombináció int típusú mennyiség, vagyis ha px a *px környezetben fordul elQ, akkor egy int típusú változónak felel meg. Gyakorlatilag a változók deklarációjának szintaxisa azoknak a kifejezéseknek a szintaxisát utánozza, amelyekben az illetQ változók elQfordulhatnak. Ez a meggondolás mindig hasznos, még bonyolult deklarációk esetében is. Pl.

double atof (), *dp;

azt fejezi ki, hogy kifejezésekben atof() és *dp double típusú értékkel rendelkeznek.
Vegyük észre továbbá, hogy a deklaráció azt is kimondja, hogy a mutatónak mindig a megadott típusú objektumra kell mutatnia.
Mutatók kifejezésekben is elQfordulhatnak. Ha pl. px az egész típusú x-re mutat, akkor *px minden olyan szövegkörnyezetben elQfordulhat, ahol x elQfordulhat.

y = *px + 1

hatására y 1 -gyel nagyobb lesz, mint x;

printf ("%d \n", *px)

kinyomtatja x pillanatnyi értékét, továbbá

d = sqrt((double)*px)

d-ben elQállítja x négyzetgyökét, ahol x double típusúvá alakul át, mielQtt átadódna az sqrt függvénynek (l. a 2. fejezetet).
Az olyan kifejezésekben, mint

y = *px + 1

a * és & egyoperandusú operátorok szorosabban kötnek, mint az aritmetikai operátorok, így a kifejezést kiértékelQ program elQször kiolvassa azt, amire px mutat, majd hozzáad 1 -et, és hozzárendeli y-hoz. Rövidesen visszatérünk arra, hogy mit jelenthet

y = *(px + 1)

Mutatóhivatkozások értékadások bal oldalán is elQfordulhatnak. Ha px és x-re mutat, akkor

*px = 0

az x-et kinullázza és

*px += 1

vagy

(*px)++

inkrementálja. Az utóbbi példában a zárójelek szükségesek: nélkülük a kifejezés px-_t inkrementálná, nem pedig azt, amire px mutat, mivel az egyoperandusú operátorok, esetünkben * és ++ jobbról balra értékelQdnek ki.
Végül, mivel a mutatók változók, ugyanúgy kezelhetQk, mint a többi változó. Ha py egy másik, int típusú mennyiségre mutat, akkor

py = px

a px tartalmát py-ba másolja, miáltal py ugyanarra fog mutatni, mint px.

5.2. Mutatók és függvényargumentumok

Mivel a C nyelv az argumentumokat érték szerinti hívás formájában adja át a függvényeknek, a hívott függvény semmilyen közvetlen módon nem tudja megváltoztatni a hívó függvény változóit. Mit tegyünk, ha tényleg meg kell változtatnunk valamelyik közönséges argumentumot? Példának okáért a rendezQrutin megcserélhet két, rossz sorrendben levQ elemet a swap nevq függvény segítségével. Nem elég, ha azt írjuk, hogy

swap (a, b);

ahol a swap függvény definíciója:

swap (x, y) /*ROSSZ*/
int x, y;
{
int temp;
temp = x;
x = y;
y = temp;
}

Az érték szerint történQ hívás miatt a swap nem képes az Qt meghívó rutinban elQforduló a és b argumentumokat megváltoztatni.
Szerencsére van lehetQség a kívánt hatás elérésére. A hívó rutin a megváltoztatandó értékeket megcímzQ mutatókat ad át:

swap (&a, &b);

Mivel az & operátor a változó címét állítja elQ, &a az a változót megcímzQ mutató lesz. Magában a swap rutinban az argumentumokat mutatókként deklaráljuk, és az aktuális operandusokat ezeken keresztül érjük el:

swap (px, py) /*px és py megcserélése*/
int *px, *py;
{
int temp;
temp = *px;
*px = *py;
*py = temp;
}

A mutatóargumentumokat gyakran alkalmazzák az olyan függvényekben, amelyeknek egynél több értéket kell visszaadniuk. (Mondhatjuk pl., hogy swap két értéket ad vissza, tudniillik argumentumainak az új értékeit.) Példaként tekintsük a getint függvényt, amely szabadformátumú bemeneti konverziót végez oly módon, hogy egy karaktersorozatot egész típusú értékekre tördel, hívásonként egy-egy egész értéket szolgáltatva. A getint függvénynek vagy már általa talált értéket kell visszaadnia, vagy pedig - amennyiben nincs több beolvasandó karakter az állomány vége jelet. Az értékeknek külön-külön objektumokként kell visszatérniük, függetlenül attól, hogy milyen értéket használunk EOF-ként, amely maga is egy beolvasott egész mennyiség lehet.
Az egyik megoldás szerint, amely a 7. fejezetben ismertetésre kerülQ scanf beolvasófüggvényen alapul, getint az EOF-ot mint saját függvényértékét adja vissza olyankor, amikor megtalálta az állomány végét; minden más visszatérQ érték közönséges egész számra utal. A megtalált egész szám numerikus értéke argumentumon keresztül adódik vissza, amelynek egész számot megcímzQ mutatónak kell lennie. Ez a fajta szervezés elválasztja az állomány vége állapotot a numerikus értékektQl.
A következQ ciklus a getint hívásai segítségével egész számokkal tölt fel egy tömböt:

int n, v, array [SIZE];
for (n = 0; n < SIZE && getint (&v) != EOF; n++)
array [n] = v;

Minden egyes hívás beírja a v változóba a bemeneten talált következQ egész számot. Vegyük észre, hogy a getint argumentumaként &y-t kell írnunk v helyett. Ha a puszta v-t használjuk, akkor valószínqleg címzési hibát követünk el, mivel getint azt hiszi, hogy érvényes mutatót kapott.
Maga a getint a már korábban megírt atoi természetes módosítása:

getint (pn) /*A következQ egész beolvasása a
bemenetrQl*/
int *pn;
{
int c, sign;
while ((c = getch ()) == ' ' || c == '\n' || c == '\t')
; /*Az üres közt átugorja*/
sign = 1;
if (c == '+' || c == '-') { /*Feljegyzi az elQjelet*/
sign = (c == '+') ? 1 : -1;
c = getch ();
}
for (*pn = 0; c >= '0' && c <= '9'; c = getch ())
*pn = 10 * *pn + c - '0';
*pn *= sign;
if (c != EOF)
ungetch (c);
return (c);
}

A getint függvényben a *pn végig közönséges int tipusú változóként szerepel. Felhasználtuk a geth és ungeth függvényeket is (leírásukat l. a 4. fejezetben), így azt az egy plusz karaktert, amit még ki kell olvasni, vissza lehet helyezni a bemenetre.

5.1. Gyakorlat. îrjuk meg a getfloat függvényt, amely a getint lebegQpontos megfelelQje! Milyen tipust ad vissza getfloat függvényértékként?

5.3. Mutatók és tömbök

A C nyelvben szoros kapcsolat van a mutatók és a tömbök között. Ez indokolja, hogy a mutatókkal és a tömbökkel egyidejqleg foglalkozzunk. Valamennyi mqvelet, amely tömbindexeléssel végrehajtható, mutatók használatával éppúgy elvégezhetQ. 9ltalában az utóbbi változat gyorsabb, de különösen a kezdQk számára elsQ ránézésre nehezebben érthetQ. Az

int a [10]

deklaráció definiálja azt a tömböt, amelynek mérete 10, vagyis egy tíz, egymást követQ objektumból, az a[0], a[1], . . ., a[9] nevq elemekbQl álló blokkot határoz meg. Az a[i] jelölésmód a tömbnek a kezdettQl számított i-edik pozícióját fejezi ki. Ha pa egészt megcímzQ mutató, amelyet

int *pa

deklarál, akkor a

pa =&a[0]

értékadás úgy állítja be pa-t, hogy az az a nulladik elemére mutasson, vagyis pa az a[0] elem címét tartalmazza. Ekkor az

x = *pa

értékadás a[0] tartalmát x-be másolja.
Ha pa az a tömb adott elemére mutat, akkor definíció szerint pa + 1 a tömb következQ elemére mutat. 9ltalában pa - i i elemmel pa elé, pa + i pedig i elemmel pa mögé mutat. îgy ha pa az a[0]-ra mutat, akkor

*(pa + 1)

a[1] tartalmát szolgáltatja, pa+i az a[i] elem címe és *(pa+i) az a[i] elem tartalma.
Ezek a megjegyzések az a tömbben elhelyezkedQ változók tipusátol függetlenül mindig igazak. Az "adj 1-et a mutatóhoz" és ennek kiterjesztéseként az egész mutatóaritmetika alapdefiníciója, hogy a növekmény mértékegysége annak az objektumnak a tárbeli mérete, amire a mutató mutat. îgy pa+i esetében a pa-hoz hozzáadás elQtt i azoknak az objektumoknak a méretével szorzódik, amire pa mutat.
Az indexelés és a mutatóaritmetika között láthatóan nagyon szoros kapcsolat van. Gyakorlatilag a tömbre való hivatkozást a fordító a tömb kezdetét megcímzQ mutatóvá alakítja át. Ennek hatására a tömb neve nem más, mint egy mutatókifejezés, amibQl számos hasznos dolog következik. Mivel a tömb neve ugyanaz, mint az illetQ tömb nulladik elemének címe, a

pa = &a[0]

értékadás úgy is írható, mint

pa = a

Legalábbis elsQ ránézésre még meglepQbb az a tény, hogy az a[i]-re történQ hivatkozás *(a+i)-ként is írható. a[i] kiértékelésekor a C fordító azonnal átalakítja ezt *(a+i)-vé; a két alak teljesen egyenértékq. Ha az ekvivalencia mindkét elemére alkalmazzuk az & operátort, akkor azonnal következik, hogy &a[i] és a+i szintén azonosak: a+i az a-t követQ i-edik elem címe. Az érem másik oldala viszont az, hogy ha pa mutató, akkor azt kifejezések indexelhetik: pa[i] azonos *(pa+i)-vel. Röviden, bármilyen tömb vagy indexkifejezés leírható, mint egy mutató plusz egy eltolás és viszont, akár egy utasításon belül is.
Van azonban egy fontos különbség a tömbnév és a mutató között, amire ügyelnünk kell. A mutató változó, így pa=a és pa++ értelmes mqveletek. A tömbnév azonban állandó, nem pedig változó: az olyan konstrukciók, mint a=pa vagy a++, vagy p=&a nem megengedettek!
Amikor a tömbnév egy függvénynek adódik át, a függvény valójában a tömb kezdetének címét kapja meg. A hívott függvényen belül ez az argumentum változó, ugyanúgy, mint a többi, a tömbnévargumentum tehát csakugyan mutató, vagyis egy címet tartalmazó változó. E tényt kihasználva megírhatjuk az strlen karakterlánchossz-számító függvény új változatát:

strlen (s) /*Visszaadja az s karakterlánc hosszát*/
char *s;
{
int n;
for (n = 0; *s != '\0'; s++)
n++;
return (n);
}

s inkrementálása teljesen megengedett, mivel a mutatók változók; s++-nak nincs hatása az strlen hívó függvénybeli karakterláncra - csupán a címnek az strlen-ben található másolatát inkrementálja.
Függvénydefinícióban

char s [];

és

char *s;

egyaránt szerepelhet formális paraméterként; azt, hogy melyiket használjuk, nagymértékben az dönti el, hogy miként írjuk le a kifejezéseket a függvényen belül. Amikor a tömbnév adódik át valamelyik függvénynek, a függvény tetszése szerint hiheti azt, hogy tömböt vagy mutatót kapott, és ennek megfelelQen kezelheti azt. Akár mindkét típusú mqveletet használhatja, ha ez célszerqnek és világosnak látszik.
LehetQség van arra, hogy a tömbnek csupán egy részét adjuk át valamelyik függvénynek oly módon, hogy a résztömb kezdetét megcímzQ mutatót adunk át. Ha pl. a egy tömb neve, akkor

f(&a[2])

és

f(a+2)

egyaránt az a[2] elem címét adja át az f függvénynek, mivel &a[2] és a+2 egyaránt mutatókifejezés, mindkettQ az a tömb harmadik elemére vonatkozik. f-en belül az argumentumdeklaráció akár

f(arr)
int arr [];
{
. . .
}

akár

f (arr)
int *arr;
{
. . .
}

is lehet. Ami f-et illeti, az a tény, hogy az argumentum valójában egy nagyobb tömb egy részére vonatkozik, semmiféle következménnyel sem jár.

5.4. Címaritmetika

Ha p mutató, akkor p++ oly módon inkrementálja p-t, hogy az a megcímzett tetszQleges típusú objektum következQ elemére, p += i pedig úgy, hogy az a pillanatnyilag megcímzett elem utáni i-edik elemre mutasson. Az ilyen és hasonló szerkezetek a mutató- vagy címaritmetika legegyszerqbb és legközönségesebb formái.
A C nyelv következetes és szabályos módon közelít a címaritmetikához; a mutatók, tömbök és a címaritmetika egységes kezelése a nyelv egyik legfQbb erénye. Szemléltessük ezt azzal, hogy megírunk egy elemi tárfoglaló programot (amely azonban egyszerqsége ellenére is használható)! Két rutinunk van: az alloc(n) rutin n egymást követQ karakterpozíciót megcímzQ p mutatót ad vissza, amelyet az alloc hívója karakterek tárolására használhat; továbbá free(p), amely felszabadítja az alloc rutinnal nyert tárterületet késQbbi használat céljára. A rutinok "elemiek", mivel free hívásai fordított sorrendben kell, hogy történjenek, mint az alloc hívásai. îgy az alloc és a free által kezelt tárterület egy verem, vagyis egy "utolsó-be, elsQ-ki" (last-in, first-out) lista. A szabványos C könyvtárban rendelkezésre állnak az ezeknek megfelelQ függvények, amelyekre azonban nem vonatkoznak ilyen megszorítások, és a 8. fejezetben is bemutatunk javított változatokat. Addig azonban számos alkalmazáshoz megfelel a triviális alloc is, ha arra van szükségünk, hogy elQre nem látható méretq kisebb tárterületek elQre nem látható idQpontokban rendelkezésre álljanak.
A legegyszerqbb megvalósításban az alloc egy allocbuf-nak nevezett nagy karaktertömb darabjait szolgáltatja. Ez a tömb az alloc és a free kizárólagos tulajdona. Mivel ez a két rutin mutatókat és nem tömbindexeket használ, a tömb nevét egyetlen más rutinnak sem kell ismernie, így az static extern-ként deklarálható, vagyis csak az alloc és a free függvényeket tartalmazó állományban lesz érvényes és azon kívül láthatatlan. A gyakorlatban akár nem is kell, hogy névvel rendelkezzen a tömb: ehelyett úgy is elQállítható, hogy a program az operációs rendszertQl elkér valamilyen név nélküli tárblokkot megcímzQ mutatót.
Tudnunk kell azt is, hogy mennyi került felhasználásra allocbuf-ból. E célból egy, a következQ szabad elemet megcímzQ mutatót használunk, amelynek neve allocp. Ha valaki az alloc-tól n karaktert kér, akkor az ellenQrzi, hogy maradt-e még ennyi hely allocbuf-ban. Ha igen, akkor alloc visszaadja az allocp pillanatnyi értékét (vagyis a szabad blokk kezdQcímét), majd azt n-nel inkrementálja, hogy a következQ szabad területre mutasson. free(p) egyszerqen p-re állítja be allo_p-t, ha p az allocbuf-on belül van.

#define NULL 0 /*Mutató a hibajelzéshez*/
#define ALLOCSIZE 1000 /*A rendelkezésre álló terület
mérete*/
static char allocbuf [ALLOCSIZE]; /*Tárhely
alloc-nak*/
static char *allocp = allocbuf; /*Köv. szabad hely*/

char *alloc (n) /*n karaktert megcímzQ mutatót ad vissza*/
int n;
{
if (allocp + n <= allocbuf + ALLOCSIZE) { /*Befér*/
allocp += n;
return (allocp - n); /*Régi p*/
} else /*Nincs elég hely*/
return (NULL);
}

free (p) /*p által megcímzett terület felszabadítása*/
char *p;
{
if (p >= allocbuf && p < allocbuf + ALLOCSIZE)
allocp = p;
}

Néhány megjegyzés: 9ltalában a mutató éppen úgy inicializálható, mint bármilyen más változó, noha közönséges esetben értelmes érték csupán a NULL (l. a továbbiakban) vagy olyan kifejezés lehet, amely a korábban definiált megfelelQ típusú adatok címeit tartalmazza. A

static char *allocp = allocbuf;

deklaráció úgy definiálja allocp-t, hogy az karaktermutató legyen, és úgy inicializálja, hogy allocbuf-ra mutasson, amely a következQ szabad pozíció a program indításakor. Ezt úgy is írhattuk volna, hogy:

static char *allocp = &allocbuf [0];

mivel a tömb neve egyben a nulladik elemének a címe; mindig a természetesebb változatot használjuk! Az

if (allocp + n <= allocbuf + ALLOCSIZE)

vizsgálat ellenQrzi, hogy van-e elegendQ hely az n számú karakter elhelyezésére vonatkozó kérés teljesítésére. Ha igen, akkor az allocp új értéke legfeljebb eggyel mutat túl az allocbuf végén. Ha a kérés kielégíthetQ, az alloc közönséges mutatóval tér vissza (figyeljük meg magának a függvénynek a deklarációját). Ha a kérés nem teljesíthetQ, akkor az alloc-nak valamilyen jel visszaadásával kell jeleznie, hogy nem maradt hely. A C nyelv gondoskodik arról, hogy semmiféle olyan mutató, amely érvényes módon adatra mutat, nem tartalmazhat nullát, így a nulla visszatérési érték használható az abnormális esemény jelzésére, adott esetben annak közlésére, hogy nincs hely. Számszerq nulla helyett azonban NULL-t írunk, hogy ezzel világosabban jelezzük : ez a mutató különleges értéke. 9ltalában egész számok nem rendelhetQk értelmes módon mutatókhoz, a nulla speciális eset.
Az olyan vizsgálatok, mint

if (alloc + n <= allocbuf + ALLOCSIZE)

és

if (p >= allocbuf && p < allocbuf + ALLOCSIZE)

a mutatóaritmetika lényeges sajátosságaira világítanak rá. ElQször is, a mutatók bizonyos körülmények között összehasonlíthatók. Ha p és q ugyanannak a tömbnek az elemeire mutat, akkor az olyan relációk, mint <, >= stb. megfelelQen mqködnek.

P < q

pl. akkor igaz, ha p a tömb kisebb sorszámú elemére mutat, mint q. Az == és != relációk ugyancsak alkalmazhatók. Bármilyen mutató nullával való egyenlQsége vagy nemegyenlQsége értelmes módon ellenQrizhetQ. Vigyázat! Ne végezzünk viszont különbözQ tömböket megcímzQ mutatókkal aritmetikai mqveleteket vagy összehasonlításokat! Ha szerencsénk van, akkor minden gépen nyilvánvaló értelmetlenséget kapunk. Ha azonban nincs szerencsénk, akkor a programunk mqködni fog az egyik gépen, de rejtélyes módon össze fog omlani egy másikon.
Másodszor láttuk, hogy az egész számot megcímzQ mutatókkal összeadás és kivonás végezhetQ.

p + n

a p által éppen megcímzett objektumot követQ n-edik objektumot jelenti. Ez igaz, függetlenül attól, hogy a p-t milyen típusú objektumot megcímzQ mutatónak deklaráltuk: a fordító n-et olyan egységekben számlálja, amelyek megfelelnek a p által megcímzett objektum méretének, amely utóbbit p deklarációja határozza meg. A PDP-11-en például a méretfaktor char esetében 1 short-nál 2, long és float esetén 4 és double esetén 8.
Mutatók kivonása szintén megengedett: ha p és q ugyanannak a tömbnek az elemeire mutatnak, akkor p - q a p és q közötti elemek darabszáma. E tényt kihasználva megírhatjuk az strlen újabb változatát:

strlen (s) /*Visszaadja az s karakterlánc hosszát*/
char *s;
{
char *p = s;
while (*p != '\0')
p++;
return (p - s);
}

A deklarációban p kezdeti értékeként s-et adtuk meg, vagyis p kezdetben az s elsQ karakterére mutat. A while ciklusban addig vizsgáljuk az egymást követQ karaktereket, amíg a véget jelzQ \0 elQ nem kerül. Mivel \0 értéke nulla, és mivel a while csupán azt vizsgálja, hogy a kifejezés nulla-e, elhagyható az explicit vizsgálat. Az ilyen ciklusokat gyakran az alábbi alakban írják:

while (*p)
p++;

Minthogy p karakterekre mutat, p++ minden alkalommal a következQ karakterre lépteti p-t, és p - s az átlépett karakterek számát, vagyis a karakterlánc hosszát adja meg. A mutatóaritmetika következetes: ha float mennyiségekkel dolgoznánk, amelyek a char-oknál több tárterületet foglalnak el, és ha p float-ot megcímzQ mutató lenne, akkor p++ a következQ float-ra léptetne. îgy az alloc másik változatát, amely pl. char-ok helyett float változókkal dolgozik, egyszerqen úgy írhatjuk meg, hogy az alloc-ban és free-ben végig float-okra cseréljük a char változókat. Az összes mutatómqvelet automatikusan számításba veszi a megcímzett objektum méretét, így semmi egyebet nem kell megváltoztatni.
Az említett mqveleteken kívül (mutató és integer összeadása és kivonása, két mutató kivonása és összehasonlítása) minden más mutatómqvelet tilos! Nincs megengedve két mutató összeadása, szorzása, osztása, mutatók léptetése, maszkolása, sem pedig float vagy double mennyiségeknek mutatókhoz történQ hozzáadása.

5.5. Karaktermutatók és függvények

Az

"Ez itt egy karakterlánc"

alakú karakterláncállandó nem más, mint egy karaktertömb. A fordító a belsQ ábrázolásban a tömböt a \0 karakterrel zárja le, hogy a programok megtalálhassák a karakterlánc végét. A tárterület hossza tehát eggyel nagyobb, mint az idézQjelek közötti karakterek száma.
A karakterlánc-állandó leggyakrabban talán függvényargumentumokban fordul elQ, mint pl.

printf ("Figyelem, emberek\n");

A programban ily módon megjelenQ karakterlánc karaktermutatón keresztül érhetQ el; printf a karaktertömböt megcímzQ mutatót kap.
A karaktertömböknek természetesen nem kell feltétlenül függvényargumentumoknak lenniük. Ha message-et

char *message;

deklarálja, akkor a

message = "now is the time"; /*Ideje*/

utasítás message-hez a tényleges karaktereket megcímzQ mutatót rendel hozzá. Ez nem karakterlánc-másolás, a dolog csak a mutatókat érinti. A C nyelvben nincsen olyan operátor, amellyel teljes karakterláncot egy egységként dolgozhatnánk fel.
A mutatók és tömbök további vonatkozásait a 7. fejezetben ismertetendQ szabványos be- és kiviteli (I/O) könyvtár két hasznos függvényén keresztül mutatjuk be.
Az elsQ függvény az strcpy(s, t), amely a t karakterláncot az s karakterláncba másolja. Az argumentumokat az értékadáshoz való hasonlóság miatt írtuk ebben a sorrendben, hiszen a t karakterláncnak az s karakterlánchoz történQ hozzárendelésekor azt mondanánk, hogy

s = t

ElQször a tömbös változatot mutatjuk be:

strcpy (s, t) /*t másolása s-be*/
char s [], t [];
{
int i;
i = 0;
while ((s [i] =t [i]) != '\0')
i++;
}

Összehasonlításul íme az strcpy mutatóval írt változata:

strcpy (s, t) /*t másolása s-be;
1. mutatót alkalmazó változat*/
char *s, *t;
{
while ((*s = *t) != '\0') {
s++;
t++;
}
}

Mivel az argumentumok átadása érték szerint történik, strcpy tetszés szerinti módon használhatja s-t és t-t. Az adott esetben ezek alkalmas módon inicializált mutatók, amelyek karakterenként végighaladnak a tömbökön, amíg a t-t lezáró \0 át nem másolódik s-be.
A gyakorlatban strcpy-t nem az elQbb bemutatott módon írnánk meg. Egy másik lehetQség pl. :

strcpy (s, t) /*t másolása s-be;
2. mutatót alkalmazó változat*/
char *s, *t;
{
while ((*s++ = *t++) != '\0')
;
}

Ez a programkód s és t inkrementálását a feltételvizsgálatba helyezi át. *t++ értéke az a karakter, amire t inkrementálás elQtt mutatott; a ++ postfixum mindaddig nem változtatja meg t-t, amíg ez a karakter feldolgozásra nem került. Hasonlóképpen, s inkrementálása elQtt a karakter a régi s pozícióban tárolódik. Egyben ez a karakter lesz az az érték, amelyet a ciklusvezérlés érdekében \0-val összehasonlítunk. Végeredményben a karakterek a záró \0-ig, a záró \0-t is beleértve átmásolódnak t-bQl s-be.
VégsQ lerövidítésként vegyük ismét észre, hogy a \0-val való összehasonlítás redundáns, ezért a függvény gyakran így jelenik meg:

strcpy (s, t) /*t másolása s-be;
3. mutatót alkalmazó változat*/
char *s, *t;
{
while (*s++ = *t++)
;
}

Bár elsQ ránézésre titokzatosnak tqnhet, ez a jelölés nagyon kényelmes, és már csak azért is el kell sajátítanunk, mert gyakran találkozunk vele C programokban.
A másik rutin az strcmp(s, t), amely összehasonlítja az s és t karakterláncokat, és negatív számot, nullát vagy pozitív számot ad vissza aszerint, hogy s lexikografikusan kisebb, mint t, egyenlQ t-vel vagy nagyobb, mint t. A visszaadott értéket úgy nyerjük, hogy az elsQ olyan pozíción, ahol s és t nem egyeznek meg, kivonjuk egymásból a karaktereket.

strcmp (s, t) /*A visszatérQ érték < 0, ha s < t;
0, ha s == t, > 0, ha s > t*/
char s [], t [];
{
int i;
i = 0;
while (s [i] == t [i])
if (s [i++] == '\0')
return (0);
return (s [i] - t [i]);
}

Az strcmp mutató alkalmazásával:

strcmp (s, t) /*A visszatérQ érték < 0, ha s < t;
0, ha s == t; > 0, ha s > t*/
char *s, *t;
{
for (; *s == *t; s++,t++)
if (*s == '\0')
return (0);
return (*s - *t);
}

Mivel ++ és -- akár prefix, akár postfix operátorok lehetnek, ritkábban ugyan, de a * és ++, ill. -- más kombinációi is elQfordulhatnak. Pl.

*++p

p-t még azelQtt inkrementálja, hogy a p által megcímzett karakterhez való hozzáférés megtörténne.

*--p

elQször dekrementálja p-t.

5.2. Gyakorlat. îrjuk át a 2. fejezetben bemutatott strcat függvényt mutató alkalmazásával (strcat(s, t) a t karakterláncot az s karakterlánc végére másolja)!

5.3. Gyakorlat. îrjunk makrót strcpy-ra!

5.4. Gyakorlat. îrjuk át a korábbi fejezetek erre alkalmas programjait és gyakorlatait úgy, hogy tömbindexelés helyett mutatókat használunk! Jó lehetQség pl. a getline (l. az 1. és 4. fejezetet), az atoi az itoa és változataik (l. a 2., 3., 4. fejezetet), valamint az index és a getop (4. fejezet).

5.6. A mutatók nem egész számok

Régebbi C programok igen liberálisan kezelték a mutatók másolásának kérdését. 9ltalában a legtöbb gépen a mutatót hozzá lehetett rendelni egy egész típusú_ mennyiséghez és viszont, anélkül, hogy maga a mutató megváltozott volna sem méretszámítás, sem konverzió nem történt, nem vesztek el bitek. Sajnos azonban ez oda vezetett, hogy sokan szabadosan kezelték a mutatókat visszaadó rutinokat, és a kapott mutatókat egyszerqen más rutinoknak adták át gyakran elmulasztva a szükséges mutatódeklarációkat. Tekintsük pl. az strsave(s) függvényt, amely az alloc hívásával kapott biztos helyre másolja az s karakterláncot, majd az azt megcímzQ mutatóval tér vissza. E program helyesen így fest :

char *strsave (s) /*Elmenti az s karakterláncot*/
char *s;
{
char *p, *alloc ();
if ((p = alloc (strlen (s) + 1)) != NULL)
strcpy (p, s);
return (p);
}

A gyakorlatban erQs a kísértés, hogy elhagyjuk a deklarációkat:

strsave (s) /*Elmenti az s karakterláncot*/
{
char *p;
if ((p = alloc (strlen (s) + 1)) != NULL)
strcpy (p, s);
return (p);
}

Ez sok gépen mqködni fog, mivel a függvények és argumentumok alapértelmezés szerinti típusa int, és az int-ek és mutatók között általában mindkét irányban biztonságosan végezhetQ hozzárendelés. Mégis, az ilyenfajta programkód használata kockázatos, mivel hatása az adott nyelvi megvalósítástól és a gépi architektúrától függ, s így az általunk éppen használt fordító esetében esetleg nem a várt módon mqködik. Ésszerqbb tehát, ha minden egyes deklarációt kiírunk. (Ha errQl mégis elfeledkeznénk, a lint program figyelmeztet az ilyen esetekre.)

5.7. Többdimenziós tömbök

A C nyelv mátrixjellegq többdimenziós tömbök használatát is megengedi, noha a gyakorlatban ilyeneket sokkal ritkábban használunk, mint mutatótömböket. Ebben a szakaszban ezek néhány tulajdonságát mutatjuk be.
Tekintsük a dátumkonverzió problémáját: a hónap adott napjának az év egy napjává és vissza történQ alakítását. Példának okáért a március 1. nem szökQévekben a 60., szökQévekben a 61. nap. Az átalakítások elvégzésére definiáljunk két függvényt: day_of_year a hónapot és napot az év napjává, míg month_day az év adott napját hónappá és nappá alakítja át. Mivel az utóbbi függvény két értékkel tér vissza, a hónap és a nap argumentum mutató lesz:

month_day (1977, 60, &m, &d)

hatására m 3 és d 1 lesz (március 1 .).
Mindkét függvénynek ugyanarra az információra van szüksége, tudniillik az egyes hónapok napjainak számát tartalmazó táblázatra. Mivel a hónapokban levQ napok száma eltér a szökQévekben és a nem szökQévekben, egyszerqbb, ha ezeket egy kétdimenziós tömb két sorában elkülönítjük, mint ha a számítás során próbálnánk nyomonkövetni, hogy mi is történik februárban. A tömb és az átalakításokat végzQ függvények a következQk :

static int day_tab [2][13] = {
{0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
{0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
};

day_of_year (year, month, day) /*Az éven belüli
napsorszám kiszámítása
a hónapból és napból*/
int year, month, day;
{
int i, leap;
leap = year % 4 == 0 && year % 100 != 0
|| year % 400 == 0;
for (i = 1; i < month; i++)
day += day_tab [leap][i];
return (day);
}

month_day (year, yerday, pmonth, pday) /*Hónap és nap
kiszámítása az*/
int year, yearday, *pmonth, *pday; /*év adott sorszámú
napjából*/
{
int i, leap;
leap = year % 4 == 0 && year % 100 != 0
|| year % 400 == 0;
for (i = 1; yearday > day_tab [leap][i]; i++)
yearday -= day_tab [leap][i];
*pmonth = i;
*pday = yearday;
}

A day_tab tömbnek kívül kell lennie mind a day_of_year, mind pedig a month_day függvényen, hogy mindketten használhassák.
A day_tab az elsQ kétdimenziós tömb, amellyel eddig találkoztunk. A C-ben definíció szerint a kétdimenziós tömb valójában olyan egydimenziós tömb, amelynek minden eleme tömb. Ezért írjuk az indexeket

day_tab [i][j]

nem pedig

day_tab [i,j]

alakban, mint a legtöbb más nyelvben. EttQl eltekintve a kétdimenziós tömb ugyanúgy kezelhetQ, mint a többi nyelvben. Az elemek soronként tárolódnak, vagyis a legjobboldalibb index változik a leggyorsabban, amikor az elemekhez a tárolás sorrendjében történik hozzáférés.
A tömböt a kezdeti értékek kapcsos zárójelek közé zárt listájával inicializáljuk; a kétdimenziós tömb minden sorát a megfelelQ allista inicializálja. A day_tab tömböt egy nullákat tartalmazó oszloppal kezdtük, hogy a hónapszámok ne 0-tól 11-ig, hanem a megszokott módon 1 -tQl 12-ig fussanak. Mivel az adott esetben az elfoglalt tárhely mennyisége nem lényeges, ez a megoldás egyszerqbb, mint az indexek kiigazítása.
Ha a kétdimenziós tömböt függvénynek kell átadni, a függvénybeli argumentumdeklarációnak tartalmaznia kell az oszlopméretet; a sorméret közömbös, mivel mint korábban, most is mutatót adunk át. Az adott esetben ez a 13 int-et tartalmazó tömbökre mint objektumokra mutat. îgy, ha a day_tab tömböt kell átadni az f függvénynek, akkor f deklarációja:

f (day_tab)
int day_tab [2][13];
{
. . .
}

Az f-beli argumentumdeklaráció lehetne

int day_tab [][13];

is, mivel a sorok száma közömbös, vagy lehetne

int (*day_tab)[13];

amely azt fejezi ki, hogy az argumentum egy 13 egészbQl álló tömböt jelölQ mutató. A zárójelek szükségesek, mivel [ ] (szögletes zárójelek) precedenciája nagyobb, mint a * szimbólumé, így zárójelek nélkül az

int *day_tab [13];

deklaráció egy 13 darab, egészt megcímzQ mutatóból álló tömböt jelent, amint ezt a következQkben látni fogjuk.

5.8. Mutatótömbök; mutatókat megcímzQ mutatók

Mivel a mutatók maguk is változók, joggal várható, hogy mutatókból álló tömbök is használhatók. Ez valóban így van. E lehetQséget olyan program megírásán keresztül mutatjuk be, amely egy szövegsorokból álló halmazt alfabetikus sorrendbe rendez: ez a UNIX-beli sort rendezQ segédprogram egyszerqsített változata.
A 3. fejezetben bemutattuk a Shell sort függvényét, amely egészekbQl álló tömböt rendez. Ugyanez az algoritmus fog itt is mqködni, attól eltekintve, hogy most szövegsorokkal kell foglalkoznunk, amelyek különbözQ hosszúságúak, és az egészektQl eltérQen nem hasonlíthatók össze, és egyetlen mqvelettel nem mozgathatók. Olyan adatábrázolásra van szükségünk, amely hatékonyan és kényelmesen bírkózik meg változó hosszúságú szövegsorokkal.
Itt lépnek be a mutatókból álló tömbök. Ha a rendezendQ sorokat elejétQl végig egyetlen hosszú karaktertömbben tároljuk (amelyet esetleg az alloc kezel), akkor minden sor elérhetQ az elsQ karakterét megcímzQ mutatón keresztül. Maguk a mutatók egy tömbben tárolhatók. Két sor oly módon hasonlítható össze, hogy mutatóikat átadjuk strcmp-nek. Ha két, nem megfelelQ sorrendben levQ sort meg kell cserélni, akkor a mutatótömbben levQ mutatók cserélQdnek fel, nem pedig maguk a szövegsorok. Ezáltal elkerüljük azt a két összetartozó problémát, amit a bonyolult tárkezelés és a tényleges szövegsorok mozgatásával együttjáró nagy megterhelés jelentene.
A rendezés folyamata három lépésbQl tevQdik össze:

az összes bemeneti sor beolvasása,
a beolvasott sorok rendezése,
a helyes sorrendben történQ kinyomtatás.

Szokás szerint legcélszerqbb, ha a programot olyan függvényekre bontjuk fel, amelyek ezt a természetes felosztást követik, és a fQrutin végzi a folyamat vezérlését.
Egy pillanatra hagyjuk a rendezési lépést és összpontosítsunk az adatstruktúrára, valamint a be- és kivitelre. A bemeneti rutinnak össze kell gyqjtenie, majd tárolnia kell az egyes sorok karaktereit és össze kell állítania a sorokat megcímzQ mutatók tömbjét. Ugyancsak a bemeneti rutinnak kell megszámlálnia a beérkezQ sorokat, mivel erre az információra a rendezéskor és nyomtatáskor szükség lesz. Tekintve, hogy a beolvasófüggvény csak véges számú bemeneti sorral tud megbírkózni, valamiféle nemlétezQ sor-darabszámot, pl. -1-et ad vissza, ha túl sok szöveg érkezett. A kimeneti rutinnak abban a sorrendben kell kinyomtatnia a sorokat, amelyben azok a mutatók tömbjében megjelennek.

#define NULL 0
#define LINES 100 /*A rendezendQ sorok max. száma*/
main () /*Beolvasott sorok rendezése*/
{
char * lineptr [LINES]; /*A szövegsorokatmegcímzQ
mutatók*/
int nlines; /*A beolvasott sorok száma*/
if ((nlines = readlines (lineptr, LINES)) >= 0) {
sort (lineptr, nlines);
writelines (lineptr, nlines);
}
else
printf ("a bemenet túl nagy a rendezéshez \n");
}

#define MAXLEN 1000
readlines (lineptr, maxlines) /*Sorok beolvasása*/
char *lineptr []; /*Rendezéshez*/
int maxlines;
{
int len, nlines;
char *p, *alloc (), line [MAXLEN];
nlines = 0;
while ((len = getline (line, MAXLEN)) > 0)
if (nlines >= maxlines)
return (-1);
else if ((p = alloc (len)) == NULL)
return(-1);
else {
line [len - 1] = '\0'; /*Az újsort levágja*/
strcpy (p, line);
lineptr [nlines++] = p;
}
return (nlines);
}

A sorok végén található újsor karakterek törlQdnek, így nem befolyásolják a rendezési sorrendet.

writelines (lineptr, nlines) /*A kimenetre kerülQ
sorok kiírása*/
char *lineptr [];
int nlines;
{
int i;
for (i = 0; i < nlines; i++)
printf ("%s \n", lineptr [i]);
}

A legfontosabb új dolog lineptr deklarációja:

char *lineptr [LINES];

azt jelenti, hogy a lineptr egy LINES számú elembQl álló, mutatókat tartalmazó tömb, amelynek minden eleme egy-egy char-ra mutat. Más szóval lineptr [i] karaktermutató, és *lineptr [i] egy karakterhez fér hozzá.
Mivel lineptr tömb, amelyet writelines-nak adunk át, pontosan ugyanúgy kezelhetjük mutatóként, mint azt a korábbi példákban láttuk. A függvény tehát így is írható:

writelines (lineptr, nlines) /*A kimenetre kerülQ
sorok kíírása*/
char *lineptr [];
int nlines;
{
while (--nlines >= 0)
printf ("%s \n", *lineptr++);
}

A *lineptr kezdetben az elsQ sorra mutat; minden inkrementálás a következQ sorra lépteti, miközben nlines-t leszámláljuk.
Most, hogy a be- és kimenet a kezünkben van, rátérhetünk a rendezésre. A 3. fejezetben látott Shell rendezQprogramot kismértékben meg kell változtatnunk: módosítani kell a deklarációkat, és az összehasonlítás mqveletét külön függvényben kell elhelyezni. Az alapvetQ algoritmus változatlan, ezért bízhatunk abban, hogy a program továbbra is mqködni fog.

sort (v, n) /*A v [0] ... v [n - 1] karakterláncok
rendezése növekvQ sorrendben*/
char *v [];
int n;
{
int gap, i, j;
char *temp;
for (gap = n/2; gap > 0; gap /= 2)
for (i = gap; i < n; i++)
for (j = i - gap; j >= 0; j -= gap) {
if (strcmp (v [j], v [j + gap]) <=0)
break;
temp = v [j];
v [j] = v [j + gap];
v [j + gap] = temp;
}
}

Mivel v (azaz lineptr) minden egyes eleme karaktermutató, temp-nek is annak kell lennie, hogy az egyik a másikba másolható legyen.
A programot a lehetQ legegyszerqbbre írtuk meg, hogy minél gyorsabban el tudjuk indítani. Lehetne azonban gyorsabb is, ha pl. a bejövQ sorokat közvetlenül a readlines által karbantartott tömbbe másolnánk, nem pedig elQször a line-ba, majd az alloc által kezelt, rejtett helyre. Az elsQ változatot azonban bölcsebb úgy megírni, hogy minél érthetQbb legyen. A hatékonysággal ráérünk késQbb foglalkozni. Programunkon valószínqleg nem sokat gyorsítana, ha kiküszöbölnénk a bemeneti sorok szükségtelen átmásolását. Lényeges javulást csak az hozhat, ha a Shell sort programját valami jobbal, pl. a quicksort programmal cseréljük fel.
Az 1. fejezetben rámutattunk, hogy mivel a while és a for ciklusok a végfeltételt a ciklustörzs elsQ végrehajtása elQtt vizsgálják, hozzájárulnak ahhoz, hogy a programok a lehetQ leggyorsabban mqködjenek, különösen, ha nincs bemenet. Tanulságos, ha végigmegyünk a rendezQprogram függvényein, és megvizsgáljuk, mi történik, ha egyáltalán nincs bemeneti szöveg.

5.5. Gyakorlat. îrjuk újra a readlines függvényt oly módon, hogy a sorokat a main által adott tömbben hozzuk létre és nem az alloc-ot hívjuk a tár karbantartása céljából! Mennyivel gyorsabb így a program?

5.9. Mutatótömbök inicializálása

îrjuk meg a month_name(n) függvényt, amely egy olyan mutatót ad vissza, amely az n-edik hónap nevét tartalmazó karakterláncra mutat. Ez a belsQ static tömb ideális alkalmazása. A month_name a karakterláncok számára külön tömböt tartalmaz, és hívás után a megfelelQ karakterláncot megcímzQ mutatót adja vissza. E szakasz témája az, hogy hogyan kell a nevek tömbjét inicializálni.
A szintaxis hasonlít a korábbi inicializálásokra:

char *month_name (n) /*Visszaadja az n-edik hónap nevét*/
int n;
{
static char *name [] = {
"nem létezQ hónap",
"január",
"február",
"március",
"április",
"május",
"június",
"július",
"augusztus",
"szeptember",
"október",
"november",
"december"
}
return ((n < 1 || n > 12) ? name [0] : name [n]);
}

A name karaktermutatókból álló tömb. Deklarációja ugyanaz, mint lineptr-é a rendezési példában. A kezdeti érték egyszerqen a karakterláncok listája; mindegyik hozzá van rendelve a tömb megfelelQ pozíciójához. Pontosabban szólva az i-edik karakterlánc karakterei valamilyen más helyre kerülnek, és az Qket megcímzQ mutató name [i]-ben található. Mivel a tömb mérete nincs megadva, maga a fordító számlálja meg a kezdeti értékeket és tölti be a helyes darabszámot.

5.10. Mutatók és többdimenziós tömbök

A kezdQ C programozókat gyakran megzavarja a kétdimenziós tömbök és az olyan mutatótömbök közötti különbség, mint amilyen a fenti példában name volt. Pl. az

int a [10][10];
int *b [10];

deklarációkban a és b használata ugyan hasonló, amennyiben a[5][5] és b[5] [5] egyaránt egy bizonyos int-re történQ megengedett hivatkozások. a azonban igazi tömb : 100 tárrekeszt foglaltunk le számára, egy adott elemét a hagyományos mátrixszerq indexszámítással választhatjuk ki. b esetében azonban a deklaráció csupán 10 mutatót foglal le: ezek mindegyikét úgy kell beállítani, hogy egy-egy egészekbQl álló tömbre mutasson. Feltéve, hogy mindegyikük egy-egy tízelemq tömbre mutat, 100 tárrekeszt foglaltunk le, ezenkívül további tíz rekeszre van szükség a mutatók számára. îgy a mutatókból álló tömb valamivel több területet foglal el, és explicit inicializálást igényelhet. Van azonban két elQnye: egyrészt az elemeket nem szorzással és összeadással, hanem mutatón keresztül, indirekt módon érjük el, másrészt a tömb sorai különbözQ hosszúságúak lehetnek. îgy nem szükséges, hogy b minden eleme egy-egy tízelemq vektorra mutasson: lehet köztük amelyik két elemre, esetleg húszra, sQt amelyik egyetlen elemre sem mutat.
Bár az elQbbi fejtegetésben egészekrQl beszéltünk, a mutatótömbök használatának messze leggyakoribb esete az, amelyet a month_name-ben mutattunk be: különbözQ hosszúságú karakterláncok kezelése.

5.6. Gyakorlat. îrjuk át a day_of_year és a month_day rutint oly módon, hogy indexelés helyett mutatókat használjunk!

5.11. Parancssor-argumentumok

A C nyelvet támogató környezetekben lehetQség van arra, hogy a végrehajtás megkezdésekor a programnak parancssor-argumentumokat vagy paramétereket adjunk át. Amikor a végrehajtás megkezdésekor main-t meghívjuk, a hívásban két argumentum szerepel. Az elsQ (amit szokás szerint argc-nek nevezünk) azoknak a parancssor-argumentumoknak a darabszáma, amelyekkel a programot meghívtuk. A második argumentum (argv) egy mutató: ez arra a karakterlánc-tömbre mutat, amely az elQbbi argumentumokat tartalmazza. Egy karakterlánc egy argumentumnak felel meg. E karakterláncok kezelése a többszörös mélységq mutatóhasználat tipikus esete.
A többszintq mutatóhasználathoz szükséges deklarációknak, a módszer alkalmazásának legegyszerqbb szemléltetQ példája az echo program, amely egyszerqen megismétli (visszhangozza) az egy sorban megjelenQ, szóközökkel elválasztott parancssor-argumentumokat. Vagyis, ha kiadjuk az

echo Figyelem, emberek

parancsot, akkor a kimeneten

Figyelem, emberek

jelenik meg.
Megállapodás szerint argv [0] az a név, amellyel a programot hívták, így argc legalább 1 . Az elQbbi példában argc 3 és argv[0], argv [1], ill. argv [2] sorra echo, Figyelem és emberek. Az elsQ igazi argumentum argv [1] és az utolsó argv[n-1]. Ha argc értéke 1, akkor a program nevét nem követik parancssor-argumentumok. Mindezt az echo programban mutatjuk be:

main (argc, argv) /*Visszhangozza az argumentumokat
1. változat*/
int argc;
char *argv [];
{
int i;
for (i = 1; i < argc; i++)
printf ("%s %c", argv [i], (i < argc-1) ? ' ' : '\n');
}

Mivel argv mutatótömböt megcímzQ mutató, többféleképpen is megírhatjuk ezt a programot úgy, hogy a tömb indexelése helyett a mutatót kezeljük. Lássunk két változatot :

main (argc, argv) /*Visszhangozza az argumentumokat ;
2. változat*/
int argc;
char *argv [];
{
while (--argc > 0)
printf ("%s %c", *++argv,(argc > 1) ? ' ' : '\n');
}

Mivel argv az argumentum-karakterláncok tömbjének kezdetét megcímzQ mutató, 1-gyel történQ inkrementálásának (++argv) hatására argv[0] helyett az eredeti argv[1]-re fog mutatni. Az egymást követQ inkrementálások hatására mindig a következQ argumentumra lép; *argv az illetQ argumentumot megcímzQ mutató. Ezzel egyidejqleg argc dekrementálódik: amikor nullává válik, nincs több kinyomtatandó argumentum.
Egy másik változat :

main (argc, argv) /*Visszhangozza az argumentumokat
3. változat*/
int argc;
char *argv [];
{
while (--argc > 0)
printf ((argc > 1) ? "%s" : "%s \n", *++argv);
}

Ez a változat azt mutatja be, hogy a printf formátumargumentuma éppúgy lehet kifejezés, mint bármilyen más függvényé. Ez a fajta használat nem túl gyakori, de érdemes rá emlékezni.
Második példaként végezzünk néhány javítást a 4. fejezet mintakeresQ programjában. Talán emlékezünk arra, hogy a keresési minta mélyen a programon belül helyezkedett el, ami nem valami kellemes megoldás. Az UNIX grep segédprogramjának fonalát követve változtassuk meg a programot oly módon, hogy az összehasonlítandó mintát a parancssor elsQ argumentuma adja meg.

#define MAXLINE 1000
main (argc, argv) /*Megkeresi az 1. argumentum
szerinti mintát*/
int argc;
char *argv [];
{
char line [MAXLINE];
if (argc != 2)
printf ("Mintakeresés \n");
else
while (getline (line, MAXLINE) > 0)
if (index (line, argv [1]) >= 0)
printf ("%s", line);
}

Most kidolgozhatjuk az alapmodellt, amivel a további mutatóalkalmazásokat szemléltethetjük. Tegyük fel, hogy két opcionális (szabadon választható) argumentumot engedünk meg. Az egyik azt mondja,hogy "nyomtass ki minden sort, kivéve azokat, amelyek illeszkednek a mintára", míg a második azt mondja, "írd minden kinyomtatott sor elé annak sorszámát".
A C programokban a mínusz jellel kezdQdQ argumentumok megállapodásszerqen opcionális jelzQt (flaget) vagy paramétert jelentenek. Ha tehát az inverzió (a kivéve, a fordítottság) jelölésére -x-et választunk, és a sorszámozást -n-nel kérjük, akkor a

find -x -n the

parancs a

Now is the time
for all good men
to come to the aid
of their party.

bemeneti szöveg esetén a

2: for all good men

kimenetet fogja eredményezni.
Az opcionális argumentumok sorrendje tetszQleges kell, hogy legyen, és a program további részének nem szabad függenie a ténylegesen megadott argumentumok számától. Az adott esetben az index hívásának nem szabad argv[2]-re hivatkoznia olyankor, amikor csupán egyetlen opcionális argumentum volt, vagy argv[ 1 ]-re, ha egyáltalán nem volt ilyen argumentum. A felhasználók számára kényelmes továbbá, ha az opcionális argumentumok konkatenálhatók, mint pl.:

find -nx the

îme a program:

#define MAXLINE 1000
main (argc, argv) /*Megkeresi az 1. argumentum
szerinti mintát*/
int argc;
char *argv [];
{
char line [MAXLINE], *s;
long lineno = 0;
int except = 0, number = 0;
while (--argc > 0 && (*++argv) [0] == '-')
for (s = argv [0] + 1; *s != '\0'; s++)
switch (*s) {
case 'x':
except = 1;
break;
case 'n':
number = 1;
break;
default:
printf ("keresés: illegális opció %c \n", *s);
argc = 0;
break;
}
if (argc != 1)
printf ("Mintakeresés: -x -n \n");
else
while (getline (line, MAXLINE) > 0) {
lineno++;
if ((index (line, *argv) >= 0) != except) {
if (number)
printf ("%1d: ", lineno);
printf ("%s", line);
}
}
}

argv minden egyes opcionális argumentum elQtt inkrementálódik, argc pedig dekrementálódik. Ha nincsenek hibák, akkor a ciklus végén argc értéke 1 és *argv a mintára mutat. Vegyük észre, hogy *++argv argumentum-karakterláncot megcímzQ mutató: (*++argv) [0] az elsQ karaktere. A zárójelek szükségesek, mivel nélkülük a kifejezés *++(argv[0]) lenne, aminek az értelme teljesen más (és rossz). Megengedett alak lenne még *++argv is.

5.7. Gyakorlat.îrjuk meg az add nevq programot, amely kiértékel egy, a parancssorban szereplQ fordított lengyel alakú kifejezést! Például
add 2 3 4 + *
2 * (3+4)-et számítja ki.

5.8. Gyakorlat. Módosítsuk az entab és detab programokat (amelyeket az 1. fejezetben gyakorlatként írtunk meg) oly módon, hogy az a tabulátor stop-ok listáját argumentumokként fogadja! Argumentum megadásának hiányában a normál tabulátorbeállításokat használjuk!

5.9. Gyakorlat. BQvítsük az entab és detab programokat úgy, hogy elfogadják az
entab m +n
rövidített jelölést, amely az m-edik oszloptól kezdve minden n-edik oszlopon egy-egy tabulátor stop-ot jelent. Válasszunk (a felhasználó számára) kényelmes magatartást az alapesetre (default)!

5.10. Gyakorlat. îrjuk meg a tail nevq programot, amely kinyomtatja az utolsó n bemeneti sort! Az n alapértelmezése legyen pl. 10, amit opcionális argumentum változtathat meg, tehát
tail - n
az utolsó n sort nyomtatja ki. A programnak elfogadhatóan kell viselkednie, függetlenül attól, hogy mennyire értelmetlen a bemenet vagy n értéke. Irjuk meg úgy a programot, hogy a legjobban kihasználja a rendelkezésre álló tárterületet: a sorokat tároljuk úgy, mint a sort-ban, nem pedig rögzített méretq kétdimenziós tömbben!

5.12.Függvényeket megcímzQ mutatók

A C nyelvben maga a függvény nem változó, de mód vanfüggvényt megcímzQ mutató definiálására, amellyel mqveletek végezhetQk, függvényeknek átadható, tömbökbe helyezhetQ stb. Mindezt a fejezet korábbi részében bemutatott rendezQprogram módosításával szemléltetjük: ha megadjuk a -n opcionális argumentumot, akkor a program nem lexikografikusan, hanem numerikusan rendezi a bejövQ sorokat.
A rendezés gyakran három részbQl áll - összehasonlításból, amely tetszQleges objektumpár sorrendjét határozza meg, cserébQl, amely megfordítja ezek sorrendjét és rendezQalgoritmusból, amely mindaddig végzi az összehasonlításokat és cseréket, amíg az objektumok a helyes sorrendbe nem kerülnek. Maga a rendezQalgoritmus független az összehasonlítási és felcserélési mqveletektQl, így különbözQ összehasonlító és felcserélQ függvényeket átadva különbözQ kritériumok szerint rendezhetünk. Ezt az elvet alkalmazzuk az új rendezQprogramban.
A korábbiaknak megfelelQen két sor lexikografikus összehasonlítását az strcmp, felcserélését pedig a swap végzi. Szükségünk lesz még a numcmp rutinra, amely numerikus értékük alapján hasonlít össze két sort, és strcmp-hez hasonlóan valamiféle feltételjelzést ad vissza. Ezt a három függvényt a mainben deklaráljuk, és az Qket megcímzQ mutatókat adjuk át sort-nak. A sort viszont a mutatókon keresztül hívja a függvényeket. Az argumentumokkal kapcsolatos hibafeldolgozást elhanyagoltuk, hogy a fQ feladatokkal foglalkozhassunk.

#define LINES 100 /*A rendezendQ sorok
maximális száma*/
main (argc, argv) /*Beolvasott sorok rendezése*/
int argc;
char *argv [];
{
char *lineptr [LINES]; /*Mutatók a szövegsorokra*/
int nlines; /*A beolvasott sorok száma*/
int strcmp (), numcmp (); /*Összehasonlító
függvények*/
int swap (); /*FelcserélQ függvény*/
int numeric = 0; /*1, ha a rendezés numerikus*/
if (argc > 1 && argv [1][0] == '-' && argv [1][1] == 'n')
numeric = 1;
if ((nlines = readlines (lineptr, LINES)) >= 0) {
if (numeric)
sort(lineptr, nlines, numcmp, swap);
else
sort (nlineptr, lines, strcmp, swap);
writelines (lineptr, nlines);
} else
printf ("a bemenet túl nagy a rendezéshez \n");
}

Az strcmp, numcmp és swap - függvények címei; mivel ezek bizonyosan függvények, az & operátor ugyanúgy nem szükséges, mint ahogy nincs rá szükség a tömbök neve elQtt sem. A fordító gondoskodik a függvény címének átadásáról.
Második lépésben a sort-ot módosítjuk:

sort (v, n, comp, exch) /*A v[0] ... v[n-1] karakterláncok
rendezése növekvQ sorrendbe*/
char *v [];
int n;
int (*comp) () , (*exch) ();
{
int gap, i, j;
for (gap = n / 2; gap > 0; gap /= 2)
for (i = gap; i < n; i++)
for (j = i - gap; j >= 0; j -= gap) {
if ((*comp) (v [j], v [j + gap]) <= 0)
break;
(*exch) (&v [j], &v [j + gap]);
}
}

Vizsgáljuk meg a deklarációkat!

int (*comp) ()

azt fejezi ki, hogy a comp olyan függvényt megcímzQ mutató, amely int-et ad vissza. Az elsQ zárójelpár szükséges, nélkülük

int *comp ()

azt fejezné ki, hogy a comp olyan függvény, amely int-et megcímzQ mutatót ad vissza, ami egészen más dolog. comp használata az

if ((*comp) (v [j],v [j + gap]) <= 0)

sorban összhangban van a deklarációval: comp a függvényt megcímzQ mutató, *comp a függvény, és

(*comp) (v [j], v [j + gap])

annak hívása. Ahhoz. hogy az összetevQk helyesen kapcsolódjanak, szükség van a zárójelekre.
Korábban már láttuk strcmp-t, amely két karakterláncot hasonlít össze. îme numcmp amely vezetQ numerikus értékeik szerint hasonlít össze két karakterláncot :

numcmp (s1, s2) /*s1 és s2 numerikus összehasonlítása*/
char *s1 , *s2;
{
double atof (), v1 , v2;
v1 = atof (s1);
v2 = atof (s2);
if (v1 < v2)
return (-1);
else if (v1 > v2)
return (1);
else
return (0);
}

Az utolsó lépés a két mutató felcserélését végzQ swap függvény megírása. Ezt közvetlenül arra alapozhatjuk, amit a fejezet korábbi részében már közöltünk:

swap (px, py) /* *px és *py felcserélése*/
char *px [], *py [];
{
char *temp;
temp = *px;
*px = *py;
*py = temp;
}

A rendezQprogram további opciók egész sorával egészíthetQ ki; ezek közül néhány érdekes gyakorlat lehet.

5.11. Gyakorlat. Módosítsuk a sort programot oly módon, hogy kezelje -r-t, amely ellentétes irányú (csökkenQ) rendezést ír elQ! -r-nek természetesen -n-nel is mqködnie kell!

5.12. Gyakorlat. Illesszük hozzá a -f opciót, amellyel egyesítjük a kis- és nagybetqket úgy, hogy azokat a rendezés során nem különböztetjük meg: a kis- és nagybetqs adatokat együtt rendezzük, tehát a és A szomszédosként jelennek meg, nem választja el Qket az egész ábécé.

5.13. Gyakorlat. Illesszük a függvényhez a -d (szótári rendezés) opciót, amely csak betqket, számokat és szóközöket hasonlít össze! Ügyeljünk arra, hogy -f-fel együtt is mqködjön!

Gyakorlat. Egészítsük ki a függvényt mezQkezelQ szolgáltatással, amely lehetQvé teszi, hogy sorokon belül kijelölt mezQkön is végezhetQ legyen rendezés, mégpedig minden mezQn egymástól független opciókészlet szerint! (E könyv angol eredetijének tárgymutatóját kulcsszavakra -dffel lapszámokra -n-nel rendezték.)

6. fejezet : Struktúrák

A struktúra egy vagy több, esetleg különbözQ típusú változó együttese, amelyeket az egyszerq kezelhetQség érdekében gyqjtünk össze. A struktúrákat egyes nyelvekben, legismertebben a PASCAL-ban rekordoknak nevezik.
A struktúrát szemléltetQ hagyományos példa a bérfizetési lista: a dolgozót egy attributumkészlettel jellemezzük, amelyben helyet kap az illetQ neve, címe, társadalombiztosítási száma, bére stb. Ezek némelyike akár struktúra is lehet: a név, a cím vagy éppen a bér maga is több részbQl állhat.
A struktúrák különösen a nagy méretq programok esetében nyújtanak segítséget bonyolult adathalmazok szervezésében, mivel sokszor lehetQvé teszik, hogy az összetartozó változók együttesét egy egységként, nem pedig különálló elemekként kezeljük. Ebben a fejezetben megkíséreljük bemutatni a struktúrák használatát. Mintaprogramjaink a könyvünkben megszokottaknál terjedelmesebbek lesznek, de még mindig elégszerény méretqek.

6.1. Alapfogalmak

Vegyük ismét elQ az 5. fejezetben tárgyalt dátumkonverziós rutinokat. Egy-egy adat több részbQl áll, ilyenek a nap, a hónap és az év, továbbá esetleg a nap sorszáma az évben, és a hónap neve. Ez az öt változó az alábbi módon egyetlen struktúrába foglalható:

struct date {
int day;
int month;
int year;
int yearday;
char mon_name [4];
}

A struct kulcsszó a struktúra deklarációját vezeti be, amely nem más, mint egy kapcsos zárójelek közé zárt deklarációlista. A struct kulcsszót struktúracímke követheti (mint pl. az elQbb a date). Ez egy név, amely megnevezi az adott típusú struktúrát, és a továbbiakban rövidítésként használható a részletes deklaráció helyett.
A struktúrában elQforduló elemeket, ill. változatokat tagoknak nevezzük. Valamely struktúratagnak, ill. -elemnek és egy közönséges (vagyis nem tag) változónak lehet ugyanaz a neve: ebbQl nem származik zavar, mivel ezek a szövegkörnyezet alapján mindig megkülönböztethetQk. Az már természetesen stílus kérdése, hogy az ember ugyanazokat a neveket csak szorosan összetartozó objektumok esetében használja.
A tagok listáját lezáró jobboldali zárójelet a változólista követheti ugyanúgy, mint minden alaptipusnál. Eszerint

struct { . . . } x, y, z;

szintaktikusan hasonló az

int x, y, z;

sorhoz abban az értelemben, hogy mindkét utasítás a megnevezett tipusú változóként deklarálja x-et, y-t és z-t, ill. helyet foglal számukra.
Az olyan struktúradeklaráció, amit nem követ változólista, nem foglal tárhelyet, csupán a struktúra alakját (template) írja le. H azonban a deklaráció cimkézett, akkor ez a cimke a késQbbiekben a struktúra konkrét elQfordulásakor a definíciókban használható. Ha pl. adott a date elQbbi deklarációja, akkor

struct date d;

ogy definiálja a d változót, hogy az date tipusú struktúra legyen. A külsQ, ill. a statikus struktúra úgy inicializálható, hogy definícióját az elemek kezdeti értékeibQl álló lista követi:

struct date d = {4, 7, 1776, 186, "júl"};

Valamely struktúra adott tagjára kifelyezésen belül a

struktúranév.tag

alakú szerkezettel lehet hivatkozni.
A "." struktúratag operátor a struktúra nevét és a tag nevét kapcsolja össze. Ha pl. a d struktúrabeli dátum alapján akarjuk leap-et (a szökQévet) beállítani, akkor a kód

leap = d.year % 4 == 0 && d.year % 100 != 0
|| d.year % 400 == 0;

lesz. A hónap nevének vizsgálata:

if (strcmp (d.mon_name, "aug") == 0) . . .

Ha a hónapneveket az angol helyesírás szerint nagy kezdQbetqkkel írtuk volna, kisbetqssé alakításuk a következQképpen történhetne:

d.mon_name [0] = lower (d.mon_name [0]);

A struktúrák egymásba skatulyázhatók; a fizetési jegyzék pl. így nézhet ki:

struct person {
char name [NAMESIZE];
char address [ADRSIZE];
long zipcode;
long ss_number;
double salary;
struct date birthdate;
struct date hiredate;
};

A person nevq struktúra két dátumot tartalmaz. Ha az emp-et mint

struct person emp;

deklaráljuk, akkor

emp.birthdate.month

a születés hónapjára vonatkozik. A .struktúratag operátor balról jobbra köt.

6.2. Struktúrák és függvények

A C nyelvbeli struktúrákra számos megkötés vonatkozik. Ezek közül a leglényegesebb, hogy struktúrán csak kétféle mqvelet végezhetQ - a struktúra címének elQállítása az & szimbólum használatával, és a struktúra valamelyik tagjához való hozzáférés. EbbQl következQleg a struktúrákat egy egységként nem lehet semmihez sem hozzárendelni (értékül adni), ill. másolni, nem adhatók át függvényeknek, és a függvények sem adhatnak vissza struktúrákat. (A C nyelv késQbb megjelenQ változataiból ezek a megkötések ki fognak maradni.) A struktúrákat megcimzQ mutatókra már nem vonatkoznak ezek a korlátozások, így a struktúrák és a függvények kényelmesen együtt tudnak mqködni. Végezetül, az automatikus tömbökhöz hasonlóan az automatikus struktúrák sem inicializálhatók, ez csak a külsQ és a statikus struktúrák esetében lehetséges.
Vizsgáljunk meg e jellegzetességek közül néhányat! Példaként írjuk át az elQzQ fejezetben látott dátumkonverziós rutint, struktúrák használatával! Mivel a szabályok nem engedik meg, hogy struktúrát függvénynek közvetlenül átadjunk, vagy külön-külön az elemeket, vagy az egészt megcímzQ mutatót kell átadnunk. Az elsQ lehetQség úgy használja day_of_year-t, ahogy az 5. fejezetben leírtuk:

d.yearday = day_of_year(d.year, d.month, d.day);

A másik lehetQség a mutatóátadás. Ha a hiredate-et

struct date hiredate;

alakban deklaráltuk és day_of_year-t átírtuk, akkor

hiredate.yearday = day_of_year (&hiredate);

segítségével a hiredate-et megcímzQ mutatót átadhatjuk day_of_year-nek. A függvényt módosítani kell, mivel argumentuma a korábbi változólista helyett most mutató lett:

day_of_year (pd) /*Az év napjának elQállítása a
hónapból és a napból*/
struct date *pd;
{
int i, day, leap;
day = pd->day;
leap = pd->year % 4 == 0 && pd->year % 100 != 0
|| pd->year % 400 == 0;
for (i = 1; i < pd->month; i++)
day += day_tab [leap][i];
return (day);
}

A

struct date *pd;

deklaráció szerint pd olyan mutató, amely date típusú struktúrára mutat. A

pd->year

példa szerinti jelölésmód új. Ha p struktúrát megcímzQ mutató, akkor

p->struktúratag

az adott tagra vonatkozik. (A -> operátor mínuszjelbQl és az azt követQ >-ból áll.)
Mivel pd a struktúrára mutat, a year tagra a

(*pd).year

alakban is hivatkozhatunk, de struktúrákat jelölQ mutatókat olyan gyakran használunk, hogy kényelmes rövidítésként a -> jelölésmód is rendelkezésre áll. A (*pd).year alakban a zárójelek szükségesek, mivel a . struktúratag operátor precedenciája magasabb, mint a * operátoré. Mind ->, mind pedig . balról jobbra köt, így

p->q->memb
emp.birthdate.month

értelme:

(p->)->memb
(emp.birthdate).month

A teljesség kedvéért íme a másik függvény, month_day, amit szintén a struktúra használatával írunk át :

month_day (pd) /*Hónap és nap elQállítása az év napjából*/
struct date *pd;
{
int i, leap;
leap = pd->year % 4 == 0 && pd->year % 100 != 0
|| pd->year % 400 == 0;
pd->day = pd->yearday;
for (i = 1; pd->day > day_tab [leap][i]; i++)
pd->day -= day_tab [leap][i];
pd->month = i;
}

A -> és . struktúraoperátorok, valamint az argumentumlistákat közrefogó () és az indexet tartalmazó [] a precedenciahierarchia csúcsán áll, és ezért nagyon szorosan köt. Ha pl. adott a

struct {
int x;
int *y;
} *P;

deklaráció, akkor

++p->x

nem p-t, hanem x-et inkrementálja, mivel az alapértelmezés szerinti zárójelezés:

++(p->x)

Zárójelek használatával a kötés megváltoztatható: (++p)->x az x-hez való hozzáférés elQtt növeli p-t, míg (p++)->x azt követQen inkrementál. (Az utóbbi zárójelkészlet felesleges. Miért?)
Hasonlóképpen, *p->y behozza, amire p mutat; *p->y++ azután inkrementálja y-t, miután hozzáfért ahhoz, amire mutat (éppúgy, mint *s++); (*p->y) ++ azt növeli, amire y mutat, míg *p++->y azután inkrementálja p-t, hogy hozzáfért ahhoz, amire y mutat.

6.3. Struktúratömbök

A struktúrák különösen alkalmasak egymással összefüggQ változók tömbjeinek kezelésére. Tekintsük pl. azt a programot, amely a C nyelv kulcsszavainak elQfordulásait számlálja. A nevek tárolásához szükségünk lesz egy karakterlánc-tömbre, a darabszámok tárolásához pedig egy egészekbQl álló tömbre. Az egyik megoldás szerint párhuzamosan két tömböt használunk. Legyenek ezek pl. keyword és keycount:

char *keyword [NKEYS];
int keycount [NKEYS];

Azonban maga a tény, hogy a tömbök párhuzamosak, jelzi, hogy lehetQségünk van másfajta szervezésre is. Valójában minden kulcsszóbejegyzés egy pár:

char *keyword;
int keycount;

Hozzuk létre a párok tömbjét! A

struct key {
char *keyword;
int keycount;
} keytab [NKEYS];

struktúradeklaráció az ilyen típusú struktúrák keytab nevq tömbjét definiálja és tárterületet foglal le számára. A tömb minden eleme struktúra. Ezt így is írhatjuk:

struct key {
char *keyword;
int keycount;
};
struct key keytab [NKEYS];

Mivel a keytab struktúra jelen esetben a nevek állandó halmazát tartalmazza, legegyszerqbb, ha definiáláskor egyszer és mindenkorra inicializáljuk. A struktúrák inicializálása mindenben hasonlít a korábbiakhoz - a definíciót a kezdeti értékek kapcsos zárójelek közé zárt listája követi:

struct key {
char *keyword;
int keycount;
} keytab [] = {
"break", 0,
" case", 0,
" char", 0,
" continue", 0,
" default", 0,
/* ... */
"unsigned" , 0,
"while", 0,
};

A kezdeti értékeket a struktúratagoknak megfelelQen páronként soroltuk fel. Pontosabb lenne, ha minden sor vagy struktúra kezdeti értékeit zárnánk kapcsos zárójelek közé

{"break", 0},
{" case", 0},
. . .

szerint, de a belsQ kapcsos zárójelekre nincs szükség, ha a kezdeti értékek egyszerq változók vagy karakterláncok, mint a jelen esetben is. Amennyiben kezdeti értékek vannak megadva, és a [] üresen maradt, a fordító most is ezek alapján számítja ki a keytab tömb elemeinek számát.
A kulcsszó-számláló program a keytab definiálásával kezdQdik. A fQrutin a getword függvény ismételt hívásával szavanként olvassa a bemenetet. A program minden szót megkeres a keytab-ben a bináris keresQfüggvénynek a 3. fejezetben látott változatával. (A helyes mqködés érdekében a kulcsszavak listáját természetesen növekvQ sorrendben kell megadnunk.)

#define MAXWORD 20
main () /*C kulcsszavak számlálása*/
{
int n, t;
char word [MAXWORD];
while ((t = getword (word, MAXWORD)) != EOF)
if (t == LETTER)
if ((n = binary (word, keytab, NKEYS)) >= 0)
keytab [n].keycount++;
for (n = 0; n < NKEYS; n++)
if (keytab [n].keycount > 0)
printf ("%4d %s \n",
keytab [n].keycount, keytab [n].keyword);
}

binary (word, tab, n) /*Szó megkeresése tab[0] . . .
tab[n-1]-ben*/
char *word;
struct key tab [];
int n;
{
int low, high, mid, cond;
low = 0;
high = n - 1;
while (low <= high) {
mid = (low + high) / 2;
if ((cond = strcmp (word, tab [mid].keyword)) < 0)
high = mid - 1;
else if (cond > 0)
low = mid + 1;
else
return (mid);
}
return (-1);
}

Rövidesen sor kerül a getword függvény bemutatására; egyelQre elég annyit tudnunk, hogy minden alkalommal, amikor megtalál egy kulcsszót, a LETTER értéket adja vissza és az illetQ szót az elsQ argumentumába másolja.
Az NKEYS mennyiség a kulcsszavak száma a keytab-ban. Bár ezt magunk is megszámlálhatnánk, sokkal könnyebb és biztonságosabb, ha a gépre bízzuk, különösen, ha a lista változhat. Az egyik lehetQség az lenne, hogy a kezdeti értékek listáját a nulla mutatóval zárjuk le, majd ciklusban addig haladunk keytab-on, amíg a végét meg nem találtuk.
Ez azonban több, mint amire szükség van, mivel a fordító a tömb méretét fordítás közben pontosan meghatározza. A bejegyzések száma ebbQl:

keytab mérete / struct key mérete

A C-ben rendelkezésünkre áll a sizeof egyoperandusú operátor, amelynek segítségével bármilyen objektum mérete fordítási idQben meghatározható. A

sizeof (objektum)

kifejezés eredménye olyan egész szám, amely megegyezik a megadott objektum méretével. (A méretet byte-nak nevezett specifikálatlan egységekben kapjuk, amelynek mérete ugyanaz, mint egy char-é.) Az objektum valamilyen aktuális változó, tömb vagy struktúra, vagy pedig valamilyen alap-, ill. leszármaztatott tipus (l. int vagy double, ill. struktúrák) neve lehet. Esetünkben a kulcsszavak száma a tömbméret osztva egy tömbelem méretével. Ezt a számítást #define utasításban használva, állítjuk be az NKEYS értékét:

#define NKEYS (sizeof (keytab) / sizeof (struct key))

Most pedig nézzük a getword függvényt. A getword-nek az adott program számára szükségesnél általánosabb változatát írtuk meg, amely azonban nem lényegesen bonyolultabb, getword a bemeneten soron következQ szót adja vissza, ahol a szó vagy betqk és számok betqvel kezdQdQ lánca, vagy pedig egyetlen karakter. Az objektum tipusát függvényértékként kapjuk meg; ez az érték LETTER, ha az adott egység szó, EOF az állomány végén, vagy maga a karakter, ha az nem alfabetikus.

getword (w,lim) /*Vedd a következQ szót a bemenetrQl*/
char *w;
int lim;
{
int c,t;
if (type (c = *w++ = getch ()) != LETTER) {
*w = '\0';
return (c);
}
while (--lim > 0) {
t = type (c = *w++ = geth ());
if (t != LETTER && t != DIGIT) {
ungetch (c);
break;
}
}
*(w - 1) = '\0';
return (LETTER);
}

A getword a getch és ungetch rutinokat használja, amelyeket a 4. fejezetben írtunk meg. Amikor egy alfabetikus szövegegység begyqjtése befejezQdik, a getword már a szükségesnél egy karakterrel többet olvasott be. Az ungetch hívásával ezt a karaktert a getword következQ hívásáig visszaírjuk a bemenetre.
A getword a type hívásával állapítja meg az egyes bemeneti karakterek típusát. Az alábbi változat csak az ASCII karakterkészletben mqködik:

type (c) /*ASCII karakter típusának visszaadása*/
int c;
{
if (c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z')
return (LETTER);
else if (c >= '0' && c <= '9')
return (DIGIT);
else
return (c);
}

A LETTER és a DIGIT szimbolikus állandó minden olyan értéket felvehet, amely nem ütközik a nem-alfanumerikus karakterekkel és az EOF-fal, kézenfekvQ pl. az alábbi választás:

#define LETTER 'a'
#define DIGIT '0'

A getword-öt felgyorsíthatjuk, ha a type függvény hívásait valamely alkalmas type [] tömbre vonatkozó hivatkozásokkal helyettesítjük. A szabványos C könyvtárban rendelkezésünkre állnak az isalpha és isdigit nevq makrók, amelyek ily módon mqködnek.

6.1. Gyakorlat. Végezzük el a getword-nek ezt a módosítását, és mérjük meg a program sebességének változását!

6.2. Gyakorlat. îrjuk meg a type olyan változatát, amely független a karakterkészlettQl!

6.3. Gyakorlat. Irjuk meg a kulcsszószámláló program olyan változatát, amely nem számlálja az idézQjelek közé zárt karakterláncokban elQforduló kulcsszavakat!

6.4. Struktúrákat megcímzQ mutatók

A mutatókkal és struktúratömbökkel kapcsolatos megfontolásaink szemléltetésére írjuk újra a kulcsszószámláló programot, ezúttal tömbindexek helyett mutatók használatával!
A keytab külsQ deklarációját nem kell megváltoztatnunk, a main és a binary azonban módosításra szorul.

main () /*C kulcsszavak számlálása;
mutatót alkalmazó változat*/
{
int t;
char word [MAXWORD];
struct key *binary (), *p;
while ((t = getword (word, MAXWORD)) != EOF)
if (t == LETTER)
if ((p = binary (word, keytab, NKEYS)) != NULL)
p->keycount++;
for (p = keytab; p < keytab + NKEYS; p++)
if (p->keycount > 0)
printf ("%4d %s \n", p->keycount, p->keyword);
}

struct key *binary (word, tab, n) /*Szó keresése*/
char *word; /*tab [0] . . . tab [n-1]-ben*/
struct key tab [];
int n;
{
int cond;
struct key *low = &tab [0];
struct key *high = &tab [n - 1]; struct key *mid;
while (low <= high) {
mid = low + (high - low) / 2;
if ((cond = strcmp (word, mid->keyword)) < 0)
high = mid - 1;
else if (cond > 0)
low = mid + 1;
else
return (mid);
}
return (NULL);
}

Ebben a programban több dolog is említésre méltó. ElQször is binary deklarációjának jeleznie kell, hogy key típusú struktúrát megcímzQ mutatót ad vissza, és nem egész típusú mennyiséget; ezt mind a main-ben, mind binaryban deklaráltuk. Ha binary megtalálta a szót, az azt kijelölQ mutatót adja vissza; ha a keresés eredménytelen, a visszaadott érték NULL.
Másodszor, a keytab elemeihez történQ minden hozzáférés mutatókkal történik. Emiatt a binary rutin jelentQsen megváltozik: a középsQ elem kiszámítása már nem lehet egyszerqen

mid = (low + high) / 2

mivel két mutató összeadása semmiféle értelmes választ nem eredményez (még a 2-vel való osztáskor sem), sQt ez a mqvelet tiltott ! Ehelyett a

mid = low +(high - low) / 2

alakra van szükség, amely úgy állítja be mid-et, hogy az a low és a high közötti terület felezQpontjára mutasson.
Figyeljük meg low és high kezdeti értékeit is! LehetQség van arra, hogy egy mutatót valamely korábban definiált objektum címével inicializáljunk: itt éppen ezt tettük.
A main-ben azt írtuk, hogy

for (p = keytab; p < keytab + NKEYS; p++)

Ha p struktúrát megcímzQ mutató, akkor minden p-re vonatkozó aritmetikai számítás figyelembe veszi a struktúra tényleges méretét, így p++ a p-t a megfelelQ mennyiséggel inkrementálja ahhoz, hogy elQálljon a struktúrák tömbjének következQ eleme. Ne higyjük azonban, hogy a struktúra mérete megegyezik az elemei méretének az összegével - a különbözQ jellegq objektumok elhelyezkedési követelményeinek folytán lyukak lehetnek a struktúrában.
Végezetül egy megjegyzés a program alakjával kapcsolatban. Ha egy függvény bonyolult típust ad vissza, mint a

struct key *binary(word, tab, n)

esetben, akkor a függvény nevét esetleg nehéz észrevenni, vagy szövegszerkesztQ programmal (text editorral) megtalálni. Emiatt néha más formát szokás használni:

struct key
binary (word, tab, n)

Ez természetesen leginkább személyes ízlés kérdése: válasszuk ki a nekünk tetszQ alakot, és ahhoz tartsuk magunkat.

6.5. Önhivatkozó struktúrák

Tegyük fel, hogy általánosabb feladatként a bemeneti szöveg összes szavának elQfordulásait akarjuk megszámlálni. Mivel a szavak listája elQzetesen nem ismert, azt nem tudjuk alkalmas módon rendezni és nem használhatunk bináris keresést. Lineáris keresést azonban végre tudunk hajtani minden beérkezQ szóra, amivel megnézzük, hogy volt-e már ilyen szó: a program futása azonban így örökké fog tartani. (Pontosabban szólva a várható futási idQ a beolvasott szavak számával négyzetesen nQ.) Hogyan szervezzük meg az adatokat ahhoz, hogy hatékonyan meg tudjunk bírkózni a tetszQleges szavakból álló listával?
Az egyik megoldás szerint állandóan rendezett állapotban tartjuk a már megvizsgált szavakat oly módon, hogy a beérkezés sorrendjében minden szót a neki megfelelQ helyre teszünk. Ezt azonban nem úgy végezzük el, hogy a szavakat egy lineáris tömbben tologatjuk, mivel ez is túl sokáig tartana. Ehelyett a bináris fa nevq adatstruktúrát fogjuk használni.
A fa minden különbözQ szóhoz egy-egy csomópontot rendel, amelynek tartalma :

a szó szövegét megcímzQ mutató,
a szó elQfordulásainak száma,
a bal oldali gyermek (leszármazott) csomópontot megcímzQ mutató,
a jobb oldali gyermek csomópontot megcímzQ mutató.

Egyetlen csomópontnak sem lehet kettQnél több gyermeke; lehet viszont nulla vagy egy gyermeke.
A csomópontokat úgy hozzuk létre, hogy minden egyes csomópont esetében a bal oldali részfa csupa olyan szót tartalmaz, amely kisebb, mint a csomópontbeli szó, míg a jobb oldali részfában csupa olyan szó van, amely nála nagyobb. Ha el akarjuk dönteni, hogy egy új szó már rajta van-e a fán, a vizsgálatot a fa gyökerénél kezdjük, és az új szót az illetQ csomópontban tárolt szóval hasonlítjuk össze. Ha megegyeznek, a válasz igenlQ. Ha az új szó kisebb, mint a csomópontbeli szó, a keresés a bal oldali, ellenkezQ esetben a jobb oldali gyermek csomópontban folytatódik. Ha a kiválasztott irányban nincs leszármazott, a szó nincs a fán, és éppen a hiányzó leszármazottnak megfelelQ csomópontba kell beírnunk. Ez a keresési eljárás rekurzív, hiszen bármelyik csomóponttól induló keresés tartalmazza a valamelyik leszármazottjától induló keresést is. Ennek megfelelQen a legtermészetesebb az, ha a beillesztésre és kiírásra is rekurzív rutinokat használunk.
Visszatérve a csomópont leírására, a csomópont nyilván struktúra lesz, amely négy összetevQbQl áll:

struct tnode { /*Alapcsomópont*/
char *word; /*A szövegre mutat*/
int count; /*ElQfordulások száma*/
struct tnode *left; /*Bal oldali gyermek*/
struct tnode *right; /*Jobb oldali gyermek*/
}

A csomópontnak ez a rekurzív deklarációja talán bizonytalanul fest, de valójában teljesen helyes és pontos. A struktúra nem tartalmazhatja saját megnevezését, de

struct tnode *left;

a left-et a csomópontot megcímzQ mutatónak, nem pedig csomópontnak deklarálja.
Az egész program meglepQen rövid, mivel már korábban megírt segédrutinokat használ. Ezek: a getword, amellyel az egyes bemeneti szavakat olvassuk be, és az alloc, amellyel helyet biztosítunk a szavak arrébbcsúsztatásához.
A fQrutin egyszerqen a getword segítségével beolvassa a szavakat és a tree használatával elhelyezi azokat a fán.

#define MAXWORD 20
main () /*Szavak gyakoriságának számlálása*/
{
struct tnode *root, *tree ();
char word [MAXWORD];
int t;
root = NULL;
while ((t = getword (word, MAXWORD)) != EOF)
if (t == LETTER)
root = tree (root, word);
treeprint (root);
}

Maga a tree egyszerq. A main a fa tetején (a gyökér szintjén egy szót ad át. Ezt a szót minden lépésben összehasonlítjuk az adott csomópontnál már tárolt szóval és a tree rekurzív hívásai révén vagy a bal oldali, vagy a jobb oldali részfa irányában haladunk tovább. Végül is a szó vagy megegyezik valamelyik, már a fán tárolt szóval (amikor is elQfordulásainak számát növeljük), vagy pedig a program a nullamutatót találja meg, ami azt jelzi, hogy új csomópontot kell létrehozni és a fát azzal ki kell bQvíteni. oj csomópont létrehozásakor a tree az azt megcímzQ mutatóval tér vissza, ami bekerül a szülQ csomópontba:

struct tnode *tree (p, w) /*w elhelyezése p-nél
vagy p alatt*/
struct tnode *p;
char *w;
{ 'f
struct tnode *talloc ();
char *strsave ();
int cond;
if (p == NULL) { /*oj szó érkezett*/
p = talloc (); /*oj csomópont készül*/
p->word = strsave (w);
p->count = 1;
p->left = p->right = NULL;
} else if ((cond = strcmp (w, p->word)) == 0)
p->count++; /*Már volt ilyen szó*/
else if (cond < 0) /*Kisebb - a bal
részfába kerül*/
p->left = tree (p->left, w);
else /*Nagyobb - a jobb részfába kerül*/
p->right = tree (p->right, w);
return (p);
}

Az új csomópont számára szükséges tárhelyet a talloc szolgáltatja, amely a már korábban megírt alloc módosított változata. A talloc rutin a fa csomópontjának tárolására alkalmas szabad területet megcímzQ mutatót ad vissza. (errQl röviden részletesebben is szólunk.) Az új szót az strsave másolja be egy rejtett hejre, a darabszám inicializálódik, és a két leszármazott nulla lesz. A programkódnak ezt a részét csupán a fa szélein hajtjuk végre, amikor új csomópontot iktatunk be. A strsave és a talloc által visszaadott értékek hibaellenQrzését elhagytuk (ami élesben használt program esetében nem bölcs dolog).
A treeprint a fát a bal oldali részfa sorrendjében nyomtatja ki; minden egyes csomópontnál kinyomtatja a bal oldali részfát (minden olyan szót, amely a kérdéses szónál kisebb), majd magát a szót és végül a jobb oldali részfát (minden olyan szót, amely nagyobb). Ha az olvasó bizonytalannak érzi magát a rekurziós technikával kapcsolatban, rajzoljon le egy fát és nyomtassa ki a treeprint-tel: kevés ennél áttekinthetQbb rekurzív rutint találhatunk.

treeprint (p) /*A p fa rekurzív kinyomtatása*/
struct tnode *p;
{
if (p != NULL) {
treeprint (p->left);
printf ("%4d %s \n", p->count, p->word);
treeprint (p->right);
}
}

Gyakorlati megjegyzés: ha a fa "kiegyensúlyozatlanná" válik, mert a szavak nem véletlenszerq sorrendben érkeznek, a program futási ideje túl gyorsan növekedhet. A legrosszabb eset az, amikor a szavak már sorrendben vannak, ilyenkor ez a program igen költséges módon szimulálja a lineáris keresést. A bináris fának vannak általánosításai, mégpedig a 2-3 fák és az AVL fák, amelyek mentesek ettQl a legrosszabb esetben bekövetkezQ viselkedéstQl, könyvünkben azonban ezeket nem ismertethetjük.
MielQtt befejeznénk a példát, érdemes rövid kitérQt tennünk a tárterület-lefoglalással kapcsolatos egyik problémára. Nyilván jó lenne, ha a programban csak egy tárfoglaló függvény lenne, még akkor is, ha az különféle jellegq objektumok számára foglal helyet. Ha azonban ugyanaz a függvény foglal helyet pl. char-okat és struct tnode-okat megcímzQ mutatók számára, két kérdés merül fel. ElQször is, hogyan elégíti ez ki a legtöbb valóságos gépnek azt a követelményét, hogy bizonyos típusú objektumok adott elhelyezési elQírásoknak kell, hogy eleget tegyenek? (pl. az egész típusú mennyiségeket gyakran páros címen kell elhelyezni.) Másodszor, milyen deklarációk tudnak megbírkózni azzal a ténnyel, hogy az alloc szükségszerqen különbözQ típusú mutatókat ad vissza?
Az elhelyezési elQírásoknak általában - némi hely elvesztése árán - egyszerqen úgy tehetünk eleget, ha gondoskodunk arról, hogy a helyfoglaló mindig olyan mutatót adjon vissza, amely az összes elhelyezési követelménynek eleget tesz. A PDP-11-en pl. elegendQ, ha az alloc mindig páros mutatót ad vissza, mivel páros címen bármilyen típusú objektum tárolható. Ennek ára csupán egyetlen elvesztett karakterpozíció páratlan_ hosszúságú mennyiség esetén. Hasonló intézkedés tehetQ más gépeken is. îgy lehet, hogy az alloc megvalósítása nem gépfüggetlen, a használata azonban az. Az 5. fejezetben ismertetett alloc semmiféle megkülönböztetett elhelyezkedést sem garantál, a 8. fejezetben bemutatjuk, hogyan kell helyesen megoldani ezt a feladatot.
Az alloc típusdeklarációjának kérdése minden olyan nyelvben gondot okoz, amely komolyan veszi a típusellenQrzést. A C-ben a legjobb eljárás az, ha az alloc-ot úgy deklaráljuk, hogy char-t megcímzQ mutatót adjon vissza, majd típusmódosító szerkezettel explicit kényszerrel változtatjuk a mutatót a kívánt típusúvá. Ha tehát p deklarációja

char *p;

akkor

(struct tnode *)p

egy kifejezésben p-t tnode mutatóvá alakítja át. îgy a talloc leírása:

struct tnode *talloc ()
{
char *alloc ();
return ((struct tnode *) alloc (sizeof (struct tnode)));
}

Ez már több, mint amire a jelenlegi fordítók esetében szükség van, azonban jelzi a jövQre nézve a legbiztosabb irányt.

6.4. Gyakorlat. îrjunk olyan programot, amely beolvas egy C programot, és alfabetikus sorrendben kinyomtatja a változóneveknek azokat a csoportjait, amelyek elsQ 7 karakterükben megegyeznek, azonban ezt követQen valahol különböznek! Ügyeljünk arra, hogy a 7 paraméter legyen!

6.5. Gyakorlat. îrjunk elemi, keresztbe hivatkozó (cross-referencing) programot, amely kinyomtatja egy dokumentumban elQforduló szavak listáját, és minden szóra megadja azoknak a soroknak a sorszámát, ahol az illetQ szó elQfordul!

6.6. Gyakorlat. îrjunk olyan programot, amely a bemenetén elQforduló szavakat az elQfordulás csökkenQ sorrendjébe rendezve nyomtatja ki! Minden szó elé írjuk oda az elQfordulások számát!

6.6. Keresés táblában

Ebben a fejezetben egy táblakeresQ (table-lookup) programcsomag belsejét írjuk meg, amivel a struktúrák további vonatkozásait illusztráljuk. Tipikusan ilyen programkód található a makroprocesszorok vagy fordítók szimbólumtábla-kezelQ rutinjaiban. Tekintsük pl. a C #define utasítását. Ha egy olyan sor, mint

#define YES 1

fordul elQ, a YES név és az 1 helyettesítQ szöveg bekerül egy táblázatba. A késQbbiekben, amikor a YES név utasításokban fordul elQ, pl.

inword = YES;

azt 1 -gyel kell helyettesíteni.
Két fQrutin kezeli a neveket és a helyettesítQ szövegeket. Az install(s, t) beírja az s nevet és a t helyettesítQ szöveget egy táblázatba; az s és a t egyszerqen karakterláncok. A lookup(s) megkeresi s-et a táblázatban, és egy mutatót ad vissza, amely arra a helyre mutat, ahol s-et megtalálta, vagy pedig NULL, ha s nincs a táblázatban.
Az ún. hash keresési algoritmust használjuk - a program a beérkezQ nevet kis pozitív egész számmá alakítja át, amelyet késQbb azután egy mutatótömb indexelésére használ. A tömb egy eleme olyan blokkok láncának kezdetére mutat, amelyek az illetQ hash értékq neveket írják le. A tömbelem NULL, ha egyetlen név sem rendelkezik az adott hash értékkel.
A láncban egy blokk olyan struktúra, amely a nevet megcímzQ mutatókat, a helyettesítQ szöveget és a következQ láncbeli blokkot megcímzQ mutatót tartalmazza. A lánc végét a következQ blokk mutatójának nulla értéke jelzi :

struct nlist { /*Elemi tábla bejegyzés*/
char *name;
char *def;
struct nlist *next; /*KövetkezQ bejegyzés a
láncban*/
}

A mutatótömb:

#define HASHSIZE 100
static struct nlist *hashtab [HASHSIZE];
/*Mutatótábla*/

A lookup és az install által egyaránt használt hash értékképzQ függvény egyszerqen összeadja a láncbeli karakterértékeket és képezi az összegnek a tömbmérettel vett maradékát. (Ez nem a lehetQ legjobb algoritmus, de megvan az az elQnye, hogy rendkívül egyszerq.)

hash (s) /*Az s string hash értékének képzése*/
char *s;
{
int hashval;
for (hashval = 0; *s != '\0'; ;)
hashval += *s++;
return (hashval % HASHSIZE);
}

A hash-eljárás a hashtab _tömbben létrehoz egy kezdQindexet; ha a karakterlánc egyáltalán megtalálható, akkor az itt kezdQdQ blokkláncban kell lennie.
A keresést a lookup végzi. Ha lookup megtalálja a bejegyzést, a megfelelQ mutatót adja vissza; ha nem, akkor NULL-lal tér vissza.

struct nlist *lookup (s) /*s keresése hashtab-ben*/
char *s;
{
struct nlist *np;
for (np = hashtab [hash (s)]; np != NULL; np = np->next)
if (strcmp (s, np->name) == 0)
return (np); /*Megtalálta*/
return (NULL); /*Nem találta meg*/
}

Az install a lookup-ot használja annak eldöntésére, hogy a beállított név már jelen van-e. Ha igen, akkor az új definíció felülbírálja a régit, egyébként pedig teljesen új bejegyzés keletkezik. Az install NULL-t ad vissza, ha valamilyen oknál fogva nincs hely az új bejegyzés számára.

struct nlist *install (name, def) /*(name, def)
elhelyezése
hashtab-ben*/
char *name, *def;
{
struct nlist *np, *lookup ();
char *strsave (), *alloc ();
int hasval;
if ((np = lookup (name)) == NULL) { /*Nem találta meg*/
np = (struct nlist *) alloc (sizeof (np));
if (np == NULL)
return (NULL);
if ((np->name = strsave (name)) == NULL)
return (NULL);
hashval = hash( np->name);
np->next = hashtab [hashval];
hashtab [hashval] = np;
} else /*Már ott van*/
free (np->def); /*Felszabadítja az elQzQ
definíciót*/
if ((np->def (strsave (def))) == NULL)
return (NULL);
return (np);
}

Az strsave egyszerqen átmásolja az argumentumában megadott karakterláncot valamilyen biztos helyre, amit az alloc hívásával kapott. Ezt a programot az 5. fejezetben láttuk. Mivel az alloc és a free hívásai tetszQleges sorrendben elQfordulhatnak, továbbá minthogy az elhelyezkedés is számít, az alloc-nak az 5. fejezetben közölt egyszerq változata itt nem elegendQ (l. a 7. és 8. fejezetet).

6.7. Gyakorlat. îrjunk olyan rutint, amely a lookup és install által kezelt táblából töröl egy nevet és egy definíciót!

6.8. Gyakorlat. Az ebben a fejezetben közölt rutinokat, ill. getch-t és ungetch-t alapul véve valósítsuk meg a #define processzor egyszerq változatát, amely C programok számára használható!

6.7. MezQk

Ha szqkében vagyunk a tárhelynek, elQfordulhat, hogy több objektumot egyetlen gépi szóban kell elhelyeznünk. Tipikus esete ennek az egybites feltételjelzQk (flagek) alkalmazása, pl. a fordítóprogramok szimbólumtábláiban. KívülrQl kényszerített adatformátumok, pl. hardvereszközök illesztésekor, gyakran igénylik azt a lehetQséget, hogy a gépi szó egyes darabjaihoz is hozzáférhessünk.
Képzeljük el a fordítónak azt a részét, amely a szimbólumtáblát kezeli. Minden programbeli azonosítóhoz bizonyos információ társul, pl. hogy kulcsszó-e vagy sem, hogy külsQ és/vagy statikus stb. mennyiségrQl van-e szó. Az ilyen információ kódolásának legtömörebb módja az egybites feltételjelzQk készletének használata egyetlen char-on vagy int-en belül.
Ez általában úgy történik, hogy a választott bitpozícióknak megfelelQen egy maszk-készletet definiálnak, mint

#define KEYWORD 01
#define EXTERNAL 02
#define STATIC 04

(A számoknak kettQ hatványainak kell lenniük.) Ezek után a biteket a 2. fejezetben ismertetett léptetQ, maszkoló és komplementáló operátorokkal már könnyen elérhetjük.
Bizonyos fordulatok különösen gyakoriak:

flags = EXTERNAL | STATIC;

1-re állítja a flags-ben az EXTERNAL és STATIC biteket, míg

flags &= ~(EXTERNAL | STATIC);

ugyanezeket a biteket kinullázza, és

if ((flags &(EXTERNAL | STATIC)) == 0) . . .

igaz, ha mindkét bit nulla.
Bár ez a forma gyorsan elsajátítható, a C nyelv azt is lehetQvé teszi, hogy valamely szón belül ne bitenkénti logikai operátorokkal, hanem közvetlenül definiáljunk és érjünk el egyes mezQket. A mezQ (field) szomszédos bitek halmaza egyetlen int-en belül. A mezQdefiníció és -elérés szintaxisa a struktúrákon alapul. Pl. az elQbbi #define sorok három mezQ definiálásával helyettesíthetQk:

struct {
unsigned is_keyword : 1;
unsigned is_extern : 1;
unsigned is_static : 1;
} flags;

Ez a flags nevq változót definiálja, amely három 1-bites mezQt tartalmaz. A kettQspontot követQ szám jelenti a mezQszélességet bitekben. A mezQket unsigned-nak deklaráltuk annak hangsúlyozására, hogy azok ténylegesen elQjel nélküli mennyiségek.
Az egyes mezQkre flags.is_keyword, flags.is_extern stb. alakkal hivatkozhatunk, éppúgy, mint más struktúratagok esetében. A mezQk úgy viselkednek, mint kis, elQjel nélküli egész számok, és éppúgy részt vehetnek aritmetikai mqveletekben, mint más egészek. îgy a fenti példákat természetesebb módon a következQképpen írhatjuk:

flags.is_extern = flags.is_static = 1;

amely 1-re állítja;

flags.is_extern = flags.is_static = 0;

amely kinullázza, és

if (flags.is_extern == 0 && flags.is_static == 0) . . .

amely megvizsgálja a biteket.
A mezQ nem lépheti át az int határát; ha a megadott szélesség erre vezetne, a mezQ a következQ int határra fog illeszkedni. A mezQknek nem kell feltétlenül névvel rendelkezniük; név nélküli mezQket (csak egy kettQspont és a szélesség) használunk kitöltésre. A speciális 0 szélesség elQírásával a következQ int határra való illeszkedést kényszeríthetjük ki.
A mezQk használatával kapcsolatban néhány dologra ügyelnünk kell! Talán a leglényegesebb, hogy bizonyos gépeken a mezQk hozzárendelése balról jobbra, más gépeken jobbról balra történik, ami az eltérQ hardverfelépítést tükrözi. EbbQl következQleg, bár a mezQk igen hasznosak belsQleg definiált adatstruktúrák kezelésére, mielQtt külsQleg definiált adatok szétbontására használnánk Qket, alaposan meg kell fontolni, milyen is lesz, hol kezdQdik a mezQkiosztás.
További megjegyzendQ megkötések : a mezQk elQjel nélküliek; csak int-ekben tárolhatók (vagy az ezzel egyenértékq unsigned-okban); a mezQk nem tömbök; nincsen címük, így rájuk az & operátor nem alkalmazható.

6.8. Unionok

A union olyan változó, amely (különbözQ idQpontokban) különféle típusú és méretq objektumokat tartalmazhat oly módon, hogy a fordító ügyel a méretre és illeszkedésre vonatkozó követelmények teljesülésére. A unionok lehetQvé teszik, hogy ugyanazon a tárterületen különbözQfajta adatokkal dolgozzunk anélkül, hogy a programban gépfüggQ információt kellene elhelyeznünk.
Példánkat ismét a fordító szimbólumtáblájából véve tegyük fel, hogy állandóink int-ek, float-ok vagy karaktermutatók lehetnek. Valamely adott állandó értékét a megfelelQ típusú változóban kell tárolnunk, ugyanakkor a táblakezelés szempontjából a legkényelmesebb, ha az érték ugyanannyi tárterületet foglal el és ugyanazon a helyen tárolódik, a típusától függetlenül. Ez a union használatának célja - olyan változót létrehozni, amely megengedett módon több típus bármelyikét tartalmazhatja. A mezQkhöz hasonlóan a szintaxis a struktúrákon alapul.

union u_tag {
int ival;
float fval;
char *pval;
} uval;

Az uval változó elég nagy lesz ahhoz, hogy a három típus közül a legnagyobbat is tartalmazhassa, függetlenül attól a géptQl, amelyen a fordítás történik - a programkód független a hardver jellemzQitQl. E típusok bármelyike hozzárendelhetQ uval-hoz, majd kifejezésekben használható mindaddig, amíg a használat következetes: a visszanyert típus a legutoljára tárolt típussal kell, hogy megegyezzen. A programozó feladata annak követése, hogy éppen mit tárolt a unionban; ha valamit adott típusként tárolunk és más típusúként olvassuk ki, akkor az eredmények gépfüggQek.
A union tagjaihoz való hozzáférés szintaxisa:

unionnév.tag

vagy

unionmutató->tag

csakúgy, mint a struktúrák esetében. Ha az utype változó segítségével követjük az uval-ban tárolt aktuális típust, akkor az alábbihoz hasonló kódot kapunk:

if (utype == INT)
printf ("%d \n", uval.ival);
else if (utype == FLOAT)
printf ("%f \n", uval.fval);
else if (utype == STRING)
printf ("%s \n", uval.pval);
else
printf ("rossz típus %d az utype-ban\n", utype);

Unionok elQfordulhatnak struktúrákban és tömbökben, ill. viszont. A struktúrabeli union (vagy megfordítva) egyik tagjához való hozzáférést leíró jelölésmód azonos az egymásba skatulyázott struktúrák jelölésmódjával. Pl. a

struct {
char *name;
int flags;
int utype;
union {
int ival;
float fval;
char *pval ;
} uval;
} symtab [NSYM];

által definiált struktúratömbben az ival változóra a

symtab [i].uval.ival

alakban, míg a pval karakterlánc elsQ karakterére a

*symtab [i].uval.pval

alakban hivatkozhatunk.
Valójában a union olyan struktúra, amelyben a tagok közötti eltolás nulla, és amely elég nagy ahhoz, hogy a legszélesebb tagot is tartalmazhassa úgy, hogy az illeszkedés a benne elQforduló összes típus számára megfeleljen.
A struktúrákhoz hasonlóan a unionokra jelenleg csak két mqvelet megengedett: valamelyik tagjához való hozzáférés, ill. a cím elQállítása. A unionokhoz semmit sem lehet hozzárendelni, nem lehet Qket függvényeknek átadni, és függvények sem adhatnak vissza unionokat. A unionokat megcímzQ mutatók ugyanúgy használhatók, mint a struktúrák mutatói.
A 8. fejezetben bemutatásra kerülQ tárterület-lefoglaló szemlélteti, hogyan lehet union használatával kikényszeríteni, hogy egy változó adott típusú tárterület határára illeszkedjen.

6.9. Típusnévdefiníciók

A C nyelv typedef-nek nevezett szolgáltatásának segítségével új adattípus-neveket hozhatunk létre. Pl. a

typedef int LENGTH;

deklaráció hatására a LENGTH név az int szinonimája lesz. A LENGTH típus deklarációban, típusmódosító szerkezetben stb. pontosan ugyanúgy használható, mint az int típus:

LENGTH len, maxlen;
LENGTH *lengths [];

Hasonlóképpen, a

typedef char * STRING;

deklaráció hatására a STRING a char * , vagyis a karaktermutató szinonimája lesz, amit azután olyan deklarációkban használhatunk, mint

STRING p, lineptr [LINES], alloc ();

Figyeljük meg, hogy a typedef-ben deklarált típus a változónév helyén jelenik meg, nem pedig közvetlenül a typedef szó után. A typedef szintaktikusan olyan, mint az extern, static stb. tárolási osztályok. A nevek hangsúlyozása érdekében nagybetqket használtunk.
Bonyolultabb példaként typedef-eket készítünk az ebben a fejezetben korábban bemutatott facsomópontok számára:

typedef struct tnode { /*Alapcsomópont*/
char *word; /*A szövegre mutat*/
int count; /*ElQfordulások száma*/
struct tnode *left; /*Bal oldali
gyermek*/
struct tnode *right; /*Jobb oldali
gyermek*/
} TREENODE, *TREEPTR;

Ezzel két új típuskulcsszó, TREENODE (struktúra) és TREEPTR (a struktúramutatója)jön létre. Ekkor a talloc rutinból

TREEPTR talloc ()
{
char *alloc ();
return ((TREEPTR) alloc (sizeof (TREENODE)));
}

lesz.
Hangsúlyozzuk, hogy a typedef deklaráció semmilyen értelemben sem hoz létre új típust; egyszerqen új nevet ad valamilyen, már létezQ típusnak. Szemantikailag sincs benne semmi új : az ily módon deklarált változók pontosan ugyanolyan tulajdonságúak, mint azok a változók, amelyeknek deklarációit explicit módon leírtuk. Valójában typedef olyan, mint #define, attól eltekintve, hogy mivel a fordító értelmezi, olyan szöveges helyettesítésekkel ismeg tud bírkózni, amelyek meghaladják a C makroprocesszor képességeit. Pl.:

typedef int (*PFI) ();

létrehozza a PFI típust az int-et visszaadó függvényt megcímzQ mutató számára, amely olyan összefüggésekben használható, mint

PFI strcmp, numcmp, swap;

az 5. fejezet rendezQprogramjában.
A typedef deklarációk használatának két fQ oka van. Az elsQ ok a programok paraméterezése a gépfüggQségi problémák kivédésére. Ha a typede-feket olyan adattípusokra használjuk, amelyek gépfüggQek, akkor a program áthelyezésekor csupán a typedef-eket kell megváltoztatni. Az egyik szokásos eset az, amikor különféle egész mennyiségek számára használunk typedef neveket, majd minden egyes befogadó gépre elkészítjük a short, int és long választékából álló megfelelQ készletet.
A typedef-ek használatának másik célja a programdokumentálás javítása - a TREEPTR-nek nevezett típust könnyebb megérteni, mint azt, amelyiket csupán egy bonyolult struktúra mutatójaként deklaráltunk.
Végezetül, mindig megvan annak a lehetQsége, hogy a jövQben a fordító vagy valamelyik másik program, mint pl. a lint fel tudja használni a typedef deklarációkban tárolt információt a program valamilyen külön ellenQrzése céljából.

7. fejezet : Bevitel és kivitel

A be- és kiviteli (I/O) szolgáltatások nem részei a C nyelvnek, ezért ezekre idáig nem fordítottunk különösebb figyelmet. Kétségtelen azonban, hogy a valódi programok sokkal bonyolultabb módon állnak kapcsolatban környezetükkel mint ahogy azt idáig bemutattuk. Ebben a fejezetben a szabványos (standard) be- és kiviteli könyvtárat ismertetjük; ez a függvényeknek olyan készlete, amelyek a C programok szabványos be- és kiviteli rendszerét képezik. E függvények feladata, hogy kényelmes programozási csatlakozást biztosítsanak, ugyanakkor csupán olyan mqveleteket valósítsanak meg, amelyek a legtöbb modern operációs rendszerben rendelkezésre állnak. A rutinok - függetlenül attól, hogy milyen kritikus alkalmazásról van szó - elég jól mqködnek, a felhasználók ritkán érezhetik úgy, hogy a nagyobb hatékonyság érdekében más megoldást kell alkalmazniuk. Végül a rutinok gépfüggetlenek abban az értelemben, hogy kompatibilis formában mqködnek minden olyan rendszeren, amelyen a C létezik, és azok a programok, amelyek a rendszerrel folytatott párbeszédüket a szabványos könyvtár által nyújtott szolgáltatásokra korlátozzák, lényegében változtatás nélkül vihetQk át egyik géprQl a másikra.
Ezen a helyen nem kíséreljük meg a teljes be- és kiviteli könyvtár leírását; sokkal fontosabbnak tartjuk, hogy bemutassuk, hogyan kell az operációs rendszerbeli környezetükkel kapcsolatot tartó C programokat írni.

7.1. Hozzáférés a szabványos könyvtárhoz

Minden olyan forrásállománynak, amely valamelyik szabványos könyvtárbeli függvényre hivatkozik, valahol az állomány elején tartalmaznia kell az

#include < stdio.h >

sort.Az stdio.h állomány bizonyos, a be- és kiviteli könyvtár által használt makrókat és változókat definiál. A < és > könyökzárójeleknek a szokásos idézQjelek helyetti használata hatására a fordító az állományt abban a katalógusban (directory-ban) fogja keresni, amely a szabványos fej (header) információt tartalmazza (a UNIX-ban tipikusan /usr/include).
A program betöltésekor szükséges lehet továbbá a könyvtár explicit megadása, a PDP-11 UNIX rendszeren pl. a program fordítását elQíró parancs

cc forrásállományok stb. ls

ahol ls jelzi a szabványos könyvtárból történQ betöltést. (Az l karakter az "el" betq, load = betölteni.)

7.2. Szabványos be- és kivitel; getchar és putchar

A legegyszerqbb beviteli mechanizmus az, amikor getchar-ral karakterenként olvasunk a szabványos bemenetrQl (standard inputról), általában a felhasználói terminálról. getchar() minden hívása után a következQ bemeneti karaktert adja vissza. A legtöbb olyan környezetben, amely a C-t támogatja, a terminált egy állománnyal helyettesíthetjük a C konvenció segítségével: ha a prog program a getchar-t használja, akkor a

prog < infile

parancssor hatására a prog az infile-t fogja olvasni a terminál helyett. A bemenet átkapcsolása oly módon történik, hogy maga a prog érzéketlen a változtatásra; közelebbrQl, az
otherprog > prog

parancssor két programot futtat, mégpedig az otherprog-ot és a prog-ot, és úgy intézkedik, hogy a prog szabványos bemenete az otherprog szabványos kimenetérQl jöjjön.
A getchar az EOF értéket adja vissza, amikor az általa éppen olvasott, bármiféle bemenet végére ért. A szabványos könyvtár az EOF szimbolikus állandót -1-nek definiálja (egy #define-nal az stdio.h állományban), a vizsgálatokat ennek ellenére EOF-ra és ne -1-re végezzük, hogy ezáltal az adott értéktQl függetlenek maradjunk.
Ami a kimenetet illeti, a putchar (c) a c karaktert szabványos kimenetre (standard outputra) teszi, ami alapértelmezés szerint szintén a terminál. A kimenet > használatával irányítható állományba; ha prog a putchar-t használja, akkor

prog > outfile

a szabványos kimenetet a terminál helyett az outfile-ra írja. A UNIX rendszerben parancsláncot (pipe) is használhatunk:

prog < anotherprog

a prog szabványos kimenetét az anotherprog szabványos bemenetére teszi. A prog ebben az esetben sem vesz tudomást az átirányításról.
A printf által létrehozott kimenQ szöveg szintén a szabványos kimenetre kerül. A putchar és a printf hívásai keverhetQk.
MeglepQen nagy azoknak a programoknak a száma, amelyek csupán egyetlen bemeneti folyamot olvasnak és csupán egyetlen kimeneti folyamot írnak. Ilyen programok esetében a be- és kivitel getchar, putchar, ill. printf függvényekkel történQ megvalósítása teljesen megfelelQ, és az induláshoz feltétlenül elég. Ez különösen igaz akkor, ha az egyik program kimenetének a következQ program bemenetével történQ összekapcsolása céljából rendelkezésre áll az állomány-átirányítás és a parancslánc-mechanizmus. Tekintsük pl. a lower programot, amely a bemenetet kisbetqssé képezi le:

#include < stdio.h>
main () /*A bemenet kisbetqssé alakítása*/
{
int c;
while ((c = getchar ()) != EOF)
putchar(isupper(c)) ? tolower(c) : c);
}

Az isupper és tolower függvények valójában az stdio.h-ban definiált makrók. Az isupper makró ellenQrzi, hogy az argumentum nagybetq-e és nemnullát ad vissza, ha az, ill. nullát, ha nem. A tolower makró a nagybetqket kisbetqkké alakítja. Függetlenül attól, hogy az adott gépen ezek a függvények hogyan vannak megvalósítva, kívülrQl nézve egyformán viselkednek, így az azokat használó programoknak nem kell ismerniük a karakterkészletet.
Több állomány konvertálásakor az állományok összegyqjtésére olyan programot használhatunk, mint a UNIX cat segédprogramja:

cat file1, file2 . . . > lower > output

így nem kell megtanulnunk, hogyan lehet állományokat programból elérni. (A cat-ot e fejezet késQbbi részében mutatjuk be.)
Mellékesen megjegyezzük, hogy a szabványos be- és kiviteli könyvtárban a getchar és putchar függvények valójában makrók lehetnek, így elkerülhetQ a karakterenkénti függvényhívás miatti terhelés (overhead). A 8. fejezetben fogjuk ennek tényleges megvalósítását megmutatni.

7.3. Formátumozott kimenet; printf

A kivitel céljából használt printf és a beolvasást végzQ scanf rutin (l. a következQ szakaszt) numerikus mennyiségek karakteres ábrázolásra és karakteres mennyiségek numerikus ábrázolásra történQ átalakítását, formátumozott sorok létrehozását és értelmezését teszi lehetQvé. A printf függvényt az elQzQ fejezetekben kötetlenül használtuk, íme a teljesebb és pontosabb leírás:

printf(control, arg1 , arg2, . . .)

A printf az argumentumait konvertálja, formátumozza és a szabványos kimenetre nyomtatja a control karakterlánc vezérlete alatt. A vezérlQ karakterlánc kétféle típusú objektumot tartalmaz: közönséges karaktereket, amelyeket egyszerqen a kimeneti folyamra másol és konverzió-specifikációkat, amelyek mindegyike a printf soron következQ argumentumának konvertálását és kinyomtatását írja elQ.
Minden konverzió-specifikációt a % karakter vezet be, és konverziós karakter zár le. A % és a konverziós karakter között a következQk állhatnak:
- Mínuszjel, amely az ebbe a mezQbe konvertált argumentum balra igazítását írja elQ.
- SzámjegyekbQl álló karakterlánc, amely a minimális mezQszélességet határozza meg. Az átalakított szám legalább ilyen széles vagy szükség esetén szélesebb mezQbe nyomtatódik ki. Ha a konvertált argumentum kevesebb karakterbQl áll, mint a mezQszélesség, akkor bal oldalon (vagy, ha a balra igazítás jelzQ szerepel, akkor jobb oldalon) a mezQ kitöltQdik, hogy ezáltal az elQírt mezQszélesség meglegyen. A kitöltQ karakter közönséges esetben szóköz, ill. amennyiben a mezQszélességet elQnullával adtuk meg, akkor nulla (ez a zérus nem jelent oktálisan értelmezett mezQszélességet).
- Pont, amely a mezQszélességet a rákövetkezQ számjegysorozattól választja el.
- Számjegysorozat (a pontosság), amely a láncból kinyomtatásra kerülQ karakterek maximális számát vagy float és double esetében a tizedes- ponttól jobbra kinyomtatandó számjegyek számát határozza meg.
- Az l (el betq) hosszmódosító, amely arra utal, hogy a szóban forgó adat int helyett long.

A konverziós karakterek és jelentésük:

d Az argumentum decimális jelölésmódúvá alakul.
o Az argumentum elQjel nélküli, oktális számmá konvertálódik (elQnulla nélkül.
x Az argumentum elQjel nélküli, hexadecimális számmá konvertálódik (vezetQ 0x nélkül).
u Az argumentum elQjel nélküli decimális jelölésmódúvá alakul.
c Az argumentumot egyetlen karakternek tekinti.
s Az argumentum karakterlánc; a láncbeli karakterek mindaddig nyomtatódnak, amíg a nullakarakter nem kerül sorra, vagy amíg a pontossági specifikáció által elQírt számú karakter kiírása meg nem történt.
e Az argumentumot float-nak vagy double-nak tekinti, és a [-]m.nnnnnnE[+]xx decimális jelölésmódba konvertálja, ahol az n-ek karakterláncának hosszát a pontosság adja meg. Az alapértelmezés szerinti pontosság 6.
f Az argumentumot float-nak vagy double-nak tekinti, és a [-]mmm.nnnnn decimális jelölésmódba konvertálja, ahol az n-ek karakterláncának hosszát a pontosság adja meg. Az alapértelmezés szerinti pontosság 6. Jegyezzük meg, hogy a pontosság nem határozza meg az f formátumban kinyomtatott értékes számjegyek számát!
g %e és %f közül a rövidebbet használja; az értéktelen nullákat elhagyja.

Ha a %-ot követQ karakter nem konverziós karakter, az illetQ karakter nyomtatásra kerül: így a % mint %% nyomtatható ki.
A legtöbb formátumkonverzió jelentése nyilvánvaló, és a korábbi fejezetekben ezeket megtárgyaltuk. Ez alól az egyik kivétel az, hogy a pontosság miként vonatkozik a karakterláncokra. Az alábbi táblázat különféle specifikációknak a "halló, világ" (12 karakter) kinyomtatására gyakorolt hatását mutatja. Minden mezQ köré kettQspontokat helyeztünk, hogy ezzel szemléltessük a mezQ kiterjedését:


Figyelmeztetés: a printf az elsQ argumentumát használja annak eldöntésére, hogy még hány argumentum következik, és azoknak mi a típusa. A program összezavarodik, és értelmetlen választ kapunk, ha nincs elég argumentum, vagy ha azok nem a megfelelQ típusúak.

7.1. Gyakorlat. îrjunk olyan programot, amely tetszQleges bemenetet képes ésszerq módon kinyomtatni! Minimális feladatként a nemgrafikus karaktereket (a helyi szokásnak megfelelQen) oktálisban vagy hexadecimálisban nyomtassa ki, és hajtogassa össze a hosszú sorokat!

7.4. Formátumozott bemenet; scanf

A scanf függvény a printf bemeneti megfelelQje, amely az ellenkezQ irányban nyújt számos, a fentiekben leírt szolgáltatást:

scanf(control, arg1, arg2, . . .)

A scanf karaktereket olvas a szabványos bemenetrQl, a control-ban meghatározott formátum szerint értelmezi azokat, és az eredményeket a többi argumentumban tárolja. A vezérlQargumentumot az alábbiakban írjuk le; a többi argumentum, amelyek mindegyike mutató kell, hogy legyen, azt jelzi, hogy hol kell tárolni az átalakított bemenetet.
A vezérlQ karakterlánc általában olyan konverziós utasításokat tartalmaz, amelyek feladata a bemeneti jelsorozat közvetlen értelmezése. A vezérlQ karakterlánc tartalmazhat:
- Szóközöket, tabokat és újsorokat (üres karaktereket), amelyeket figyelmen kívül hagy.
- Közönséges karaktereket (nem %-ot), amelyek várhatóan illeszkednek a bemeneti folyam következQ nemüres karakterére.
- Konverzióspecifikációkat, amelyek a % karakterbQl, a * hozzárendelés-elnyomó karakterbQl, egy, a maximális mezQszélességet meghatározó számból, valamint egy konverziós karakterbQl állnak, ezek közül a két középsQ (* és a szám) elhagyható.
A konverzióspecifikáció a következQ bemeneti mezQ átalakítását irányítja. Közönségesen az eredmény a megfelelQ argumentum által megcímzett változóba kerül. Ha azonban a * karakter a hozzárendelés elnyomását írja elQ, a vezérlés a bemeneti mezQt egyszerqen átugorja, és nem történik értékadás. A beolvasott mezQ definíciószerqen a nemüres karakterek lánca, tehát vagy a következQ üres karakterig tart, vagy addig, amíg el nem fogy az esetleg megadott mezQszélesség. EbbQl következQleg a scanf sorhatárokon keresztül is olvassa a bemenetét, mivel az újsor karakterek üres helyek.
A konverziós karakter a beolvasott mezQ értelmezésére utal; a hozzá tartozó argumentumnak mutatónak kell lennie, amint azt a C nyelv érték szerint hívó szemantikája megkívánja. A következQ konverziós karakterek megengedettek:
d A bemeneten decimális egész számot vár; a megfelelQ argumentumnak egészre kell mutatnia.
o A bemeneten oktális egész számot vár (elQnullával vagy anélkül); a megfelelQ argumentumnak egész mutatónak kell lennie.
x A bemeneten hexadecimális egész számot vár (vezetQ 0x-szel vagy anélkül); a megfelelQ argumentumnak egész mutatónak kell lennie.
h A bemeneten short egész számot vár; a megfelelQ argumentum short egészt megcímzQ mutató kell, hogy legyen.
c Egyetlen karaktert vár; a megfelelQ argumentum karaktermutató kell, hogy legyen; a következQ bemeneti karakter a megjelölt helyre kerül. Az üres karakterek szokásos átugrása ebben az esetben letiltódik; a következQ nemüres karakter beolvasásához használjunk %ls-t.
s Karakterláncot vár; a megfelelQ argumentum karaktermutató; olyan karaktertömbre mutat, amely elég nagy ahhoz, hogy befogadja a karakterláncot és a lezáró \0-t.
f LebegQpontos számot vár; a megfelelQ argumentum float-ot megcímzQ mutató kell, hogy legyen. Az e konverziós karakter az f szinonimája. A float-ok bemeneti formátuma: elQjel (elhagyható), számokból álló lánc, amely tizedespontot és egy (elhagyható) kitevQmezQt tartalmazhat, amely utóbbi egy E-bQl vagy e-bQl és az azt követQ, esetleg elQjeles egész számból áll.
A d, o és x konverziós karaktereket az l (el betq) elQzheti meg, amely arra utal, hogy az argumentumlistában long-ot és nem int-et megcímzQ mutató jelenik meg. Az e vagy f konverziós karaktereket ugyancsak megelQzheti az l, ebben az esetben aztjelezve, hogy az argumentumlista double-ra és nem float-ra hivatkozó mutatót tartalmaz. Például az

int i;
float x;
char name[50];
scanf("%d %f %s", &i, &x, name);

hívás a

25 54.32E-1 Thompson

bemenet esetén az i-hez a 25 értéket rendeli hozzá, az x-hez az 5.432 értéket és a name-hez a \0-val rendesen lezárt "Thompson" karakterláncot. A három bemeneti karakterláncot tetszQleges számú szóközzel, tabbal és újsorral lehet egymástól elválasztani. Az

int i;
float x;
char name[50];
scanf("%2d %f %*d %2s", &i, &x, name);

hívás az

56 789 0123 45a78

bemenettel 56-ot rendel i-hez, 789.0-t az x-hez, átugorja a 0123-at és a 45 karakterláncot a name-be teszi. Bármelyik bemeneti rutin következQ hívása az a betqnél történQ kereséssel fog indulni. E két példában a name mutató, és nem elQzheti meg az & szimbólum.
Másik példaként a 4. fejezetben ismertetett elemi kalkulátorprogramot most úgy írjuk át, hogy a scanfvégezze a bemeneti konverziót:

#include < stdio.h>
main() /* Elemi kalkulátorprogram*/
{
double sum, v;
sum = 0;
while (scanf("%lf", &v) != EOF)
printf("\t%.2f\n", sum += v);
}

A scanf akkor fejezi be mqködését, amikor kimerítette a vezérlQ karakterláncát, vagy amikor valamelyik bemenet nem illeszkedik a vezérlési specifikációra. A scanf visszatérési értéke a sikeresen illesztett és hozzárendelt bemeneti tételek száma. EbbQl meghatározható, hogy a scanf hány bemeneti tételt talált. 9llomány vége esetén a visszaadott érték EOF; ügyeljünk arra, hogy ez nullától eltérQ érték, amely azt jelenti, hogy a következQ bemeneti karakter nem illeszkedik a vezérlQ karakterlánc elsQ specifikációjára! A scanf következQ hívásakor a keresés közvetlenül az utoljára visszaadott karakter után folytatódik.
Még egy utolsó figyelmeztetés: a scanf argumentumainak mutatóknak kell lenniük! A leggyakoribb hiba, amikor valaki azt írja, hogy

scanf("%d", n);

ahelyett, hogy

scanf("%d", &n);

-et írna.

7.5. Formátumkonverzió a táron belül

A scanf és printf függvényekkel rokon az sscanf és sprintf, amelyek ugyanezeket a konverziókat végzik, de állomány helyett karakterláncon dolgoznak. Az általános formátum:

sprintf(string, control, arg1, arg2, ...)
sscanf(string, control, arg1, arg2, ...)

Az elQzQekhez hasonlóan az sprintf a control szerint formátumozza az arg1, arg2 stb.-beli argumentumokat, az eredményt azonban a szabványos kimenet helyett a string-be teszi. A string-nek természetesen elég nagynak kell lennie ahhoz, hogy befogadja az eredményt. Ha pl. string karaktertömb és n egész, akkor

sprintf(name, "temp %d", n);

a name-ben létrehoz egy temp<_'NN alakú karakterláncot, ahol NNN az n értéke.
Az sscanf az ellentétes irányú konverziókat hozza létre - a control-ban megadott formátum szerint végighalad a karakterláncon, és a kapott eredményeket az arg1, arg2 stb.-ben helyezi el. Ezen argumentumoknak mutatóknak kell lenniük. A

sscanf(name, "temp%d", &n);

hívás az n-et annak a számjegyekbQl álló karakterláncnak az értékére állítja be, amely a name-ban a temp-et követi.

7.2. Gyakorlat. îrjuk meg újra a 4. fejezetben látott kalkulátorprogramot úgy, hogy a bemenetre és a számkonverzióra a scanf és/vagy sscanf függvényeket alkalmazzuk!

7.6. 9llomány-hozzáférés
Az idáig megírt programok mindegyike a szabványos bemenetet olvasta és a szabványos kimenetre írt, amelyekrQl mindeddig feltételeztük, hogy valamilyen varázslatos módon a helyi operációs rendszer elQre definiálta Qket a számunkra.
A be- és kivitellel való ismerkedésünk következQ lépéseként olyan programot írunk, amellyel programhoz nem rendelt állományhoz férhetünk hozzá. Az ilyen mqveletek szükségességét világosan bizonyító program a cat, amely megnevezett állományok halmazát gyqjti ki (konkatenálja) a szabványos kimenetre. A cat feladata állományoknak a terminálra történQ kinyomtatása, valamint általános célú bemeneti információgyqjtés azon programok számára, amelyek maguk nem képesek állományokhoz név szerint hozzáférni. Pl. a

cat x.c y.c

parancs az x.c és y.c állományok tartalmát a szabványos kimenetre nyomtatja.
Kérdés, hogyan érjük el, hogy a megnevezett állományok beolvasásra kerüljenek - azaz,
__v_ya__ átve__e__ük a _c___aac__a_v a__a_ n_gv__uv__ nu_av
neveket azokhoz az utasításokhoz, amelyek ténylegesen elolvassák az adatokat.
A szabályok egyszerqek. MielQtt egy állományt olvasni vagy írni lehetne, az fopen szabványos könyvtári függvénnyel meg kell nyitni. Az fopen vesz egy külsQ nevet (mint x.c vagy y.c), bizonyos nyilvántartást végez, és párbeszédet folytat az operációs rendszerrel (aminek részleteivel nem kell törQdnünk), és olyan belsQ nevet ad vissza, amelyet az állomány ezután következQ olvasásai, ill. írásai során használnunk kell.
E belsQ név valójában mutató, amelyet állománymutatónak nevezünk, és amely egy, az állományról különbözQ információkat tartalmazó struktúrára mutat. Itt található pl. a puffer címe, a pillanatnyi pufferbeli karakterpozíció, annak jelzése, hogy az állomány éppen olvasás vagy írás alatt áll stb. A felhasználóknak a részleteket nem kell ismerniük, mivel az stdio.h-tól nyert szabványos be- és kiviteli definíciók egyik része a FILE-nak nevezett struktúradefiníció. Az állománymutató számára szükséges egyetlen deklarációra nézve példa a

FILE *fopen(), *fp;

Eszerint fp FILE-t megcímzQ mutató, és fopen szintén ilyen mutatóval tér vissza. Figyeljük meg, hogy FILE, csakúgy, mint int, típusnév, nem pedig struktúracímke; typedef-ként valósították meg. (Annak részleteit, hogy mindez miként mqködik a UNIX operációs rendszerben, a 8. fejezetben ismertetjük.)
Az fopen tényleges hívása a programon belül így fest:

fp = fopen(name, mode);

Az fopen elsQ argumentuma az állomány neve, amely egy karakterlánc. A második argumentum, amely szintén karakterlánc, a mód, amely azt jelzi, hogy a felhasználó hogyan akarja használni az állományt. A megengedett módok az olvasás (r: read), az írás (w: write) és a hozzáfüggesztés (a: append).
Ha írásra vagy hozzáfüggesztésre nem létezQ állományt nyitunk meg, akkor az illetQ állomány (ha lehet) létrejön. LétezQ állomány írásra történQ megnyitásának hatására annak korábbi tartalma elvész. Hibát jelent, ha nemlétezQ állományt olvasni akarunk. Más hibaokok is elQfordulhatnak (pl. ha olyan állományt próbálunk meg olvasni, amelyre nincs engedélyünk). Bármilyen hiba esetén az fopen a NULL mutatóértékkel tér vissza (amelynek definíciója a kényelem kedvéért szintén stdio.h-ban van).
A következQkben azt kell tudnunk, hogyan olvashatjuk a már megnyitott állományokat. Több lehetQség van, amelyek közül a getc és putc csak a legegyszerqbb. getc az állomány soronkövetkezQ karakterével tér vissza, állománymutatóval kell megadnunk, hogy melyik állományról van szó. îgy

c = getc(fp)

az fp által hivatkozott állományból a következQ karaktert c-be helyezi, ill. EOF kerül c-be, ha elértük az állomány végét.
A putc a getc inverze:

putc(c, fp)

a c karaktert az fp állományba helyezi és c-t adja vissza. A getchar és putchar függvényekhez hasonlóan a getc és putc is lehet függvény helyett makró.
Három állomány minden program indításakor automatikusan megnyílik, és a rendszer állománymutatókat is rendelkezésre bocsát számukra. Ezek az állományok: a szabványos bemenet, a szabványos kimenet és a szabványos hibakimenet; az ezeknek megfelelQ mutatók neve: stdin, stdout és stderr. Közönséges esetben ezek mindegyike a terminálhoz van rendelve, azonban az stdin és stderr mutatókat a 7.2. szakaszban leírt módon állományokba vagy parancsláncokba lehet átirányítani.
A getchar és a putchar az alábbi módon definiálható a getc, a putc, az stdin és az stdout segítségével:

#define getchar() getc(stdin)
#define putchar(c) putc(c, stdout)

9llományok formátumozott beolvasására vagy kiíratására az fscanf és fprintf függvényeket használhatjuk. Ezek azonosak a scanf és printf függvényekkel, eltekintve attól, hogy az elsQ argumentum állománymutató, amely az olvasandó vagy írandó állományt határozza meg; a vezérlQ karakterlánc a második argumentum.
E bevezetés után már abban a helyzetben vagyunk, hogy megírhatjuk az állományokat konkatenáló cat programot. Az alapfelépítés azonos azzal, ami már sok programban kényelmesnek bizonyult: ha vannak parancssor-argumentumok, akkor azok feldolgozása sorrendben történik. Ha nincsenek argumentumok, akkor a szabványos bemenetet dolgozzuk fel. îly módon a program akár önállóan, akár valamely nagyobb feldolgozás részeként használható.

#include < stdio.h>
main(argc, argv) /* cat: állományok konkatenálása*/
int argc;
char *argv[];
{
FILE *fp, *fopen();
if (argc == 1) /* Nincs arg., a szabványos bemenetet másolja*/
filecopy(stdin);
else
while (--argc > 0)
if ((fp = fopen(*++argv, "r")) == NULL) {
printf("cat: nem nyitható meg %s\n", *argv);
break;
} else {
filecopy(fp);
fclose(fp);
}
}

filecopy(fp) /* 9llomány másolása a szabványos kimenetre*/
FILE *fp;
{
int c;
while ((c = getc(fp)) != EOF)
putc(c, stdout);
}

Az stdin, ill. stdout állománymutatók a be- és kiviteli könyvtárban szabványos bemenetként, ill. szabványos kimenetként elQredefiniáltak; mindenütt használhatók, ahol FILE típusú objektumokat használni lehet. Ezek azonban állandók és nem változók, tehát ne próbáljunk semmit sem hozzájuk rendelni!
Az fclose függvény az fopen inverze: megszakítja az állománymutató és a külsQ név között az fopen által létrehozott kapcsolatot, és így az állománymutató egy másik állomány számára szabadul fel. Mivel a legtöbb operációs rendszerben az egyidejqleg megnyitott állományok száma korlátozott, célszerq azokat felszabadítani, ha már nincs rájuk szükség, amint ezt a cat-ban is tettük. Az fclose kimeneti állományra való alkalmazásának másik oka is van: üríti azt a puffert, amelyben a putc a kimenetet gyqjti. (A program normális befejezQdésekor az fclose automatikusan meghívódik minden megnyitott állományra.)

7.7. Hibakezelés; stderr és exit

A hibáknak az a fajta kezelése, amit a cat-ban használtunk, nem ideális. A baj az, hogy ha az állományok egyike valamely oknál fogva hozzáférhetetlen, a hibajelzés a konkatenált kimenet végére nyomtatódik. Ez elfogadható, ha a kimenet a terminálra irányul, azonban rossz, ha egy állományba vagy parancsláncon keresztül egy másik programba megy.
A jobb hibakezelés érdekében az stdin és stdout állománnyal azonos módon a programhoz egy második kimeneti állomány, az stderr is hozzá van rendelve. Ha egyáltalán lehetséges, az stderr-re írt kimenet még akkor is megjelenik a felhasználói terminálon, amikor a szabványos kimenetet átirányították.
Módosítsuk a cat programot úgy, hogy a hibaüzeneteket a szabványos hibaállományra írja!

#include
main(argc, argv) /* cat: állományok konkatenálása*/
int argc;
char *argv[];
{
FILE *fp, *fopen();
if (argc == 1) /* Nincs arg., a szabványos bemenetet másolja*/
filecopy(stdin);
else
while (--argc > 0)
{
if ((fp = fopen(* ++argv, "r")) == NULL) {
fprintf(stderr, "cat: nem nyitható meg %s\n", *argv);
exit(1);
} else {
filecopy(fp);
fclose(fp);
}
}
_exit(0);
}

A program kétféleképpen jelzi a hibákat. Az fprintf által elQállított diagnosztikai kimenet az stderr-re megy, tehát a felhasználó termináljára kerül, és nem tqnik el egy parancsláncon keresztül vagy valamelyik kimeneti állományban.
A program az exit szabványos könyvtári függvényt is használja. Az exit meghívása a program befejezQdését eredményezi. Az exit argumentum bármilyen, az exit-et hívó folyamat rendelkezésére áll, így a program sikeres vagy sikertelen lefutását egy másik program oly módon ellenQrizheti, hogy ezt a programot mint részfolyamatot használja. Megállapodás szerint a 0 visszatérési érték azt jelzi, hogy minden rendben ment, míg a különféle nemnulla értékek a normálistól eltérQ állapotokat jelzik.
Az exit minden megnyitott kimeneti állományra meghívja az fclose-t az összes pufferelt kimenet kiürítése érdekében, majd meghívja az _exit nevq rutint, amelynek hatására a programfutás mindenféle pufferürítés nélkül azonnal végetér. Az exit szükség esetén természetesen közvetlenül is hívható.

7.8. Szövegsorok beolvasása és kivitele
A szabványos könyvtárban rendelkezésre áll az fgets rutin, amely meglehetQsen hasonlít a könyvben végig használt getline függvényhez. Az

fgets(line, MAXLINE, fp)

hívás az fp állományból a line karaktertömbbe beolvassa a következQ bemeneti sort (az újsort is beleértve); legfeljebb MAXLINE-1 sort fog olvasni. A kapott tömb \0-val zárul. Normál esetben az fgets a line-t adja vissza, állomány végén pedig NULL-t. (A getline függvényünk a sorhosszat, ill. állomány vége esetén a nullát adja vissza.)
Kivitelkor az fputs függvény karaktersorozatot (amely nem kell, hogy újsort tartalmazzon) ír az állományra:

fputs(line, fp)

Annak érzékeltetésére, hogy az olyan függvények körül, mint fgets és fputs nincs semmi varázslatos, a szabványos be- és kiviteli könyvtárból közvetlenül ide másoltuk e függvények programkódját:

#include < stdio.h>
char * fgets(s, n, iop) /* Legfeljebb n karakter olvasása iop-ról*/
char *s;
int n;
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);
}

fputs(s, iop) /*Az s karakterláncot az iop állományra írja*/
register char *s;
register FILE *iop;
{
register int c;
while (c = *s++)
putc(c, iop);
}

7.3. Gyakorlat. îrjunk olyan programot, amelyik összehasonlít két állományt, és kiírja az elsQ olyan sort és karakterpozíciót, ahol az állományok eltérnek egymástól!

7.4. Gyakorlat. Módosítsuk az 5. fejezet mintakeresQ programját oly módon, hogy a bemenetét argumentumokként megnevezett állományok halmazából vegye, vagy ha ilyenek nincsenek, akkor a szabványos bemenetrQl! Ki kell-e íratni az állomány nevét, ha a program egymásra illeszkedQ sorokat talál?

7.5. Gyakorlat. îrjunk olyan programot, amely több állományt nyomtat ki, minden újabb állományt új oldalon, cím kiírásával kezd, és az oldalakat állományonként folyamatosan számozza!

7.9. Néhány további függvény
A szabványos könyvtár számos függvényt bocsát rendelkezésünkre, amelyek közül néhány különösen hasznos. Már említettük az strlen, shcpy, strcat és strcmp karakterlánc-kezelQ függvényeket. îme néhány további függvény.

Karakterosztály-vizsgálat és -átalakítás
Több makro végez karaktervizsgálatot és átalakítást:

isalpha(c) nemnulla, ha c alfabetikus, 0, ha nem.
isupper(c) nemnulla, ha c nagybetq, 0, ha nem.
islower(c) nemnulla, ha c kisbetq, 0, ha nem.
isdigit(c) nemnulla, ha c számjegy, 0, ha nem.
isspace(c) nemnulla, ha c szóköz, tab vagy újsor, 0, ha nem.
toupper(c) c átalakítása nagybetqssé.
tolower(c) c átalakítása kisbetqssé.

Az ungetch függvény
A szabványos könyvtárban megtaláljuk a 4. fejezetben általunk megírt ungetch függvény egy meglehetQsen szqkített változatát; ennek neve ungetc.
Az

ungetc(c, fp);

a c karaktert az fp állományba helyezi vissza. 9llományonként csak egy karakternyi visszatolás megengedett. Az ungetc minden olyan bemeneti függvénnyel és makróval együtt használható, mint a scanf, getc vagy a getchar.

Rendszerhívás
A system(s) függvény az s karakterláncban tartalmazott parancsot hajtja végre, majd visszatér az adott program végrehajtásához. Az s tartalma erQsen függ a helyi operációs rendszertQl.Triviális példaként a UNIX-ban a

system("date");

sor hatására lefut a date nevq program; amely kinyomtatja a dátumot és a napon belüli idQpontot.

Tárkezelés
A calloc függvény igen hasonlít a korábbi fejezetekben használt alloc függvényre.

calloc(n, sizeof(objektum))

egy mutatót szolgáltat, amely olyan helyre mutat, ahol elegendQ hely van n darab megadott méretq objektum számára, ill. a NULL értéket adja vissza, ha a kérés nem teljesíthetQ. A tárterület kezdeti nagysága nulla.
A mutató a szóban forgó objektum típusának megfelelQ helyre mutat, azonban típusmódosító szerkezettel a megfelelQ típusúvá kell alakítani, pl. :

char *calloc();
int *ip;
ip = (int *)calloc(n, sizeof(int));

A cfree(p) felszabadítja a p által megcímzett helyet, ahol p-t eredetileg a calloc valamelyik hívásával nyertük. A helyfelszabadítás sorrendjére nincs megkötés, azonban végzetes hiba, ha olyasvalamit szabadítunk fel, amit nem a calloc hívásával nyertünk.
A 8. fejezetben bemutatjuk a calloc-hoz hasonló tárterület-foglaló függvény megvalósítását, amelyben a lefoglalt blokkok tetszQleges sorrendben szabadíthatók.

8. fejezet: Csatlakozás a UNIX operációs rendszerhez

E fejezet anyaga a C programok és a UNIX operációs rendszer közötti kapcsolattal foglalkozik. Mivel a legtöbb C programozó UNIX rendszer alatt dolgozik, ezek az ismeretek az olvasók többsége számára hasznosak lesznek. SQt, még ha az olvasó a C nyelvet más gépen is használja, e példák tanulmányozása révén mélyebb betekintést nyerhet magába a C programozásba is.
A fejezet három fQ témakörre oszlik: bevitel/kivitel, állománykezelés és tárterület-foglalás. Az elsQ két rész feltételezi a UNIX külsQ megjelenésének legalább némi ismeretét.
A 7. fejezet olyan rendszer-határfelülettel foglalkozott, amely számos operációs rendszerben egyforma. Bármelyik konkrét rendszerben a szabványos könyvtár rutinjait a befogadó rendszerben rendelkezésre álló be- és kiviteli szolgáltatások figyelembevételével kell megírni. A következQ néhány szakaszban a UNIX operációs rendszer be- és kiviteli rendszerének alapvetQ belépési pontjait ismertetjük, és azt szemléltetjük, miként lehet ezek segítségével a szabványos könyvtár egyes részeit megvalósítani.

8.1. 9llományleírók

A UNIX operációs rendszerben az összes be- és kivitel állományok írásával és olvasásával valósul meg, mivel az összes periféria, még a felhasználó terminálja is egy-egy állományként jelenik meg. Ez azt jelenti, hogy egyetlen homogén csatolóprogram kezeli a program és a perifériák között az összes kapcsolatot.
A legáltalánosabb esetben egy állomány írása vagy olvasása elQtt értesítenünk kell a rendszert errQl a szándékunkról. Ezt a folyamatot az állomány megnyitásának nevezzük. Ha írni akarunk egy állományba, akkor szükség lehet az állomány létrehozására is. A rendszer ellenQrzi, hogy minderre van-e jogunk (Létezik-e az állomány? Van-e hozzáférési engedélyünk?), és ha minden rendben van, akkor a programhoz egy állományleírónak nevezett kis egész számmal tér vissza. Minden esetben, amikor az állományon be- vagy kiviteli mqveletet akarunk végezni, az állomány azonosítása céljából annak neve helyett az állományleírót használjuk (Ez nagyjából hasonlít a READ(5, ...) és WRITE(6, ...) használatára a FORTRAN-ban.) A megnyitott állományra vonatkozó összes információt a rendszer kezeli, a felhasználói program csupán az állományleírón keresztül hivatkozik az állományra.
Mivel a felhasználói terminálon keresztül megvalósított be- és kivitel egészen mindennapos tevékenység, a UNIX tervezQi igyekeztek ezt minél kényelmesebbé tenni. Amikor a parancsértelmezQ (a shell) valamelyik programot futtatja, három állományt nyit meg a 0, 1 és 2 állományleírókkal, amelyeknek neve szabványos bemenet, szabványos kimenet és szabványos hibakimenet. Alaphelyzetben mindhárom a terminálhoz van rendelve, ha tehát egy program a 0 állományleírót olvassa, ill. az 1 és 2 állományleíróra ír, a terminálon keresztüli be/kivitel közben nem kell törQdnie az állományok megnyitásával.
A program felhasználója az állományokkal folytatott be- és kivitelt átirányíthatja a < és > szimbólumokkal:

prog < infile > outfile

Ebben az esetben a shell a 0 és 1 állományleíróra vonatkozó alap-hozzárendeléseket a terminálról a megnevezett állományokra irányítja. Normál esetben a 2 állományleíró továbbra is a terminálhoz lesz rendelve, így a hibaüzenetek oda íródhatnak ki. Hasonlóképpen jellemezhetjük azt az esetet, amikor a bemenet vagy a kimenet valamilyen parancslánchoz kapcsolódik. MegemlítendQ, hogy az állomány-hozzárendeléseket mindig a shell változtatja meg, nem pedig a program. Maga a program mindaddig nem tudja, hogy a bemenete honnan jön és a kimenete hová megy, amíg a 0 állományt használja bevitelre és az 1 és 2 állományt kivitelre.

8.2. Alacsony szintq bevitel és kivitel; read és write

A be- és kivitel UNIX-beli legalacsonyabb szintje nem nyújt sem pufferelést, sem egyéb szolgáltatást: ez valójában az operációs rendszer közvetlen belépési pontja. Az összes bevitelt és kivitelt két függvény végzi, amelyeknek neve: read és write. Az elsQ argumentum mindkét esetben az állományleíró. A második argumentum a programunkban elhelyezett puffer, ahonnan az adatok érkeznek, ill. ahová beíródnak. A harmadik argumentum az átvitelre kerülQ byte-ok száma. A hívások:

n_read = read(fd, buf, n);
n_written = write(fd, buf, n);

Mindegyik hívás byte-darabszámot ad vissza, amely a ténylegesen átvitt byte-ok száma. Olvasáskor a visszaadott byte-szám az elQírtnál (n) kisebb lehet. A nulla byte-visszatérési érték az állomány végét jelenti, míg a -1 valamilyen hibára utal. îrás esetén a visszaadott érték a ténylegesen felírt byte-ok száma; általában hibát jelent, ha ez nem egyezik meg a felírandó byte-ok elQre megadott számával.
Az olvasandó vagy írandó byte-ok száma teljesen tetszQleges. A két legközönségesebb érték az 1 , amely egyidQben egy karakter átvitelét jelenti (pufferetlen átvitel) és az 512, amely a legtöbb periféria esetében a fizikai blokkméretnek felel meg. Az utóbbi méret a leghatékonyabb, de még a karakterenkénti be- és kivitel sem rendkívül költséges.
Ezen ismeretek birtokában megírhatunk egy egyszerq programot, amely a bemenetét a kimenetére másolja - ez az 1. fejezetben megírt állománymásoló programnak felel meg. UNIX alatt ez a program bármit bármire másol, mivel a bemenet és a kimenet bármilyen állományra vagy perifériára átirányítható.

#define BUFSIZE 512 /* Legjobb méret a PDP-11 UNIX-ra*/
main() /*A bemenet másolása a kimenetre*/
{
char buf[BUFSIZE];
int n;
while ((n = read(0, buf, BUFSIZE)) > 0)
write(1, buf, n);
}

Ha az állományméret nem a BUFSIZE többszöröse, akkor valamelyik read egy ennél kisebb számot ad át write-nak a felírandó byte-ok számaként; a read ezután következQ hívása nullát fog visszaadni.
Tanulságos látnunk, hogyan használható read és write, olyan magasabb szintq rutinok létrehozására, mint a getchar, putchar stb. îme pl. a getchar egy változata, amely pufferelés nélküli olvasást végez:

#define CMASK 0377 /*A char-ok 0-vá tételére*/
getchar() /*Puffereletlen egykarakteres bevitel*/
{
char c;
return((read(0, &c, 1) > 0) ?c & CMASK : EOF);
}

A c-t char-nak kell deklarálni; mivel a read karaktermutatót fogad. A visszaadott karaktert 0377-tel maszkolni kell, hogy biztosan pozitív legyen - ellenkezQ esetben az elQjel-kiterjesztés következtében negatívvá válhat. (A 0377 állandó a PDP-11 számára megfelelQ, de nem feltétlenül jó más gépek esetén.)
A getchar második változata nagy egységekben végzi az olvasást, és egyenként adja ki a karaktereket:

#define CMASK 0377 /*A char-ok 0-vá tételére*/
#define BUFSIZE 512
getchar() /*Pufferelt változat*/
{
static char buf[BUFSIZE];
static char *bufp = buf;
static int n = 0;
if (n == 0) { /*A puffer üres*/
n = read(0, buf, BUFSIZE);
bufp = buf;
}
return ((--n >= 0) ? *bufp++ & CMASK : EOF);
}

8.3. Open, creat, close, unlink

Az alapértelmezés szerinti szabványos bemeneti, kimeneti és hibakimeneti állományon kívül az összes állományt explicit módon meg kell nyitnunk, ha azokat írni vagy olvasni akarjuk. EbbQl a célból két rendszerbelépési pont áll rendelkezésre : az open és a creat (vigyázat, nem create!).
Az open lényegében ugyanolyan, mint a 7. fejezetben tárgyalt fopen, eltekintve attól, hogy nem állománymutatót ad vissza, hanem állományleírót, ami egyszerqen egy int.

int fd;
fd = open(name, rwmode);

Az fopen-hez hasonlóan a name argumentum a külsQ állománynévnek megfelelQ karakterlánc. Az elérés módja azonban eltérQ: az rwmode értéke olvasáskor 0, íráskor 1, és egyidejq írási-olvasási hozzáférés esetén 2. Hiba elQfordulásakor az open -1-et ad vissza, egyébként a visszatérési érték az érvényes állományleíró. Hibához vezet, ha nem létezQ állományt próbálunk megnyitni.
A creat belépési pont új állományok létrehozására vagy régiek felülírására szolgál:

fd = creat(name, pmode);

állományleírót ad vissza, ha létre tudta hozni a name nevq állományt, és -1-et, ha nem. Ha az állomány már létezik, a creat nulla hosszúságúra vágja le, nem jelent tehát hibát már létezQ állomány creat-tel történQ létrehozása.
Ha az állomány vadonatúj, a creat azt a pmode argumentumban megadott védelmi móddal hozza létre. A UNIX rendszerben minden állományhoz kilenc bitbQl álló védelmi információ társul. Ezek a bitek az állomány tulajdonosára, a tulajdonos csoportjára, valamint a másokra vonatkozó olvasási, írási és végrehajtási engedélyeket szabályozzák. Az engedélyeket így legkényelmesebben egy háromjegyq oktális számmal adhatjuk meg. Pl. 0755 olvasási-írási-végrehajtási engedélyt ad a tulajdonosnak, és olvasási-végrehajtási engedélyt a csoport tagjainak és mindenki másnak.
Szemléltetés céljából közöljük a UNIX cp nevq segédprogramjának egyszerqsített változatát, amely egy állományt egy másikba másol. (A fQ egyszerqsítés az, hogy az itt közölt változat csak egyetlen állományt másol és nem teszi lehetQvé, hogy a második argumentum katalógus (directory) legyen.)

#define NULL 0
#define BUFSIZE 512
#define PMODE 0644 /* RW a tulajdonosnak, R a csoportnak és másoknak*/
main(argc, argv) /*cp: f1 másolása f2-be*/
int argc;
char *argv[];
{
int f1, f2, n;
char buf[BUFSIZE];
if (argc != 3)
error("Használat: cp honnan hová", NULL);
if ((f1 = open(argv[1], 0)) == -1)
error("cp: nem nyitható meg %s", argv[1]);
if ((f2 = creat(argv[2], PMODE)) == -1)
error("cp: nem hozható létre %s", argv[2]);
while ((n = read(f1, buf, BUFSIZE)) > 0)
if (write(f2, buf, n) != n)
error("cp: íráshiba", NULL);
exit(0);
}

error(s1, s2) /*A hibaüzenetet kiírja és leáll*/
char *s1, *s2;
{
printf(s1, s2);
printf("\n");
exit(1);
}

A programok által egyidejqleg nyitva tartható állományok száma korlátozott (tipikusan 15-25). Ennek megfelelQen minden olyan programot, amelynek sok állományt kell feldolgoznia, úgy kell elkészíteni, hogy képes legyen az állományleírók újbóli használatára. A close rutin megszakítja az állományleíró és a megnyitott állomány közötti kapcsolatot és felszabadítja az állományleírót, így azt a késQbbiekben más állomány használhatja. A program exit hatására történQ befejezése és a fQprogramból való visszatérés az összes megnyitott állományt lezárja.
Az unlink(filename) függvény a filename nevq állományt törli az állományrendszerbQl.

8.1. Gyakorlat. îrjuk át a 7. fejezetben látott cat programot úgy, hogy a read, write, open és close rutinokat használjuk azok szabványos könyvtárbeli megfelelQi helyett! Végezzünk kísérleteket a két változat egymáshoz viszonyított sebességének meghatározására!

8.4. Véletlen hozzáférés; seek és lseek

9llományok be- és kivitele általában soros: minden read és write az állománynak azon a pozícióján történik, amely közvetlenül a megelQzQ be- vagy kivitel állománybeli pozícióját követi. Szükség esetén azonban az állomány tetszQleges sorrendben olvasható vagy írható. Az lseek rendszerhívás lehetQvé teszi, hogy tényleges olvasás vagy írás nélkül mozoghassunk az állományban:

lseek(fd, offset, origin);

hatására az fd leírójú állományban az aktuális pozíció az offset pozícióra mozdul, amelyet az origin által meghatározott helyhez képest relatíven értelmezünk. Az ezt követQ olvasás vagy írás ezen az új pozíción fog kezdQdni. Az offset long típusú: az fd és az origin int típusúak. Az origin 0, 1 vagy 2 lehet, jelezve, hogy az offset-et az állomány elejétQl, a pillanatnyi pozíciótól, vagy az állomány végétQl kell számítani. Ha pl. az állományhoz valamit hozzá akarunk függeszteni, írás elQtt keressük meg az állomány végét:

lseek(fd, 0L, 2);

Ha vissza akarunk térni az állomány elejére ("visszatekercselés"):

lseek(fd, 0L, 0);

Figyeljük meg a 0L argumentumot, ezt (long)0-nak is írhatnánk.
Az lseek használatával lehetQségünk van arra, hogy az állományokat - lassúbb hozzáférés árán - nagy tömbökhöz hasonlóan kezeljük. Az alábbi egyszerq függvény pl. az állomány tetszQleges pontjáról tetszQleges számú byte-ot olvas be:

get(fd, pos, buf, n) /*n byte olvasása a pos pozícióról*/
int fd, n;
long pos;
char *buf;
{
lseek(fd, pos, 0); /*Elmegy pos-ra*/
return(read(fd, buf, n));
}

A UNIX rendszer 7-est megelQzQ változataiban a be- és kiviteli rendszer alapvetQ belépési pontjának neve: seek. A seek és az lseek azonosak, attól eltekintve, hogy az elQbbinek az offset argumentuma nem long, hanem int. Ennek megfelelQen, mivel a PDP- 11 int-ek 16 bitesek, a seek-nek megadható offset felsQ korlátja 65535; ezért a 3, 4, 5 origin értékek hatására a seek a megadott offset értéket 512-vel (a fizikai blokkban található byte-ok számával) megszorozza, majd az origin-t úgy értelmezi, mintha az adott sorrendben 0, 1 vagy 2 lenne. îly módon, ha egy nagy állomány tetszQleges pontjára akarunk lépni, akkor két seek-re van szükségünk: az elsQvel a blokkot választjuk ki, a másodikkal pedig, amelyben az origin értéke 1 , a blokkon belül a kívánt byte-ra mozdulunk.

8.2. Gyakorlat. Világos, hogy az lseek a seek felhasználásával megírható és viszont. îrjuk meg mindkettQt a másik felhasználásával!

8.5. Példa; az fopen és a getc megvalósítása

Próbáljuk meg egységbe foglalva szemléltetni a mondottakat az fopen és getc szabványos könyvtári rutinok egyik megvalósításának bemutatásával.
Emlékezzünk arra, hogy a szabványos könyvtár állomány-ait nem állományleírók, hanem állománymutatók jellemzik. Ez utóbbiak olyan struktúrára mutatnak, amely az állományra vonatkozó különbözQ információkat tartalmaz: egy puffert megcímzQ mutatót, ami lehetQvé teszi az információ nagy darabokban történQ beolvasását; a pufferben maradt karakterek darabszámát; a következQ pufferbeli karakterpozíciót megcímzQ mutatót; néhány jelzQt (flag-et), amelyek pl. az olvasás/írás módot írják le; és végül az állományleírót.
Az állományt leíró adatstruktúra az stdio.h állományban található, amelyet (#include-dal) minden olyan forrásállományba be kell iktatni, amely a szabványos könyvtár valamelyik rutinját használja. A könyvtárbeli függvények ugyancsak tartalmazzák. Az stdio.h-ból vett alábbi kivonatban azok a nevek, amelyeket csak a könyvtárbeli függvények használhatnak, aláhúzással kezdQdnek, így kisebb annak a valószínqsége, hogy valamelyik felhasználói programbeli névvel összeütközésbe kerüljenek.

#define BUFSIZE 512
#define NFILE 20 /*KezelhetQ állományok száma*/
typedef struct iobuf {
char *ptr; /*KövetkezQ karakterpozíció*/
int cnt; /*Megmaradt karakterek száma*/
char *_base; /*A puffer címe*/
int flag; /*Az állományelérés módja*/
int fd; /*9llományleíró*/
}FILE;
extern FILE iob[NFILE];
#define stdin (&iob[0])
#define stdout (&iob[1])
#define stderr (&iob[2])
#define _READ 01 /*9llománymegnyitás olvasásra*/
#define _WRITE 02 /*9llománymegnyitás írásra*/
#define UNBUF 04 /*Az állomány puffereletlen*/
#define BIGBUF 010 /*Nagy pufferlefoglalás*/
#define EOF 020 /*EOF fordult elQ ebben az állományban*/
#define ERR 040 /*Hiba fordult elQ ebben az állományban*/
#define NULL 0
#define EOF (-1)
#define getc(p) (--(p) - ) cnt >= 0\
? *(p) > ptr++ & 0377 : fillbuf(p))
#define getchar() getc(stdin)
#define putc(x, p) (--(p) == cnt >= 0\
? *(p) == ptr++ = (x) : flushbuf((x), p))
#define putchar(x) putc(x, stdout)

A getc makró normál esetben egyszerqen dekrementálja a darabszámot, elQrelépteti a mutatót, és visszaadja a karaktert. (A hosszú #define-okat fordított \ törtvonallal lehet folytatni.) Ha a darabszám negatívvá válik, a getc meghívja a _fillbuf függvényt, amivel újratölti a puffert, újrainicializálja a struktúra tartalmát, és egy karaktert ad vissza. A függvények rendelkezhetnek gépfüggetlen csatlakozófelülettel, akkor is, ha maguk gépfüggQ konstrukciókat tartalmaznak: a getc 0377-tel maszkolja a karaktert, amely felülbírálja a PDP-11 által végrehajtott elQjel-kiterjesztést, és biztosítja, hogy minden karakter pozitív legyen.
Bár nem kívánunk részletekbe menni, mégis beiktattuk a putc definícióját annak bemutatására, hogy az lényegében ugyanúgy mqködik, mint a getc, azaz amikor a puffere megtelt, meghívja a _flushbuf függvényt.
Ezek után megírhatjuk az fopen függvényt. Az fopen legnagyobb része azzal foglalkozik, hogy megnyitja az állományt, a megfelelQ helyre pozicionálja, és úgy állítja be a jelzQbiteket, hogy azok a helyes állapotot mutassák. Az fopen pufferterületet nem foglal le : ezt az állomány elsQ olvasásakor a _fillbuf végzi.

#include
#define PMODE 0644 /* RWW a tulajdonosnak; R másoknak*/
FILE *fopen(name, mode) /*Megnyítja az állományt, az állománymutatót adja vissza*/
register char *name, *mode;
{
register int fd;
register FILE *fp;
if (*mode != 'r' && *mode != 'w' && *mode != 'a') {
fprintf(stderr, "tiltott mód %s a %s megnyitásakor \n", mode, name);
exit( 1 );
}
for (fp = &iob; fp < iob + NFILE; fp++)
if ((fp->~flag & (_READ & _WRITE)) == 0)
break; /*Szabad területet talált*/
if (fp >= iob + &NFILE) /*Nincs szabad hely*/
return(NULL);
if (*mode == 'w') /*9llományhozzáférés*/
fd = creat(name, PMODE);
else
if (*mode == 'a') {
if ((fd = open(name, 1)) == -1)
fd = creat(name, PMODE);
lseek(fd, 0L, 2);
} else
fd = open(name, 0);
if (fd == -1) /* Nem tudta a nevet elérni*/
return(NULL);
fp->_fd = fd;
fp->cnt = 0;
fp->_base = NULL;
fp->_flag &= ^(_READ & _WRITE);
fp->flag &= (*mode == 'r') ? _READ : _WRITE)_; retum(fp); ;
}

A _fillbuf függvény jóval bonyolultabb. A bonyolultság fQ oka, hogy _fillbuf akkor is megkísérli az állomány-hozzáférés engedélyezését, ha esetleg a be- és kivitel puffereléséhez nincs elegendQ tár. Ha a calloc-tól további hely nyerhetQ újabb puffer létrehozására, akkor minden rendben van. Ha nem, akkor a fillbuf puffereletlenbe- és kivitelt végez egyetlen karakter használatával, amelyet az egyik saját tömbjében tárol.

#include
fillbuf(fp) /*Bemeneti puffer lefoglalása és feltöltése*/
register FILE *fp;
{
static char smallbuf[NFILE]; /*puffereletlen I/O-ra*/
char *calloc();
if ((fp->flag& _READ) == 0 || (fp->flag& (_EOF || ERR)) != 0)
return(EOF);
while (fp->base == NULL) /*Pufferterületet keres*/
if (fp->flag& UNBUF) /*Puffereletlen*/
fp->base = &smallbuf[fp->fd];
else
if ((fp->base = calloc(BUFSIZE, 1)) == NULL)
fp->flag &= UNBUF; /*Nem kap nagy puffert*/
else
fp->flag &= BIGBUF; /*Nagy puffert kapott*/
fp->ptr = fp->base;
fp->cnt = read(fp->fd, fp->ptr, fp-> ~flag & _UNBUF ? 1 : _BUFSIZE);
if (--fp->cnt < 0) {
if (fp->cnt = -1)
fp->flag &= &EOF;
else
fp->flag &= ERR;
fp->cnt = 0;
return(EOF);
}
return(*fp->*ptr++ & 0377); /*A karaktert pozitívvá teszi*/
}

A getc valamely állományra vonatkozó elsQ hívásakor a darabszám 0, ami elQidézi a fillbuf meghívását. Ha a _fillbuf úgy találja, hogy az állomány nincs olvasásra megnyitva, azonnal az EOF értékkel tér vissza. Egyébként megkísérli a nagy puffer lefoglalását, és ha ez nem sikerül, az egykarakteres puffert utalja ki a _flag-beli pufferelési információ értelemszerq beállításával.
Ha egyszer a puffer létrejött, a _fillbuf annak feltöltésére egyszerqen meghívja a read rutint, beállítja a darabszámot és a mutatókat, majd a puffer kezdetén található karakterrel tér vissza. A fillbuf további hívásaikor a puffer már rendelkezésre áll.
Az egyetlen dolog, amit még nem tisztáztunk, hogy mindez hogyan indul. Az stdin, stdout és stderr számára definiálni és inicializálni kell az iob tömböt:

FILE iob[NFILE] = {
{NULL, 0, NULL, READ, 0}, /* stdin*/
{NULL, 0, NULL, WRITE, 1}, /*stdout*/
{NULL, 0, NULL, WRITE, UNBUF, 2} /* stderr*/
};

A struktúra flag részének inicializálása mutatja, hogy stdin-t olvasni, stdout-ot írni kell, stderr-re pedig pufferelés nélkül írunk.

8.3. Gyakorlat. îrjuk át fopen-t és _fillbuf-ot úgy, hogy explicit bitmqveletek helyett mezQket használunk!

8.4. Gyakorlat. Tervezzük és írjuk meg a _flushbuf és fclose rutinokat!

8.5. Gyakorlat. A szabványos könyvtárban rendelkezésünkre áll az
fseek(fp, offset, origin)
függvény, amely azonos az lseek függvénnyel attól eltekintve, hogy fp állománymutató és nem állományleíró. îrjuk meg fseek-et! Gondoskodjunk arról, hogy az általunk írt fseek helyesen mqködjön együtt a könyvtár többi függvényei számára végzett pufferkezeléssel!

8.6. Példa; katalógusok kilistázása

IdQnként az eddigiektQl eltérQ jellegq párbeszédet kell folytatnunk az állományrendszerrel: magára az állományra vonatkozó információra van szükségünk, nem pedig arra, hogy mit tartalmaz az állomány. Példa erre az ls (list directory) nevq UNIX parancs, amely kinyomtatja az adott katalógusban található állományok nevét, és kívánság szerint egyéb információt is közöl, mint pl. a méreteket, az engedélyeket stb.
Mivel legalábbis a UNIX esetében a katalógus maga is egy állomány, semmi különös nincs az olyan parancsokban, mint az ls: beolvas egy állományt, és kiemeli belQle a számára fontos információt. Ennek az információnak a formátumát ugyanakkor maga a rendszer határozza meg, nem pedig a felhasználói program, így az ls-nek ismernie kell az operációs rendszer ábrázolásmódját.
E megjegyzések közül néhányat az fsize program megírásával fogunk szemléltetni. Az fsize az ls olyan speciális formája, amely az argumentumlistájában megnevezett összes állomány méretét kinyomtatja. Ha az állományok valamelyike katalógus, az fsize erre rekurzívan alkalmazza önmagát. Ha egyáltalán nem adtunk meg argumentumot, az aktuális katalógust dolgozza fel.
Indulásként röviden átismételjük az állománykezeléssel kapcsolatos tudnivalókat. A katalógus (directory) olyan állomány, amely állománynevek listáját tartalmazza, és utal arra, hogy a megfelelQ állományok hol találhatók.
Az állományok címe valójában egy másik táblázatba, az inode táblázatba mutató index. Az állomány inode-ja az a hely, ahol a nevet kivéve az állományra vonatkozó összes információ tárolódik. A katalógus bejegyzés csupán két tételt tartalmaz: az inode-számot és az állomány nevét. A pontos specifikáció a sys/dir.h állomány beiktatásával jön létre, amelynek tartalma:

#define DIRSIZ 14 /*Az állománynév max. hossza*/
struct direct /*A katalógusbejegyzés struktúrája*/
{
ino_t d_ino; /* Inode-szám*/
char d_name[DIRSIZ]; /*9llománynév*/
};

Az ino_t típus olyan typedef, amely az inode-táblázatba mutató indexet ír le. A PDP 11 UNIX esetében ez unsigned, de ilyenfajta információt nem szokás a programba ágyazni: más rendszerben ez eltérQ lehet. Innen a typedef. A rendszertípusok teljes készlete a sys/types.h-ban található.
A stat függvény veszi az állomány nevét, és az annak inode-jában található összes információt (vagy hiba esetén -1-et) adja vissza. Eszerint:

struct stat stbuf;
char *name;
stat(name, &stbuf);

az állománynévre vonatkozó inode információval tölti fel az stbuf struktúrát. A stat által visszaadott értéket leíró struktúra a sys/stat.h-ban található, formája a következQ:

struct stat /*A stat által visszaadott struktúra*/
{
dev_t st_dev; /*Az inode perifériája*/
ino_t st_ino; /*Inode-szám*/
short st_mode; /*Mód bitek*/
short st_nlink; /*Az állományra mutató linkek száma*/
short st_uid; /*A tulajdonos felhasználó azonosítója*/
short st_gid; /*A tulajdonos csoportjának azonosítója*/
dev_t st_rdev; /*Speciális állományokra*/
off_t st_size; /*9llományméret karakterekben*/
time_t st_atime; /*Az utolsó hozzáférés idQpontja*/
time_t st_mtime; /*Az utolsó módosítás idQpontja*/
time_t st_ctime; /*Az eredeti létrehozás idQpontja*/
};

Ezek legtöbbjét a megjegyzések megmagyarázzák. Az st_mode bejegyzés az állományt leíró jelzQket tartalmaz; a kényelem kedvéért a jelzQdefiníciók ugyancsak részei a sys/stat.h-nak.

#define S_IFMT 0160000 /*Az állomány típusa*/
#define S_IFDIR 0040000 /*Katalógus*/
#define S_IFCHR 0020000 /*Speciális karakter*/
#define S_IFBLK 0060000 /*Speciális blokk*/
#define S_IFREG 0100000 /*Szabályos*/
#define S_ISUID 04000 /*Felhasználói azonosító beállítása végrehajtásra*/
#define S_ISGID 02000 /*csoportazonosító beállítása végrehajtásra*/
#define S_ISVTX 01000 /*Az átvitt szöveget használat után menti*/
#define S_IREAD 0400 /*Olvasási engedély*/
#define S_IWRITE 0200 /*îrási engedély*/
#define S_IEXEC 0100 /*Végrehajtási engedély*/

Most már meg tudjuk írni az fsize programot. Ha a stat-tól kapott mód azt jelzi, hogy az állomány nem katalógus, akkor a rendelkezésre álló méret közvetlenül kinyomtatható. EllenkezQ esetben a katalógust állományonként fel kell dolgoznunk: ez maga is tartalmazhat alkatalógusokat, így a folyamat rekurzív.
A fQrutin szokás szerint elsQsorban a parancssor-argumentumokkal foglalkozik: egy nagy pufferben ad át minden egyes argumentumot az fsize függvénynek.

#include
#include /*typedef-ek*/
#include /*Katalógusbejegyzés struktúra*/
#include /*A stat által visszaadott struktúra*/
#define BlJFSIZE 256
main(argc, *argv) /*fsize: állományméretek kinyomtatása*/
int argc;
char *argv[];
{
char buf[BUFSIZE];
if (argc == 1) { /*Alapértelmezés: az aktuális katalógus*/
strcpy(buf, ".");
fsize(buf);
}
else
while (argc > 0) {
strcpy(buf, *++argv);
fsize(buf);
}
}

Az fsize függvény az állomány méretét nyomtatja ki. Azonban ha az állomány katalógus, akkor elQször az összes benne levQ állomány kezelése érdekében meghívja a directory függvényt. Figyeljük meg a stat.h-ban az S_IFMT és S_IFDIR jelzQnevek használatát:

fsize(name) /*Kinyomtatja a megadott nevq állomány méretét*/
char *name;
{
struct stat stbuf;
if (stat(name, &stbuf) == -1) {
fprintf(stderr, "fsize: %s nem található\n", name);
return;
}
if ((stbuf.st_mode& S_IFMT) == S_IFDIR)
directory(name);
printf("%0ld %s\n", stbuf.st_size, name);
}

A directory függvény a legbonyolultabb. A legnagyobb része azonban a szóban forgó állomány teljes elérési nevének (pathname) elQállításával foglalkozik.

directory(name)
char *name;
{
struct direct dirbuf;
char *nbp, *nep;
int i, fd;
nbp = name + strlen(name);
nbp++ = '/'; /*/ hozzáadása a katalógus nevéhez*/
if (nbp+DIRSIZ+2 >= name+BUFSIZE) /*A név túl hosszú*/
return;
if ((fd = open(name, 0)) == -1)
return;
while (read(fd, (char *)&dirbuf, sizeof(dirbuf)) > 0 {
if (dirbuf.d_ino == 0) /*A rovat nincs használatban*/
continue;
if (strcmp(dirbuf.d_name, ".") == 0 && strcmp(dirbuf.d_name, "..") == 0)
continue; /*Önmagát és a szülQt átugorja*/
for (i = 0 , nep = nbp; i < DIRSIZ; i++)
*nep++ = dirbuf.d_name[i];
*nep++ = '\0';
fsize(name);
}
close(fd);
_nbp = '\0'; /*Név helyreállítása*/
}

Ha a katalógus adott rovata éppen nincs használatban (mivel az állományt átnevezték), a mód bejegyzés nulla, és ezt a pozíciót átugorjuk. Minden katalógus tartalmazza bejegyzésként önmagát a "."név alatt, valamint a szülQjét a ".." név alatt. Ezeket nyilván át kell ugrani, különben a program jó ideig futni fog.
Bár az fsize program meglehetQsen speciális, számos fontos gondolatot mutat be. ElQször is, sok program nem rendszerprogram, csupán olyan információt használ, amelynek formáját vagy tartalmát az operációs rendszer kezeli. Másodszor, ilyen programok esetében lényeges, hogy az információ ábrázolása csak olyan szabványos, ún. fej (header) állományokban jelenjen meg, mint stat.h és dir.h, továbbá, hogy a programok a konkrét deklarációk alkalmazása helyett ezeket az állományokat iktassák be.

8.7. Példa; tárterület lefoglalása

Az 5. fejezetben az alloc egyszerqsített változatát mutattuk be. A most megírandó változat már nem tartalmaz korlátozásokat abban az értelemben, hogy most az alloc és a free hívásai tetszQleges sorrendben követhetik egymást, szükség esetén az alloc az operációs rendszertQl igényel további tárterületet. Ezek a rutinok önmagukban is hasznosak, emellett rávilágítanak: arra, hogyan lehet gépfüggQ programokat viszonylag gépfüggetlen módon megírni, és a struktúrák, az unionok, ill. a typedef valós életbQl vett alkalmazásait is bemutatják.
Az alloc a helyfoglalást nem a program részét képezQ, rögzített méretq tömbbQl végzi, hanem szükség szerint az operációs rendszertQl igényel újabb tárterületet. Mivel a programban folyó egyéb tevékenységek aszinkron módon ugyancsak igényelhetnek helyet, elQfordulhat, hogy az alloc által kezelt terület nem lesz folytonos. îgy a szabad terület szabad blokkokból álló láncot alkot. A blokkok a tulajdonképpeni szabad hely mellett egy méretet és egy, a következQ blokkot megcímzQ mutatót tartalmaznak. NövekvQ tárcím szerint követik egymást, és az utolsó (legmagasabb címq) blokk a legelsQre mutat. îly módon a lánc valójában gyqrqt képez.
Tárkérés esetén a program átvizsgálja a szabad blokkok listáját, hogy tartalmaz-e elegendQen nagy szabad blokkot. Ha a talált blokk mérete pontosan megegyezik a kért mérettel, akkor lekapcsolja a listáról és átadja a felhasználónak. Ha a- blokk túlságosan nagy, akkor a program kettévágja, és a felhasználónak csak a megfelelQ méretq területet utalja ki, a maradékot pedig visszahelyezi a szabad listába. Végül, ha nem talált elegendQen nagy blokkot, akkor újabb blokkot kér az operációs rendszertQl, rákapcsolja a szabad listára, majd újra kezeli a keresést.
A blokkfelszabadítás szintén a szabad lista vizsgálatával indul, a programnak ugyanis keresnie kell a listában egy olyan helyet, ahová a felszabadítani kívánt blokkot beillesztheti. Ha a felszabadított blokk bármelyik oldalán szomszédos egy listabeli blokkal, akkor a kettQ egyetlen, nagyobb blokká egyesül, így a tár nem töredezik fel túlságosan. A szomszédosság tényét könnyen megállapíthatjuk, hiszen a szabad listában a blokkokat címnövekvQ sorrendben tartjuk nyilván.
Az egyik probléma, amit az 5. fejezetben érintettünk annak biztosítása volt, hogy az alloc által visszaadott terület helyesen illeszkedjen azokhoz az objektumokhoz, amelyeket ott tárolni kívánunk. Bár a gépek különbözQek, minden gépen létezik egy olyan típus, amely, ha egy adott címen tárolható, akkor ott az összes többi típus is biztosan tárolható. Pl. az IBM 360/370, a Honeywell 6000 és sok más gép esetében bármilyen objektum tárolható olyan határon, amely a double számára, a PDP 11 esetében pedig az int számára megfelelQ.
A szabad blokkban a tulajdonképpeni szabad területet megelQzQ vezérlési információt (a láncban következQ blokkot megcímzQ mutatót és a blokk méretét) fejnek nevezzük. Az illesztés egyszerqsítése érdekében minden blokk a fejméret többszöröse, maga a fej pedig megfelelQen illeszkedik. Ezt az alábbi unionnal érhetjük el, amely tartalmazza a kívánt fejstruktúrát, valamint a legnehezebben illeszthetQ típusra vonatkozó kitételt:

typedef int ALIGN; /*Illeszkedést biztosít a PDP-11-en*/
union header { /*Szabad blokk fej*/
struct {
union header *ptr; /*Köv. szabad blokk*/
unsigned size; /*Ennek a szabad blokknak a mérete*/
} s;
ALIGN x; /*A blokkok illesztése*/
};
typedef union header HEADER;

Az alloc rutinban a karakterekben elQírt méretet felkerekítjük a megfelelQ számú fejméretq egységgé. A ténylegesen kiutalt blokk eggyel több ilyen egységet tartalmaz, t.i. egy egységre magának a fejnek is szüksége van, és ez a darabszám kerül a fej size mezQjébe. Az alloc által visszaadott mutató a szabad területre mutat, nem pedig magára a fejre.

static HEADER base; /*Üres lista az induláshoz*/
static HEADER *allocp = NULL; /*Az utolsó lefoglalt blokk*/
char *alloc(nbytes) /*9ltalános célú tárfoglaló*/
unsigned nbytes;
{
HEADER *morecore();
register HEADER *p, *q;
register int nunits;
nunits = 1 + (nbytes + sizeof(HEADER) -1) / sizeof(HEADER);
if ((q = allocp) == NULL) { /*Még nincs szabad lista*/
base.s.ptr = allocp = q = &base;
base.s.size = 0;
}
for (p = q->s.ptr; ; q = p , p = p->s.ptr) {
if (p->s.size >= nunits) { /*Elég nagy*/
if (p->s.size == nunits) /*Pontosan akkora*/
q->s.ptr = p->s.ptr;
else { /*A hátsó felét foglalja le*/
p->s.size -= nunits;
p += p->s.size;
p->s.size = nunits;
}
allocp = q;
return((char *)(p + 1));
}
if (p == allocp) /*Körüljárt aa szabad listát*/
if ((p = morecore(nunits)) == NULL)
return(NULL); /*Nincs több*/
}
}

A base nevq változót használjuk induláskor. Ha, mint alloc elsQ hívásakor, az allocp értéke NULL, egy elfajult szabad lista jön létre: egyetlen, nulla méretq blokkot tartalmaz és saját magára mutat. Ezután a program minden esetben végigkeresi a szabad listát. A megfelelQ méretq szabad blokkot azon az (allocp) ponton kezdi keresni, ahol legutoljára talált szabad blokkot; ez a stratégia elQsegíti, hogy a lista homogén maradjon. Ha a program túl nagy blokkot talál, akkor a felhasználó a blokk második felét kapja meg, íly módon az eredeti fejben csak a méretet kell helyesbíteni. A felhasználónak átadott mutató mindig a tényleges szabad területre mutat, amely egy egységgel a fej mögött helyezkedik el. Figyeljük meg, hogy p karakterré alakul át, mielQtt az alloc visszaadná.
A morecore függvény az operációs rendszertQl kér tárterületet. Ennek megoldási módja természetesen operációs rendszertQl függQen változik. A UNIX-ban az sbrk(n) rutin olyan mutatót ad vissza, amely n byte-nyi tárterületre mutat. (A mutató minden illeszkedési megkötésnek eleget tesz.) Mivel tár kérése a rendszertQl viszonylag költséges mqvelet, ezt nem akarjuk az alloc minden hívásakor megtenni, ezért a morecore a kért egységek számát nagyobb értékre kerekíti fel; ezt a nagyobb blokkot aztán szükség szerint darabolhatjuk fel. A megnövelés értéke olyan paraméter, amely az igényeknek megfelelQen változtatható.

#define NALLOC 128 /*Az egyszerre lefoglalandó egységek száma*/
static HEADER *morecore(nu) /*Tár kérése a rendszertQl*/
unsigned nu;
{
char *sbrk();
register char * cp;
register HEADER *up;
register int rnu;
rnu = NALLOC *((nu + NALLOC -=1) / NALLOC);
cp = sbrk(rnu *sizeof(HEADER));
if ((int)cp == -1) /*Egyáltalán nincs hely*/
return(NULL);
up = (HEADER *)cp;
up->s.size = rnu;
free((char *)(up + 1));
return(allocp);
}

Amennyiben nem volt hely, az sbrk = -1-et ad vissza, bár a NULL célszerqbb választás lett volna. A biztonságos összehasonlíthatóság érdekében a -1-et int-té kell alakítani. Ismét sqrqn használtuk a típusmódosítást, így a függvény viszonylag érzéketlen az egyes gépek mutatóábrázolásának különbözQségére.
Maga a free utolsónak maradt. Egyszerqen átvizsgálja a szabad listát az allocp-tQl kezdve, miközben keresi a szabad blokk beillesztésére alkalmas helyet. Ez vagy két, már létezQ blokk közé esik, vagy a lista végén van. Ha a felszabadítandó blokk bármelyik esetben szomszédos valamely másik szabad blokkal, akkor a program a kettQt egyesíti. Csupán arra kell ügyelni, hogy a mutatók mindig a megfelelQ helyre mutassanak és a méretek helyesek legyenek!

free(ap) /*Az ap blokkot a szabad listába teszi*/
char *ap;
{
register HEADER *p, *q;
p = (HEADER *)ap - 1; /*A fejre mutat*/
for (q=allocp; !(p > q && p < q->s.ptr); q=q->s.ptr)
if (q >= q->s.ptr && (p > q || p < q->s.ptr))
break; /*Egyik vagy másik végén*/
if (p + p->s.size == q->s.ptr) { /*Egyesül a felsQ szomszéddal*/
p->s.size += q->s.ptr->s.size;
p->s.ptr = q->s.ptr->s.ptr;
}
else
p->s.ptr = q->s.ptr;
if (q + q->s.size == p) { /*Egyesül az alsó szomszéddal*/
q->s.size += p->s.size;
q->s.ptr = p->s.ptr;
}
else
q->s.ptr = p;
allocp = q;
}

Bár a tárterületfoglalás lényegénél fogva gépfüggQ, a bemutatott program szemlélteti, hogyan tarthatjuk kézben és korlátozhatjuk a program egészen kis részére a gépfüggQ vonatkozásokat. A typedef és az union segítségével gondoskodhatunk az összeillesztésrQl (feltéve, hogy az sbrk a megfelelQ mutatót szolgáltatja). A típusmódosító szerkezetek használata explicitté teszi a mutatókonverziókat, és még rosszul tervezett rendszercsatlakozással is megbírkózik. Noha az itt közölt részletek a tárterületfoglalásra vonatkoznak, az elv, a megközelítés más esetekben is alkalmazható.

8.6. Gyakorlat. A calloc(n, size) szabványos könyvtári függvény n darab size nagyságú objektumot megcímzQ mutatót ad vissza, a tárterület kezdeti nagysága nulla. îrjuk meg a calloc függvényt úgy, hogy az alloc-ot mintaként vagy hívott függvényként használjuk!

8.7. Gyakorlat. Az alloc a méretre vonatkozó kérést anélkül fogadja el, hogy annak jogosságát ellenQrizné. A free azt hiszi, hogy az a blokk, amelynek felszabadítását tQle kérik, érvényes méretq mezQt tartalmaz. Javítsuk e programok minQségét azzal, hogy nagyobb gondot fordítunk a hibaellenQrzésre!

8.8. Gyakorlat. îrjuk meg a bfree(p, n) rutint, amely az alloc és a free által kezelt szabad lista számára felszabadítja az n karakterbQl álló tetszQleges p blokkot! bfree használatával a felhasználó bármikor beiktathat a szabad listába egy statikus vagy külsQ tömböt.
Itt


Hasonló témájú dokumentumok
A mások által feltöltött dokumentumokat értékelheted. Ha úgy ítéled meg, hogy a vizsgára való felkészülés szempontjából hasznos volt egy dokumentum, akkor adj rá sokcsillagos értékelést.
Ha hibákat tartalmaz, vagy egyéb probléma van vele, akkor keveset.
A dokumentumok sorrendje az értékelések alapján adódik. Ami fentebb van a listában, azt hasznosabbnak ítélték társaid. Az új dokumentumok pedig (értékelések hiányában) szintén a lista tetején kezdenek.

Hozzászólások

Ha észrevételed van egy dokumentummal kapcsolatban (például hibát találtál benne), akkor a Hozzászólások részben jelezheted. Az olyan jellegű kérdéseket mint pl.: A 2. feladat 4. sorából milyen átalakítással jutottunk az 5. sorban szereplő képlethez? - szintén ide érdemes írni
Egy tipp az oldalhoz! - Sikeres vizsga után írd meg tapasztalataid a tantárggyal, vizsgával kapcsolatban. Miből érdemes tanulni, mennyi készülés kell, milyen volt a vizsga... Ha mindenki így tesz, sokkal egyszerűbb lesz elkezdeni a tanulást egy olyan ember tapasztalatainak a birtokában, aki már elvégezte a tantárgyat. Ehhez kattints a tantárgyra a Tanulmányaimban, majd a Véleményem a tárgyról linkre a jobb felső részen.

Cimkefelhő

1. félév 2008 tavasz 3. óra adatbázis adatbiztonság analízis dolgozat dm életmód előadásanyag, mechatronika emission trade épterv eredménykimutatás európai integráció evolúció fogalomtár fogyasztóvédelem gyak humánbiosz gyak innováció ismertető katalízis keringés kodolányi - levelező konjunktúrakutatás korreláció könyv 2 környezettechnikai műveletek környezetvédelem kritgyak litoszféra marketing tétel matek 1 mit tudtak a régiek művelődéstörténet nevelés political science pricing strategies reklámelmélet és gyakorlat szintay szociológia tétel szöveg technika témák természetvédelmi mérnök újság választások valós érték villanytan vizsgára word