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...

Bauer Péter - Varjasi Norbert - A C programnyelv

Országok listájaHungaryDebreceni EgyetemInformatikai KarProgramtervező informatikusBevezetés Az InformatikábaEgyéb SegítségBauer Péter - Varjasi Norbert - A C programnyelv

2009.01.06 15:13:54
(10)
Szerző: Simintoo
Cimkék: bauer péter, varjasi norbert, c nyelv, programozás


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.

C programnyelv

1

Kedves Kollegina, Kolléga! A jegyzetet Önnek készítettem azért, hogy referencia anyaga legyen a Programnyelv és a Programfejlesztés tárgyakhoz. Szeretném a segítségét igénybe venni abból a célból, hogy a jegyzet minél pontosabb, megbízhatóbb legyen. Épp ezért arra kérem, ha az olvasás során valamilyen magyartalanságba, nem elégséges magyarázatba vagy uram bocsá' hibába ütközne, jelezze vissza nekem! Ténykedését elre megköszönöm. Gyr, 2003. február (B609) Tel.: (96) 503400/3254 Bauer Péter Varjasi Norbert e-mail: bauer@sze.hu varjasin@sze.hu

2

BEVEZETÉS ÉS ALAPISMERETEK

1 BEVEZETÉS
A Széchenyi István Egyetem különféle informatika szakjai és szakirányai C programnyelvi jegyzetigényét hivatott kielégíteni ez a jegyzet. Az olvasóról feltételezi, hogy tisztában van a számítástechnikai alapfogalmakkal [1]. Alapos strukturált programozási ismereteket szerzett, és járatos az alapvet algoritmikus elemekben [2]. Jól ismeri a PASCAL programnyelvet és fejleszt környezetet [3]. Magyarán ismer, és kezel ilyen fogalmakat, mint: · Adatok és adattípusok. · Konstansok, változók és azonosítók. · Vezérlési szerkezetek: szekvencia, szelekció és iteráció. · Tömbök és sztringek (karakterláncok). · Programszerkezeti elemek: eljárások, függvények és programmodulok. · Láncolt adatszerkezetek: listák és fák. · Fájlok stb. A C nyelvet tervezje, Dennis Ritchie, a Bell Laboratóriumban fejlesztette ki az 1970­es évek végén [4], és a UNIX operációs K&R rendszer programnyelvének szánta. Ezt a változatot jelöli az ábrán a K&R. A C nyelv ezt követen praktikussága miatt széles körben elterjedt. Sokan ANSI C készítettek sokféle C fordítót saját, vagy környezetük igényeinek megfelelen. A sokszínségben amerikai nemzeti szabfordító vánnyal (ANSI) teremtettek rendet az 1980­as évek végén [5]. Az ANSI C szabványt aztán Európában (ISO) is elfogadták néhány évvel késbb. Az ábrából látszik, hogy az ANSI C bvítette a K&R C halmazt. További történeti áttekintéshez a [4] és az [5] bevezet részeit ajánljuk!

C programnyelv

3

Az ábrán a legbvebb C halmaz a fordító. Ha valamikor is valamilyen gondunk lenne azzal, hogy egy konkrét C utasítás, módosító stb. megfelel­e az ANSI C szabványnak, akkor fordítás eltt kapcsoljuk be az integrált programfejleszt rendszer egy menüpontjával az ANSI C kompatibilis fordítást! A C általános célú programnyelv, mely tömörségérl, hatékonyságáról, gazdaságosságáról és portabilitásáról (hordozhatóságáról) ismert. Nem tartalmaz túl sok vezérlési szerkezetet. Bséges viszont az operátorkészlete, és több adattípus megléte jellemzi. Jól használható tehát mszaki­ tudományos, vagy akár adatfeldolgozási problémák megoldására. A C elég alacsony szint ­ hardver közeli ­ programnyelv is egyben, hisz tervezje a UNIX operációs rendszert e nyelv segítségével készítette el néhány száz gépi kódú utasítás felhasználásával. A C programok gyakran ugyanolyan gyorsak, mint az ASSEMBLER nyelven készültek, de jóval könnyebben olvashatók és tarthatók karban. Jegyzetünkben nem kívánunk elmerülni konkrét integrált programfejleszt rendszerek, operációs rendszerek és processzorok részleteinek taglalásában. Teljes általánosságban azonban még sem célszer a dolgokról beszélni, mert akkor ilyeneket kéne mondani, mint: · Képezzük az operációs rendszernek megfelel végrehajtható fájlt! · Futtassuk a végrehajtható fájlt az operációs rendszerben! Ehelyett rögzítsük azt, hogy fogalmainkkal az IBM PC kompatibilis személyi számítógépek területén maradunk! Erre a gépcsaládra is rengeteg cég készített C fordítót (compiler). Itt állapodjunk meg két f gyártónál: a Borlandnál és a Microsoftnál! Az integrált programfejleszt keretrendszer legyen menüvel irányítható, s ne parancssori paraméterként megadott kapcsolókkal kelljen vezérelni a fordítót és a kapcsoló­ szerkesztt (linker). Az operációs rendszer számunkra jobbára csak olyan szempontból érdekes, hogy legyen karakteres szabványos bemenete (standard input), és létezzen karakteres szabvány kimenete (standard output), valamint szabványos hibakimenete (standard error output). A szabvány bemenet alapértelmezés szerint a billentyzet, a kimenetek viszont a karakteres üzemmódú képernyre, vagy a karakteres konzol ablakba dolgoznak. A karakteres képerny, vagy konzol ablak felbontása természetesen változtatható, de mi minden példánál feltételezzük a 25 sorszor 80 oszlopot! A szabvány kimeneteken a mindenkori aktuális pozíciót kurzor jelzi.

4

BEVEZETÉS ÉS ALAPISMERETEK

2 JELÖLÉSEK
Figyelem felkeltés. Valamely következtetés levonása az eddigiekbl. Esetleg: merre találhatók további részletek a kérdéses témával kapcsolatban. Lexikális ismeretek taglalása. Valamely folyamat pontosabb részletei. Egy fogalom precízebb definíciója. Valamilyen aránylag könnyedén elkövethet, de nehezen lokalizálható hiba. Egy alapvet, úgy nevezett ,,ököl" szabály.
Forrásprogramok és képerny tartalmak szövege.

Valamilyen konkrétummal helyettesítend szintaktikai egység. Kulcsszó vagy valamilyen azonosító. A fogalom els elfordulásának jelölésére szolgál. A megoldandó feladatokat így jelöltük.

C programnyelv

5

3 ALAPISMERETEK
3.1 Forrásprogram Els közelítésben induljunk ki abból, hogy a C program (a forrásprogram) fájlazonosítójában C kiterjesztéssel rendelkez, ASCII kódú szövegfájl, mely elállítható, ill. módosítható · akármilyen ASCII kódú szövegszerkesztvel, vagy · a programfejleszt rendszer beépített szövegszerkesztjével. Az ASCII kódú szövegfájl sorokból áll. Egy sorban a szöveg sorbeli karaktereinek ASCII kódjai következnek rendre az egymás utáni bájtokban. A sorhoz végül még két, a soremelést leíró bájt tartozik, melyekben egy soremelés (Line Feed ­ 10) és egy kocsi vissza (Carriage Return ­ 13) vezérl karakter van. Vigyázni kell ASCII kódú szövegfájlban a decimálisan 31 érték bájt használatával, mert ez fájlvég jelzés a legtöbb operációs rendszerben! Készítsük el els C programunkat, és mentsük el PELDA1.C néven!
/* PELDA1.C */ #include void main(void){ printf("Ez egy C program!\n"); }

Fordítsuk le a programot, szerkesszük meg a végrehajtható fájlt, és futtassuk le! A mindenki által gyanított végeredmény az a képernyn, hogy megjelenik a szöveg, és a következ sor elején villog a kurzor:
Ez egy C program! _

3.2 Fordítás A fordító sikeres esetben a forrásprogramból egy vele azonos nev (OBJ kiterjesztés) tárgymodult állít el, PELDA1.C fordító
1. ábra: Fordítás

PELDA1.OBJ

és üzeneteket jelentet meg többek közt a hibákról. A hibaüzenetek legalább kétszintek: · (fatális) hibaüzenetek és

6 · figyelmeztet üzenetek.

BEVEZETÉS ÉS ALAPISMERETEK

A (fatális) hibákat, melyek jobbára szintaktikai jellegek, mindig kijavítja a programozó, mert korrekciójuk nélkül nem készíti el a tárgymodult a fordító. A figyelmeztet üzenetekkel azonban, melyek sok esetben a legsúlyosabb problémákat jelzik, nem szokott tördni, mert a fordító létrehozza a tárgymodult, ha csak figyelmeztet üzenetekkel zárul a fordítás. A PELDA1.C programunk els sora megjegyzés (comment). A megjegyzés írásszabálya látszik a sorból, azaz: · /* karakter párral kezddik és · */ karakter párral végzdik. A megjegyzés több forrássoron át is tarthat, minden sorba is írható egy, st akár egy soron belül több is megadható a szintaktikai egységek között. Egyetlen tilos dolog van: a megjegyzések nem ágyazhatók egymásba!
/* Ez a befoglaló megjegyzés eleje. /* Itt a beágyazott megjegyzés. */ Ez meg a befoglaló megjegyzés vége. */

Vegyük észre, hogy bármely hibás program rögtön hibátlanná válik, ha /*­ot teszünk az elejére, és a záró */­t elfelejtjük megadni a további forrásszövegben! A fordítót egybeépítették egy speciális elfeldolgozóval (preprocessor), mely az ,,igazi" fordítás eltt · elhagyja a forrásszövegbl a megjegyzéseket, · végrehajtja a neki szóló direktívákat, és · ezeket is elhagyja a forrásszövegbl. elfeldolgozó PELDA1.C fordító
2. ábra: Fordítás pontosabban

PELDA1.OBJ

Az elfeldolgozó direktívák egy sorban helyezkednek el, és #­tel kezddnek. Pontosabban # kell, legyen a sor els nem fehér karaktere. Fehér karakterek a szóköz, a soremelés, a lapdobás karakter, a vízszintes és a függleges tabulátor karakter. Meg kell említeni, hogy a meg-

C programnyelv

7

jegyzés is fehér karakternek minsül. A fehér karakterek szolgálhatnak szintaktikai egységek elválasztására, de a felesleges fehér karaktereket elveti a fordító. A PELDA1.C programunk második sora egy #include direktíva, melynek hatására az elfeldolgozó megkeresi és betölti a paraméter szövegfájlt a forrásszövegbe, és elhagyja belle ezt a direktívát. A pillanatnyilag betöltend szövegfájl, az stdio.h, H kiterjesztés. Az ilyen kiterjesztés szövegfájlokat C környezetben fejfájloknak (header) nevezik. A fejfájl egy témával kapcsolatos adattípusok, konstansok definícióit és a vonatkozó függvények jellemzit tartalmazza. Fedezzük fel, hogy az stdio a standard input output rövidítésébl származik, s így lényegében a szabvány kimenetet és bemenetet ,,kapcsoltuk" programunkhoz. Nem volt még szó arról, hogy a paraméter fájlazonosító <> jelek között áll! A <> jel pár tájékoztatja az elfeldolgozót, hogy milyen könyvtárakban keresse a szövegfájlt. A programfejleszt rendszer menüpontjai közt van egy, melynek segítségével megadhatók a fejfájlok (include fájlok) keresési útjai. alakú paraméter hatására az #include direktíva csak a programfejleszt rendszerben elírt utakon keresi a fájlt, és sehol másutt. PELDA1.C programunk harmadik és negyedik sora a main (f) függvény definíciója. A függvénydefiníció szintaktikai alakja:
típus függvénynév(formális­paraméterlista) { függvény­test }

· A visszatérési érték típusa void, mely kulcsszó éppen azt jelzi, hogy a függvénynek nincs visszaadott értéke. · A függvénynév main. C környezetben a main az indító program. · A formális­paraméterlista mindig ()­ben van. Pillanatnyilag itt is a void kulcsszó látható, ami e helyt azt rögzíti, hogy nincs formális paraméter. · A függvény­test­et mindig {}­ben, úgy nevezett blokk zárójelekben, kell elhelyezni. PASCAL programozónak csak annyi említend, hogy a { a BEGIN, és a } az END. A forrásprogram több forrásmodulban (forrásfájlban) is elhelyezhet. Végrehajtható program létrehozásához azonban valamelyik forrásmodulnak tartalmaznia kell a main­t. Az indító program a végrehajtható

8

BEVEZETÉS ÉS ALAPISMERETEK

program belépési pontja is egyben. Ez az a hely, ahova a memóriába történt betöltés után átadja a vezérlést az operációs rendszer. Az indító programnak természetesen a végrehajtható program ,,igazi" indítása eltt el kell látnia néhány más feladatot is, s a programozó által írt main­beli függvénytest csak ezután következik. A gyártók az indító programot rendszerint tárgymodul (OBJ) alakjában szokták rendelkezésre bocsátani. A PELDA1.C programban a main függvény teste egyetlen függvényhívás. A függvényhívás szintaktikája:
függvénynév(aktuális­paraméterlista)

· A függvénynév a printf, mely függvény az aktuális paraméterét megjelenteti a szabvány kimeneten. · A ()­ben álló aktuális­paraméterlista egytagú, és momentán egy karakterlánc konstans. A karakterlánc konstans írásszabálya látszik: idézjelek közé zárt karaktersorozat. A karakterlánc konstanst a memóriában meg képzeljük úgy el, hogy a fordító a szöveg karaktereinek ASCII kódjait rendre elhelyezi egymást követ bájtokban, majd végül egy tiszta zérustartalmú (minden bitje zérus) bájttal jelzi a karakterlánc végét! A példabeli karakterlánc konstans végén azonban van egy kis furcsaság: a \n! Ha valaki áttanulmányozza az ASCII kódtáblát, akkor láthatja, hogy a lehetséges 256 kódpozíció nem mindegyikéhez tartozik karakterkép. Említsük csak meg a szóköz (32) alatti kódpozíciókat, ahol az úgy nevezett vezérl karakterek is elhelyezkednek! Valahogyan azt is biztosítania kell a programnyelvnek, hogy ezek a karakterek, ill. a karakterkép nélküli kódpozíciók is megadhatók legyenek. A C programnyelvben erre a célra az úgynevezett escape szekvencia (escape jelsorozat) szolgál. Remélhetleg világossá vált az elz okfejtésbl, hogy az escape szekvencia helyfoglalása a memóriában egyetlen bájt! Az escape szekvencia \ jellel kezddik, s ezután egy karakter következik. A teljesség igénye nélkül felsorolunk itt néhányat!

C programnyelv Escape szekvencia Jelentés \b \n \r \t \" \\ \0 \ooo visszatörlés (back space) soremelés vagy új sor (line feed) kocsi vissza (carriage return) vízszintes tabulátor (horizontal tab) egyetlen " karakter egyetlen \ karakter karakterlánc záró bájt, melynek minden bitje zérus az o­k oktális számok

9

Vegyük észre, hogy ha idézjelet kívánunk a karakterlánc konstansba írni, akkor azt csak \" módon tehetjük meg! Ugyanez a helyzet az escape szekvencia kezdkarakterével, mert az meg csak megkettzve képez egy karaktert! Lássuk még be, hogy a \ooo alakkal az ASCII kódtábla bármely karaktere leírható! Például a \012 azonos a \n­nel, vagy a \060 a 0 számjegy karakter. A printf függvényhívás után álló ; utasításvég jelzés. PELDA1.C programunk ,,utolsó fehér foltja" a printf, mely egyike a szabványos bemenet és kimenet függvényeinek. 3.3 Kapcsoló­szerkesztés (link) A gyártók a szabvány bemenet, kimenet és más témacsoportok függvényeinek tárgykódját statikus könyvtárakban (LIB kiterjesztés fájlokban) helyezik el. Nevezik ezeket a könyvtárakat futásidej könyvtáraknak (run time libraries), vagy szabvány könyvtáraknak (standard libraries) is. A könyvtárfájlokból a szükséges tárgykódot a kapcsoló­szerkeszt másolja hozzá a végrehajtható fájlhoz. Lássuk ábrán is a szerkesztést! PELDA1.OBJ indító program (OBJ) könyvtárak (LIB)
3. ábra: Kapcsoló­szerkesztés

kapcsoló­ szerkeszt

PELDA1.EXE

10

BEVEZETÉS ÉS ALAPISMERETEK

A programfejleszt keretrendszerben a sikeres mködéshez bizonyosan be kell állítani a statikus könyvtárfájlok (library) keresési útjait. Azt is meg kell adni természetesen, hogy hova kerüljenek a fordítás és a kapcsoló­szerkesztés során keletkez kimeneti fájlok. 3.4 Futtatás A végrehajtható fájl a parancssorból azonosítójának begépelésével indítható. Valószínleg a programfejleszt rendszer menüjében is van egy pont, mellyel az aktuális végrehajtható fájl futtatható. Többnyire létezik a három lépést (fordítás, kapcsoló­szerkesztés és futtatás) egymás után megvalósító egyetlen menüpont is. Ha programunk kimenete nem látható a képernyn, akkor egy másik menüpont segítségével át kell váltani a felhasználói képernyre (user screen), vagy aktuálissá kell tenni a futtatott végrehajtható fájl programablakát. 3.5 Táblázat készítése Készítsük el a következ forint­euró átszámítási táblázatot! Egy euró pillanatnyilag legyen 244 forint 50 fillér!
Forint 100 200 300 . . . 1000 Euró 0.41 0.82 1.23 . . . 4.09

Adatstruktúra: · A változók a valós típusú euro­n kívül, mind egészek. also lesz a tartomány alsó határa, felso a tartomány fels értéke, lepes a lépés-

köz, és ft a ciklusváltozó. Algoritmus: · Deklaráljuk a változókat! · Ellátjuk ket ­ az euro­tól eltekintve ­ kezdértékkel. · Megjelentetjük a táblázat fejléc sorát és az aláhúzást. · Mködtetjük a ciklust, míg ft <= felso. · A ciklusmagban kiszámítjuk az aktuális euro értéket, megjelentetjük az összetartozó ft ­ euro értékpárt, s végül léptetjük az ft ciklusváltozót. Készítsük el a programot!
/* PELDA2.C: Forint-euró átszámítási táblázat */ #include

C programnyelv
void main(void){ int also, felso, lepes, ft; /* Deklarációk */ float euro; also = 100; /* Végrehajtható utasítások */ felso = 1000; lepes = 100; ft = also; printf( " Forint| Euró\n" "---------+---------\n"); while(ft <= felso){ /* Beágyazott (bels) blokk */ euro = ft / 244.5; printf("%9d|%9.2f\n", ft, euro); ft = ft + lepes; } }

11

A PELDA2.C­bl kitnen látszik a C függvény szerkezete, azaz az úgy nevezett blokkszerkezet: · Elbb a deklarációs utasítások jönnek a blokkbeli változókra. A C szigorú szintaktikájú nyelv: ·elzetes deklaráció nélkül nem használhatók benne a változók, és ·kivétel nélkül deklarálni kell minden használatos változót! · Aztán a végrehajtható utasítások következnek. A blokkszerkezet deklarációs és végrehajtható részre bontása a C­ ben szigorú szintaktikai szabály, azaz egyetlen végrehajtható utasítás sem keveredhet a deklarációs utasítások közé, ill. a végrehajtható utasítások között nem helyezkedhet el deklarációs utasítás. Természetesen a végrehajtható utasítások közé beágyazott (bels) blokkban a szabály újra kezddik. Vegyük észre a példaprogramunk végén elhelyezked beágyazott vagy bels blokkot! Figyeljük meg a main két els sorában, hogy a deklarációs utasítás szintaktikája:
típus azonosítólista;

Az azonosítólista azonosítók sorozata egymástól vesszvel elválasztva. Megfigyelhet még, hogy az azonosítók képzéséhez az angol ábécé beti használhatók fel! Foglalkozzunk kicsit a típusokkal! Az int (integer) 16, vagy 32 bites, alapértelmezés szerint eljeles (signed), fixpontos belsábrázolású egész típus. Vegyük, mondjuk, a 16 bites esetet! A legnagyobb, még ábrázolható pozitív egész binárisan és decimálisan:

12

BEVEZETÉS ÉS ALAPISMERETEK

0111 1111 1111 11112 = 215 ­ 1 = 32767 A negatív értékek kettes komplemens alakjában tároltak. A legkisebb, még ábrázolható érték így: 1000 0000 0000 00002 = ­215 = ­32768 Eljeltelen (unsigned) esetben nincsenek negatív értékek. A számábrázolási határok zérus és 1111 1111 1111 11112 = 216 ­ 1 = 65535 közöttiek. Az elzk 32 bites esetre ugyanilyen könnyedén levezethetek! A float (floating point) 4 bájtos, lebegpontos belsábrázolású valós típus, ahol a mantissza és eljele 3 bájtot, s a karakterisztika eljelével egy bájtot foglal el. Az ábrázolási határok: ±3.4*10-38 ­ ±3.4*10+38. Ez a mantissza méret 6 ­ 7 decimális jegy pontosságot tesz lehetvé. Néhány programfejleszt rendszer a lebegpontos könyvtárakat (LIB) csak akkor kapcsolja be a kapcsoló­szerkeszt által keresésnek alávethet könyvtárak közé, ha a programban egyáltalán igény jelentkezik valamilyen lebegpontos ábrázolás, vagy mvelet elvégeztetésére. A PELDA2.C végrehajtható részének els négy utasítása értékadás. Ki kell azonban hangsúlyozni, hogy a C­ben nincs értékadó utasítás, csak hozzárendelés operátor, s a hozzárendelésekbl azért lesz utasítás, mert ;­t írtunk utánuk. A hozzárendelésre rögtön visszatérünk! Vegyük észre elbb az egész konstans írásszabályát! Elhagyható eljellel kezddik, s ilyenkor pozitív, és ezután az egész szám jegyei következnek. A fejléc sort és az aláhúzást egyetlen printf függvényhívással valósítottuk meg. Látszik, hogy a táblázat oszlopait 9 karakter szélességre választottuk. Figyeljük meg, hogy a pontos pozícionálást segítend a fejléc sort és az aláhúzást a printf­ben két egymás alá írt karakterlánc konstansként adtuk meg! A C fordító a csak fehér karakterekkel elválasztott karakterlánc konstansokat egyesíti egyetlen karakterlánc konstanssá, s így a példabeli printf­nek végül is egyetlen paramétere van. A PELDA2.C­ben az elöltesztel ciklusutasítás következik, melynek szintaktikája:

C programnyelv
while(kifejezés) utasítás

13

A kifejezés aritmetikai, azaz számérték. Az elöltesztel ciklusutasítás hatására lépésenként a következ történik: 1. Kiértékeli a fordító a kifejezést. Ha hamis (zérus), akkor vége a ciklusnak, s a while-t követ utasítás jön a programban. 2. Ha a kifejezés igaz (nem zérus), akkor az utasítás végrehajtása, és aztán újból az 1. pont következik. Világos, hogy a kifejezés értékének ,,valahogyan" változnia kell az utasításban, különben a ciklusnak soha sincs vége. Az utasítás állhat több utasításból is, csak {}­be kell tenni ket. A {}­ben álló több utasítást összetett utasításnak nevezik. Az összetett utasítás szintaktikailag egyetlen utasításnak minsül. A PELDA2.C­ben a while kifejezése reláció. A relációjelek a szokásosak: kisebb (<), kisebb egyenl (<=), nagyobb (>) és nagyobb egyenl (>=). A reláció két lehetséges értéke: az igaz és a hamis logikai érték. A C­ben azonban nincsen logikai adattípus, így az igaz az 1 egész érték, és a zérus a hamis. A példabeli bels blokk els és utolsó utasítása hozzárendelés, melynek szintaktikai alakja:
objektum = kifejezés

A hozzárendelés operátor (mveleti jel) bal oldalán valami olyan objektumnak kell állnia, ami értéket képes felvenni. A szaknyelv ezt módosítható balértéknek nevezi. Példánkban az összes hozzárendelés bal oldalán egy változó azonosítója áll. Az = jobb oldalán meghatározható érték kifejezésnek (jobbértéknek) kell helyet foglalnia. A hozzárendelés hatására a kifejezés értéke - esetlegesen az objektum típusára történt konverzió után - felülírja az objektum értékét. Az egész ,,konstrukció" értéke az objektum új értéke, és típusa az objektum típusa. A legutóbbi mondat azt célozza, hogy ha a hozzárendelés kifejezés része, akkor ez az érték és típus vesz részt a kifejezés további kiértékelésében. A beágyazott blokkbeli két hozzárendelés kifejezése aritmetikai. Az aritmetikai mveleti jelek a szokásosak: összeadás (+), kivonás (­), szorzás (*) és az osztás (/). Vegyük észre, hogy az eddigi printf függvényhívásainknak egyetlen karakterlánc konstans paramétere volt, mely változatlan tartalommal jelent meg a karakteres képernyn! A bels blokkbeli printf­ben viszont

14

BEVEZETÉS ÉS ALAPISMERETEK

három aktuális paraméter van: egy karakterlánc konstans, egy int és egy float. A gond ugye az, hogy az int és a float paraméter értékét megjelentetés eltt karakterlánccá kéne konvertálni, hisz bináris bájtok képernyre vitelének semmiféle értelme nincs! A printf els karakterlánc paramétere · karakterekbl és · formátumspecifikációkból áll. A karakterek változatlanul jelennek meg a képernyn, a formátumspecifikációk viszont meghatározzák, hogy a printf további paramétereinek értékeit milyen módon kell karakterlánccá alakítani, s aztán ezt hogyan kell megjelentetni. A formátumspecifikáció % jellel indul és típuskarakterrel zárul. A formátumspecifikációk és a printf további paraméterei balról jobbra haladva rendre összetartoznak. St ugyanannyi formátumspecifikáció lehet csak, mint ahány további paraméter van. Felsorolunk néhány típuskaraktert a következ táblázatban: Típuskarakter d f c s A hozzátartozó paraméter típusa egész típusú lebegpontos egy karakter karakterlánc decimális egészként tizedes tört alakjában karakterként karakterláncként Megjelenítés

A formátumspecifikáció pontosabb alakja:
%<.pontosság>típuskarakter

A <>­be tétel az elhagyhatóságot hivatott jelezni. A szélesség annak a meznek a karakteres szélességét rögzíti, amiben a karakterlánccá konvertált értéket ­ alapértelmezés szerint jobbra igazítva, és balról szóközfeltöltéssel ­ kell megjelentetni. Ha a szélességet elhagyjuk a formátumspecifikációból, akkor az adat a szükséges szélességben jelenik meg. Maradjunk annyiban pillanatnyilag, hogy a pontosság lebegpontos esetben a tizedes jegyek számát határozza meg! Lássuk be, hogy a "%9d|%9.2f\n" karakterlánc konstansból a %9d és a %9.2f formátumspecifikációk, míg a | és a \n sima karakte-

C programnyelv

15

rek! Vegyük észre, hogy a ,,nagy" pozícionálgatás helyett táblázatunk fejléc sorának és aláhúzásának megjelentetését így is írhattuk volna:
printf("%9s|%9s\n---------+---------\n", "Forint", "Euró");

Foglalkoznunk kell még egy kicsit a mveletekkel! Vannak · egyoperandusos (operátor operandus) és · kétoperandusos (operandus operátor operandus) mveletek. Az egyoperandusos operátorokkal kevés probléma van. Az eredmény típusa többnyire egyezik az operandus típusával, és az értéke az operandus értékén végrehajtva az operátort. Például: ­változó. Az eredmény típusa a változó típusa, és az eredmény értéke a változó értékének ­1­szerese. Problémák a kétoperandusos mveleteknél jelentkezhetnek, és hanyagoljuk el a továbbiakban az eredmény értékét! Ha kétoperandusos mveletnél a két operandus típusa azonos, akkor az eredmény típusa is a közös típus lesz. Ha a két operandus típusa eltér, akkor a fordító a rövidebb, pontatlanabb operandus értékét a hosszabb, pontosabb operandus típusára konvertálja, és csak ezután végzi el a mveletet. Az eredmény típusa természetesen a hosszabb, pontosabb típus. A ft/244.5 osztásban a ft egész típusú és a 244.5 konstans lebegpontos. A mvelet elvégzése eltt a ft értékét lebegpontossá alakítja a fordító, és csak ezután hajtja végre az osztást. Az eredmény tehát ugyancsak lebegpontos lesz. Ezt implicit típuskonverziónak nevezik. Vegyük észre közben a valós konstans írásszabályát is! Elhagyható eljellel kezddik, amikor is pozitív, és aztán az egész rész jegyei jönnek. Aztán egy tizedespont után a tört rész számjegyei következnek. A probléma a PASCAL programozó számára egészek osztásánál jelentkezik, hisz egészek osztásának eredménye is egész, és nincs semmiféle maradékmegrzés, lebegpontos átalakítás! Tételezzük fel, hogy 50 fillérrel csökkent az euró árfolyama! Alakítsuk csak át az euro=ft/244.5 hozzárendelést euro=ft/244­re, s rögtön láthatjuk, hogy az eredményekben sehol sincs tört rész! Felvetdik a kérdés, hogyan lehetne ilyenkor a helyes értéket meghatározni? A válasz: explicit típusmódosítás segítségével, melynek szintaktikai alakja:
(típus)kifejezés

16

BEVEZETÉS ÉS ALAPISMERETEK

Hatására a kifejezés értékét típus típusúvá alakítja a fordító. A konkrét esetben az osztás legalább egyik operandusát float­tá kéne módosítani, és ugye akkor a kétoperandusos mveletekre megismert szabály szerint a másik operandus értékét is azzá alakítaná a fordító a mvelet tényleges elvégzése eltt, azaz:
euro = (float)ft / 244;

Megoldandó feladatok: Készítsen programot, mely a képerny 21 sorszor 21 oszlopos területén a csillag karakter felhasználásával megjelentet: · Egy keresztet a 11. sor és 11. oszlop feltöltésével! · A fátlót (bal fels sarokból a jobb alsóba ment)! · A mellékátlót (a másik átlót)! · Egyszerre mindkét átlót, azaz egy X-et! A forint­euró átszámítási táblázatot elkészít PELDA2.C megoldásunkkal az a ,,baj", hogy túl sok változót használunk. Könnyen beláthatjuk, hogy az ft­tl eltekintve a többi változó nem is az, hisz a program futása alatt nem változtatja meg egyik sem az értékét! Készítsünk PELDA3.C néven egy jobb megoldást!
/* PELDA3.C: Forint-euró átszámítási táblázat */ #include void main(void){ int ft; printf("%9s|%9s\n---------+---------\n", "Forint", "Euró"); for(ft=100; ft<=1000; ft=ft+100) printf("%9d|%9.2f\n", ft, ft/244.5); }

PELDA3.C programunkban két új dolog látható. Az egyik a
for(; ; ) utasítás

elöltesztel, iteratív ciklusutasítás, melynek végrehajtása a következ lépések szerint történik meg: 1. A fordító végrehajtja az init­kifejezést, ha van. Az elhagyhatóságot most is <> jelekkel szemléltetjük! 2. Kiértékeli a kifejezést. Ha hamis (zérus), akkor vége a ciklusnak, s a for-t követ utasítás jön a programban. Látható, hogy a szintaktika szerint ez a kifejezés is elhagyható. Ilyenkor 1­nek (igaznak) minsül.

C programnyelv

17

3. Ha a kifejezés igaz (nem zérus), akkor az utasítás végrehajtása jön. Az utasítás most is lehetne összetett utasítás is! 4. Végül az ugyancsak elhagyható léptet­kifejezés, majd újból a 2. pont következik. Ha a for utasítást while-lal szeretnénk felírni, akkor azt így tehetjük meg:
; while(kifejezés) { utasítás; ; }

A szintaktikai szabályt összefoglalva: a for-ból akár mindegyik kifejezés is elhagyható, de az els kettt záró pontosvesszk nem! A PELDA3.C programbeli másik új dolog az, hogy a printf aktuális paramétereként kifejezés is megadható. A PELDA3.C ugyan sokat rövidült, de ezzel a megoldással meg az a probléma, hogy tele van varázs­konstansokkal. Ha megváltoztatnánk átszámítási táblázatunkban a tartomány alsó, ill. fels határát, módosítanánk a lépésközt, vagy az euró árfolyamot, akkor ennek megfelelen át kellene írnunk varázs­konstansainkat is. Az ilyen átírogatás 8 soros programnál könnyen, és remélhetleg hibamentesen megvalósítható. Belátható azonban, hogy nagyméret, esetleg több forrásfájlból álló szoftver esetében, amikor is a varázs­konstansok rengeteg helyen elfordulhatnak, ez a módszer megbízhatatlan, vagy legalább is nagyon hibagyanús. A C a probléma megoldására a szimbolikus konstansok, vagy más megnevezéssel: egyszer makrók, használatát javasolja. A metódus a következ: 1. Definiálni kell a benne használatos szimbolikus állandókat egy helyen, egyszer, a forrásfájl elején. 2. Aztán a programban végig a konstansok helyett szisztematikusan a szimbolikus konstansokat kell használni. A változtatás is nagyon egyszervé válik így: · a megváltozott konstans értékét egy helyen át kell írni, s · a többi felhasználása automatikusan módosul a következ fordításnál. A szimbolikus állandó a
#define azonosító helyettesít­szöveg

18

BEVEZETÉS ÉS ALAPISMERETEK

elfeldolgozó direktívával definiálható. A szimbolikus állandó ­ tulajdonképpen az azonosító ­ a direktíva helyétl a forrásszöveg végéig van érvényben. Az elfeldolgozó kihagyja a direktívát a forrásszövegbl, majd végigmegy rajta, és az azonosító minden elfordulását helyettesít­ szövegre cseréli. Lássuk a ,,medvét"!
/* PELDA4.C: Forint-euró átszámítási táblázat */ #include #define ALSO 100 /* A tartomány alsó határa */ #define FELSO 1000 /* A tartomány fels értéke */ #define LEPES 100 /* A lépésköz */ #define ARFOLYAM 244.5 /* Ft/euró árfolyam */ void main(void){ int ft; printf( "%9s|%9s\n---------+---------\n", "Forint", "Euró"); for(ft=ALSO; ft<=FELSO; ft=ft+LEPES) printf("%9d|%9.2f\n", ft, ft/ARFOLYAM); }

Szokás még ­ különösen több forrásmodulos esetben ­ a #define direktívákat (és még más dolgokat) külön fejfájlban elhelyezni, s aztán ezt minden forrásfájl elején #include direktívával bekapcsolni. Készítsük csak el ezt a variációt is!
/* BEGEND.H: Fejfájl az átszámítási táblához */ #include #define ALSO 100 /* A tartomány alsó határa */ #define FELSO 1000 /* A tartomány fels értéke */ #define LEPES 100 /* A lépésköz */ #define ARFOLYAM 244.5 /* Ft/euró árfolyam */ #define begin { /* {} helyett begin-end! */ #define end } #define then /* if utasításban then! */ #define LACI for /* Kulcsszavak átdefiniálása nem javasolt! */ /* PELDA5.C: Forint-euró átszámítási táblázat */ #include "BEGEND.H" void main(void) begin int ft; printf("%9s|%9s\n---------+---------\n", "Forint", "Euró"); LACI(ft=ALSO; ft<=FELSO; ft=ft+LEPES) printf("%9d|%9.2f\n", ft, ft/ARFOLYAM); end

C programnyelv

19

Vegyük észre, hogy az #include direktíva fájlazonosítója nem <>­ k, hanem ""­k között áll! Ennek hatására az elfeldolgozó a megadott azonosítójú fájlt elször az aktuális mappában ­ abban a könyvtárban, ahol az a .C fájl is elhelyezkedik, melyben a #include direktíva volt ­ keresi. Ha itt nem találja, akkor továbbkeresi a programfejleszt rendszerben beállított utakon. 3.6 Bemenet, kimenet A kissé ,,lerágott csont" forint­euró átszámítási táblázatos példánkban nem volt bemenet. Megtanultuk már, hogy a szabvány bemenet és kimenet használatához be kell kapcsolni az STDIO.H fejfájlt:
#include

Alapértelmezés szerint szabvány bemenet (stdin) a billentyzet, és szabvány kimenet (stdout) a képerny. A legtöbb operációs rendszerben azonban mindkett átirányítható szövegfájlba is. Egy karaktert olvas be a szabvány bemenetrl az int getchar(void); függvény. Ezt aztán balról zérus feltöltéssel int­té típusmódosítja, és visszaadja a hívónak. Azt, ahogyan az elbb a getchar­t leírtuk, függvény prototípusnak nevezik. A függvény prototípus teljes formai információt szolgáltat a szubrutinról, azaz rögzíti: · a függvény visszatérési értékének típusát, · a függvény nevét, · paramétereinek számát, sorrendjét és típusát. A getchar fájl végén, vagy hiba esetén EOF­ot szolgáltat. Az EOF az STDIO.H fejfájlban definiált szimbolikus állandó:
#define EOF (­1)

Tekintsük csak meg az STDIO.H fejfájlban! Nézegetés közben vegyük azt is észre, hogy a fejfájl tele van függvény prototípusokkal. Már csak az a kérdés maradt, hogy mi a fájlvég a szabvány bemeneten, ha az a billentyzet? Egy operációs rendszertl függ billentykombináció: Ctrl+Z vagy Ctrl+D. A paraméter karaktert kiviszi a szabvány kimenet aktuális pozíciójára az

20 int putchar(int k);

BEVEZETÉS ÉS ALAPISMERETEK

és sikeres esetben vissza is adja ezt az értéket. A hibát épp az jelzi, ha a putchar szolgáltatta érték eltér k­tól. Készítsünk programot, ami a szabvány bemenetet átmásolja a szabvány kimenetre!
/* PELDA6.C: Bemenet másolása a kimenetre */ #include void main(void){ int k; printf("Bemenet másolása a kimenetre:\n" "Gépeljen Ctrl+Z-ig sorokat!\n\n"); k=getchar(); while(k!=EOF){ if(k!=putchar(k)) printf("Hiba a kimeneten!\n"); k=getchar(); } }

Fogalmazzuk meg minimális elvárásainkat egy programmal szemben! A szoftver indulásakor jelezze ki, hogy mit csinál! Ha valamilyen eredményt közöl, akkor azt lássa el tájékoztató szöveggel, mértékegységgel stb.! Ha valamit bekér, akkor tájékoztasson róla, hogy mit kell megadni, milyen egységben stb.! A bemenet ellenrzend! A hibás adat helyett ­ a hiba okát esetleg kijelezve ­ azonnal kérjen újat a program! A <, <=, >, >= relációjelekrl már szó volt! A C­ben != a nem egyenl operátor és == az egyenl mveleti jel. Az == és a != ráadásul a többi relációnál eggyel alacsonyabb prioritási szinten foglal helyet. Kifejezés kiértékelése közben elbb a magasabb prioritású mveletet végzi el a fordító, s csak aztán következik az alacsonyabb. Vigyázat! Az egyenl relációt az egymás után írt, két egyenlség jel jelzi. Az egyetlen egyenlség jel a hozzárendelés operátor! A kétirányú szelekció szintaktikai alakja:
if(kifejezés) utasítás1

Az elhagyhatóságot most is a <> jelzi. Ha a kifejezés igaz (nem zérus), akkor utasítás1 végrehajtása következik. Ha a kifejezés hamis (zérus) és

C programnyelv

21

van else rész, akkor az utasítás2 következik. Mindkét utasítás összetett utasítás is lehet. A PELDA6.C megoldásunk túlzottan PASCAL ,,íz". C­ben programunk utolsó 5 sorát így kéne megírni:
while((k=getchar())!=EOF) if(k!=putchar(k)) printf("Hiba a kimeneten!\n");

A while kifejezése egy nem egyenl reláció, melynek bal oldali operandusa egy külön zárójelben álló hozzárendelés. Elbb a hozzárendelés jobb oldalát kell kiértékelni. Lássuk csak sorban a kiértékelés lépéseit! 1. Meghívja a getchar függvényt a fordító. 2. A visszakapott értékkel felülírja k változó értékét. 3. A getchar­tól kapott értéket hasonlítja EOF­hoz. A kifejezésbl a hozzárendelés körüli külön zárójel nem hagyható el, mert a hozzárendelés alacsonyabb prioritású mvelet a relációnál. Ha mégis elhagynánk, akkor a kiértékelés során a fordító: 1. Meghívná elbb a getchar függvényt. 2. A visszatérési értéket hasonlítaná EOF­hoz. Tehát kapna egy logikai igaz (1), vagy hamis (0) értéket! 3. A k változó felvenné ezt az 1, vagy 0 értéket. Figyeljük meg a PELDA6.C futtatásakor, hogy a getchar a bemenetrl olvasott karaktereket az operációs rendszer billentyzet pufferébl kapja! Emlékezzünk csak vissza! A parancssorban a begépelt szöveget szerkeszthetjük mindaddig, míg Enter­t nem nyomunk. A billentyzet pufferben lev karakterek tehát csak akkor állnak a getchar rendelkezésére, ha a felhasználó leütötte az Enter billentyt. Készítsünk programot, mely fájlvégig leszámlálja, hogy hány · numerikus karakter, · fehér karakter, · más egyéb karakter és · összesen hány karakter érkezett a szabvány bemenetrl! Megoldásunkban az összes változó egész típusú. k tartalmazza a beolvasott karaktert. A num, a feher és az egyeb számlálók. Az algoritmus:

22

BEVEZETÉS ÉS ALAPISMERETEK

· Deklaráljuk a változókat, és az összes számlálót lássuk el zérus kezdértékkel! · Jelentessük meg a program címét, és tájékoztassunk a használatáról! · Mködtessük addig a ciklust, míg EOF nem érkezik a bemenetrl! · A ciklusmagban háromirányú szelekció segítségével el kell ágazni a három kategória felé, és ott meg kell növelni eggyel a megfelel számlálót! · A ciklus befejezdése után megjelentetendk a számlálók értékei megfelel tájékoztató szövegekkel, és az is, hogy összesen hány karakter érkezett a bemenetrl!
/* PELDA7.C: A bemenet karaktereinek leszámlálása kategóriánként */ #include void main(void){ short k, num, feher, egyeb; num=feher=egyeb=0; printf("Bemeneti karakterek leszámlálása\n" "kategóriánként EOF-ig, vagy Ctrl+Z-ig.\n"); while((k=getchar())!=EOF) if(k>='0'&&k<='9')++num; else if(k==' '||k=='\n'||k=='\t')++feher; else ++egyeb; printf("Karakter számok:\n" "----------------\n" "numerikus: %5hd\n" "fehér: %5hd\n" "egyéb: %5hd\n" "----------------\n" "össz: %10ld\n", num, feher, egyeb, (long)num+feher+egyeb); }

Pontosítani kell a deklarációs utasítás eddig megismert szintaktikáját!
azonosítólista;

Az elhagyható alaptípus alapértelmezés szerint int. Az ugyancsak elhagyható típusmódosítók az alaptípus valamilyen jellemzjét változtatják meg. int típus esetén: · Az egész alapértelmezés szerint eljeles (signed), és lehetne még eljeltelen (unsigned). A signed és az unsigned módosítók egymást kizáróak. · Két, egymást kizáró hosszmódosítóval az egész belsábrázolása

C programnyelv ·bizonyosan 16 bites (short), ill. ·biztos 32 bites (long). Végül is a különféle int típusok méretei így összegezhetk: short <= int <= long

23

Vegyük észre, hogy az ismertetett szabályok szerint a short, a short int és a signed short int azonos típusok. A short és a short int írásmódnál figyelembe vettük, hogy signed az alapértelmezés. A short felírásakor még arra is tekintettel voltunk, hogy a meg nem adott alaptípus alapértelmezése int. Ugyanezek mondhatók el a long, a long int és a signed long int vonatkozásában is. Ugyan a szintaktika azt mutatja, de a deklarációs utasításban a típusmódosítók és az alaptípus egyszerre nem hagyhatók el! Feltéve, hogy a, b és c balértékek, az
a=b=c=kifejezés

értelmezése megint abból fakad, hogy a hozzárendelés a C­ben operátor, azaz:
a=(b=(c=kifejezés))

A fordító jobbról balra halad, azaz kiértékeli a kifejezést, és visszafelé jövet beírja az eredményt a balértékekbe. A konstrukció hatására a fordító gyorsabb kódot is hoz létre. Ugyanis a
c=kifejezés; b=kifejezés; a=kifejezés;

írásmódnál háromszor kell kiértékelni ugyanazt a kifejezést. C­ben a többágú (N) szelekcióra az egyik kódolási lehetség:
if(kifejezés1)utasítás1 else if(kifejezés2)utasítás2 else if(kifejezés3)utasítás3 /* . . . */ else utasításN

Ha valamelyik if kifejezése igaz (nem zérus) a konstrukcióban, akkor a vele azonos sorszámú utasítás végrehajtása következik, majd a konstrukciót követ utasítás jön. Ha minden kifejezés hamis (zérus), akkor viszont utasításN hajtandó végre.

24

BEVEZETÉS ÉS ALAPISMERETEK

Fedezzük fel a karakter konstans írásszabályát: aposztrófok között karakter, vagy escape szekvencia. A karakter konstans belsábrázolása int, így az ASCII kód egész értéknek is minsül kifejezésekben. A PELDA7.C­bl látható, hogy a logikai és mveletet && jelöli, s a logikai vagy operátor a ||. A kétoperandusos logikai operátorok prioritása alacsonyabb a relációkénál, és az && magasabb prioritású, mint a ||. Ha a kétoperandusos logikai mvelet eredménye eldl a bal oldali operandus kiértékelésével, akkor a C bele sem kezd a másik operandus értékelésébe. Az és mvelet eredménye eldlt, ha a bal oldali operandus hamis. A vagy pedig akkor kész, ha az els operandus igaz. A C­ben van inkrementálás (++) és dekrementálás (­­) egész értékekre. Mindkét mvelet egyoperandusos, tehát nagyon magas prioritású. A ++ operandusa értékét eggyel növeli meg, s a ­­ pedig eggyel csökkenti, azaz:
++változó változó=változó+1 --változó változó=változó­1

A problémák ott kezddnek azonban, hogy mindkét mvelet létezik eltag (prefix) és utótag (postfix) operátorként is! Foglalkozzunk csak a ++ operátorral! A ++változó és a változó++ hatására a változó értéke eggyel mindenképp megnövekedik. Kifejezés részeként eltag operátor esetén azonban a változó új értéke vesz részt a további kiértékelésben, míg utótag mveletnél a változó eredeti értéke számít be. Feltéve, hogy a és b egész típusú változók, és b értéke 6:
a = ++b; a = b++; /* a=7 és b=7 */ /* a=7 és b=8 */

Figyeljünk fel rá, hogy a PELDA7.C utolsó printf utasításában hosszmódosítók állnak a d típuskarakterek eltt a formátumspecifikációkban! Látszik, hogy a h jelzi a printf­nek, hogy a formátumspecifikációhoz tartozó aktuális paraméter short típusú (2 bájtos), ill. l tudatja vele, hogy a hozzátartozó aktuális paraméter long (4 bájtos). A megfelel hosszmódosítók megadása a formátumspecifikációkban elengedhetetlen, hisz nem mindegy, hogy a függvény a verem következ hány bájtját tekinti a formátumspecifikációhoz tartozónak!

C programnyelv

25

Vegyük azt is észre, hogy a típusokhoz a mezszélességgel is felkészültünk: a maximális pozitív short érték bizonyosan elfér 5 pozíción, s long pedig 10­en! Látható még, hogy arra is vigyáztunk, hogy a három maximális short érték összege részeredményként se csonkuljon! Ezért az explicit long­gá módosítás a printf utolsó paraméterében:
(long)num+feher+egyeb

Megoldandó feladatok: Készítsen programokat a PELDA4.C alapján a következképpen: · A forint 1000-tl 100­ig csökkenjen 100­asával! · Az euró növekedjék 1­tl 10­ig egyesével! · A forint 100­tól 2000­ig növekedjen 100­asával! Az eredményt a képernyn fejléccél ellátva két oszlop párban oszlopfolytonosan haladva kell megjelentetni. A bal oldali oszlop pár 100­zal, a jobb oldali viszont 1100­zal kezddjék! · A feladat maradjon ugyanaz, mint az elbb, de a megjelentetés legyen sorfolytonos. A bal oldali oszlop pár kezddjék 100­zal, a jobb oldali viszont 200­zal, s mindegyik oszlop párban 200 legyen a lépésköz! · Maradva a sorfolytonos megjelentetésnél, kérjük be elbb a kijelzend oszlop párok számát ellenrzött inputtal! Az oszlop párok száma 1, 2, 3 vagy 4 lehet. A még kijelzend fels érték ennek megfelelen 1000, 2000, 3000 vagy 4000. Az eredmény a képernyn fejléccél ellátva az elírt számú oszlop párban jelenjen meg úgy, hogy 100 továbbra is a lépésköz! · A forint 100­tól 10000­ig növekedjen 100­asával! A lista nem futhat el a képernyrl, azaz fejléccél ellátva lapozhatóan kell megjelentetni! Ez azt jelenti, hogy elször kiíratjuk a lista egy képerny lapnyi darabját, majd várunk egy gombnyomásra. A gomb leütésekor produkáljuk a lista következ lapját, és újból várunk egy gombnyomásra, és így tovább. · Legyen ugyanaz a feladat, mint az elz pontban, de a lista a képernyn fejléccél ellátva nem csak elre, hanem elre­hátra lapozhatóan jelenjen meg! Készítsen programokat, melyek a szabvány bemenetet EOF­ig olvassák, és közben megállapítják, hogy:

26

BEVEZETÉS ÉS ALAPISMERETEK

· Hány sor volt a bemeneten? A bemenet karakterei közt a '\n'­eket kell leszámlálni. Az utolsó sor persze lehet, hogy nem '\n'­nel végzdik, hanem EOF­fal. · Hány szó volt a bemeneten? A szó nem fehér karakterekbl áll. A szavakat viszont egymástól fehér karakterek választják el. Az utolsó szó lehet, hogy nem fehér karakterrel zárul, hanem EOF­fal. 3.7 Tömbök Készítsünk programot, mely a szabvány bemenetet olvassa EOF-ig! Megállapítandó és kijelzend, hogy hány A, B, C stb. karakter érkezett! A kis­ és nagybetk között nem teszünk különbséget! A betkön kívüli többi karaktert tekintsük egy kategóriának, s ezek darabszámát is jelezzük ki! Megoldásunkban az elvalaszto karakteres változó, a k, a tobbi és a betu viszont egész típusú. A k tartalmazza a beolvasott karaktert, és ciklusváltozói funkciókat is ellát. A tobbi és a betu számlálók. A betu annyi elem tömb, mint ahány bet az angol ábécében van. A tobbi a betkön kívüli többi karakter számlálója. Az elvalaszto karakteres változóra azért van szükség, mert az eredmény csak két oszlop páros listaként közölhet egy képernyn. Az algoritmus: · Deklaráljuk a változókat, és a tömböt! A számlálók nullázandók! Az elvalaszto induljon szóköz kezdértékkel! · Jelentessük meg a program címét, és tájékoztassunk a használatáról! · Mködtessük addig a ciklust, míg EOF nem érkezik a bemenetrl! · A ciklusmagban háromirányú szelekcióval el kell ágazni három kategória felé: nagybet, kisbet és más karakter. Megnövelend egygyel természetesen a megfelel számláló! · A ciklus befejezdése után két oszlop páros táblázatban megjelentetendk a betszámlálók értékei, és végül egy külön sorban a ,,többi karakter" kategória számlálója!
/* PELDA8.C: Betszámlálás a bemeneten */ #include #define BETUK 26 /* Az angol ábécé betszáma */ void main(void){ char elvalaszto; /* Listelválasztó karakter. */ int k, /* Bemeneti kar. és ciklusváltozó. */ tobbi, /* Nem betk számlálója. */ betu[BETUK]; /* Betszámlálók. */ tobbi=0; /* Kezdérték adás. */ for(k=0; k
C programnyelv
elvalaszto=' '; printf( "Bemenet betinek leszámlálása\n" "EOF-ig, vagy Ctrl+Z-ig.\n"); while((k=getchar())!=EOF) /* Nagybetk: */ if(k>='A'&&k<='Z')++betu[k-'A']; /* Kisbetk: */ else if(k>='a'&&k<='z')++betu[k-'a']; /* Más karakterek: */ else ++tobbi; /* Eredmények közlése: */ printf("\nBet|Darab Bet|Darab\n" "----+----- ----+-----\n"); for(k=0; k
27

A char típusú változó egyetlen karakter tárolására alkalmas. A char ugyanakkor 8 bites, alapértelmezés szerint eljeles (signed), fixpontos belsábrázolású egész típus is 0111 11112 = 27 ­ 1 = 127 és 1000 00002 = ­27 = ­128 ábrázolási határokkal. Az unsigned char 0 és 255 közötti ábrázolási lehetségekkel rendelkezik. A legtöbb programfejleszt rendszerben az unsigned char alapértelmezésként is beállítható karakter típus. A PELDA8.C­bl kitnen látszik, hogy a tömb definíciója
típus tömbazonosító[méret];

alakú. Pontosabban a deklarációs utasítás azonosítólistája nem csak egyszer változók azonosítóiból állhat, hanem tömbazonosító[méret] konstrukciók is lehetnek köztük. A tömbdefinícióban a méret pozitív, egész érték állandó kifejezés, és a tömb elemszámát határozza meg. Állandó kifejezés az, aminek fordítási idben kiszámítható az értéke. A tömb egy elemének helyfoglalása típusától függ. Az egész tömb a memóriában összesen
sizeof(tömbazonosító) méret*sizeof(típus)

bájtot igényel. Például 16 bites int­et feltételezve a sizeof(betu) 26*sizeof(int) pontosan 52. A magas prioritású, egyoperandusos sizeof operátor megadja a mögötte zárójelben álló objektum, vagy típus által elfoglalt bájtok számát.

28

BEVEZETÉS ÉS ALAPISMERETEK

A tömb egy elemére való hivatkozást indexes változónak is nevezik és szintaktikailag a következ:
tömbazonosító[index]

ahol az index nem negatív érték egész kifejezés
0 <= index <= méret­1

értékhatárokkal. A tömbindexelés C­ben mindig zérustól indul, és a legnagyobb még létez indexérték a méret­1! Például a betu tömbnek létezik betu[0], betu[1], betu[2], és végül betu[BETUK­1] eleme, és ezek így helyezkednek el a memóriában: betu[0] betu[1] betu[2] ... betu[24] betu[25] Vegyük észre, hogy a betu[0]­ban a program az A, a betu[1]­ben a B, ..., és a betu[25]­ben a Z karaktereket számlálja! Tételezzük fel, hogy k értéke 68! Ez ugyebár a D bet ASCII kódja. Ilyenkor a betu[k­'A'] számláló n eggyel. Az A ASCII kódja 65. Tehát betu[68­65]­rl, azaz betu[3] növelésérl van szó! Figyeljünk fel még arra, hogy az eredményeket közl ciklusbeli printf­ben a k+'A' egész kifejezés értékét %c formátumspecifikációval jelentetjük meg, azaz rendre 65­öt, 66­ot, 67­et stb. íratunk ki karakteresen, tehát A­t, B­t, C­t stb. látunk majd. Lássuk még be, hogy az elvalaszto változó értéke szóköz és soremelés karakter közt váltakozik, s így két bet­darab pár képes megjelenni egy sorban. Tehát az elvalaszto változó segítségével produkáljuk a két oszlop páros eredménylistát. Listázni csak azt érdemes, ami valamilyen információt hordoz! Tehát a zérus darabszámú betk kijelzése teljesen felesleges! Magyarán a for ciklusbeli printf­et így kéne módosítani:
if(betu[k]>0) printf("%4c|%5d%c", k+'A', betu[k], elvalaszto);

Megoldandó feladatok: Fokozza úgy a PELDA8.C­ben megoldott feladatot, hogy megszámlálja a magyar ékezetes kis­ és nagybetket is! Készítsen programot, mely a szabvány bemenetet EOF­ig olvassa! Számlálja meg és jelezze ki, hogy hány 0, 1, 2 stb. karakter érkezik! A

C programnyelv

29

nem numerikus karaktereket tekintse egy kategóriának, és ezek számát is közölje! 3.8 Függvények A függvényeket többféleképpen csoportosíthatnánk, de a legpraktikusabb úgy, hogy: · Vannak elre megírtak. Könyvtárakban (.LIB), vagy tárgymodulokban (.OBJ) találhatók, s a kapcsoló-szerkeszt kapcsolja be ket a végrehajtható fájlba. Például: a printf, a getchar, a putchar, vagy a main stb. Minden végrehajtható programban kell lennie egy függvénynek, az indító programnak (a main-nek), mely az egész program belépési pontját képezi. · Mi írjuk ket. Forrásfájlokban helyezkednek el, s kódjukat a fordító generálja. A nyelv központi eleme a függvény. A más programozási nyelvekben szokásos eljárás (procedure) itt explicit módon nem létezik, mert a C szellemében az egy olyan függvény, aminek nincs visszaadott értéke:
void eljárás();

Jelezzük ki egy táblázatban az 1001 és 1010 közötti egész számok köbét!
/* PELDA9.C: Köbtáblázat */ #include #define TOL 1001 /* A tartomány kezdete. */ #define IG 1010 /* A tartomány vége. */ long kob(int); /* Függvény prototípus. */ void main(void){ int i; printf(" Szám|%11s\n-----+-----------\n", "Köbe"); for(i=TOL; i<=IG; ++i) /* Függvényhívás. */ printf("%5d|%11ld\n", i, kob(i)); } long kob(int a){ /* Függvénydefiníció. */ return (long)a*a*a; }

A függvénydefiníció és a függvényhívás fogalmával megismerkedtünk már a Kapcsoló­szerkesztés fejezetben. A függvénydefinícióban van meg a függvény teste, azaz az a kód, amit a függvény meghívásakor végrehajt a processzor. Egy függvényre a programban csak egyetlen definíció létezhet, és ennek nem mondhatnak ellent a prototípusok (deklarációk)! A függvénydefinícióban elírt visszaadott érték típusának egyeznie kell ebbl következleg a programban bárhol elforduló, e függvényre vonat-

30

BEVEZETÉS ÉS ALAPISMERETEK

kozó prototípusokban (deklarációkban) megadott visszatérési érték típussal. A meghívott függvény akkor ad vissza értéket a hívó függvénynek a hívás pontjára, ha a processzor kifejezéssel ellátott return utasítást hajt végre benne. A ,,valamit" szolgáltató függvényben tehát lennie kell legalább egy return kifejezés; utasításnak, és rá is kell, hogy kerüljön a vezérlés. A visszaadott érték meghatározatlan, ha a processzor nem hajt végre return utasítást, vagy a return utasításhoz nem tartozott kifejezés. A visszaadott érték típusa bármi lehet végül is eltekintve a tömbtl és a függvénytl. Lehet valamilyen alaptípus, de el is hagyható, amikor is az alapértelmezés lesz érvényben, ami viszont int. Nézzük a return szintaktikáját!
return ;

A fordító kiértékeli a kifejezést. Ha a függvény visszatérési típusa típus, akkor a kifejezés típusának is ennek kell lennie, vagy implicit konverzióval ilyen típusúvá alakítja a kifejezés értékét a fordító, és csak azután adja vissza. Lássuk be, hogy a PELDA9.C­beli return­ben az explicit (long) típusmódosítás nem azért van, hogy megtakarítsuk a kifejezés értékének visszaadás eltti implicit konverzióját! Az igazi ok az, hogy egy 16 bites int köbe nem biztos, hogy elfér az int­ben! Gondoljunk 1000 köbére, ami 1000000000! Ez jóval meghaladja a 32767­es felsábrázolási korlátot. C­ben az egész típusok területén nincs sem túlcsordulás, sem alulcsordulás! Pontosabban ami túlcsordul, vagy alulcsordul, az mindenféle üzenet nélkül elveszik. A függvényhívás átruházza a vezérlést a hívó függvénybl a hívottba úgy, hogy az aktuális paramétereket is átadja ­ ha vannak ­ érték szerint. A vezérlést a függvénytest els végrehajtható utasítása kapja meg. void visszatérés függvény blokkjában aztán a végrehajtás addig folytatódik, míg kifejezés nélküli return utasítás nem következik, vagy a függvény blokkját záró }-re nem kerül a vezérlés. Ezután a hívási ponttól folytatódik a végrehajtás. Vegyük észre, hogy a return utasítás szintaktikájában az elhagyható kifejezés a paraméter nélküli return­t kívánta jelölni! Belátható, hogy a függvény prototípusnak mindig meg kell elznie a hívást a forrásszövegben. A fordító így tisztában van a hívás helyén a függvény paramétereinek számával, sorrendjével és típusával, ill. ismeri a függvény visszatérési értékének típusát is.

C programnyelv

31

A fordító a prototípus ismeretében implicit típuskonverziót is végrehajt az aktuális paraméter értékén a függvénynek történ átadás eltt, ha az aktuális paraméter típusa eltér. Ha nincs prototípus, akkor nincs implicit konverzió, és csak a ,,csoda" tudja, hogy mi történik az átadott nem megfelel típusú értékkel. Például a kob(3.0) hívás eredménye zérus, ami remélhetleg kellen szemlélteti a prototípus megadásának szükségességét. Ha nincs prototípus, akkor a fordító azt feltételezi (tehát olyan hívási kódot generál), hogy a függvénynek az alapértelmezés miatt int visszaadott értéke van. Ez ugyebár eléggé érdekes eredményre vezet void, vagy lebegpontos visszatérés függvények esetében. A nem int visszaadott érték függvényt legalább deklaráni kell a hívó függvényben! A függvénydeklaráció bemutatásához átírjuk a PELDA9.C­t:
/* PELDA9.C: Köbtáblázat */ #include #define TOL 1001 /* A tartomány kezdete. */ #define IG 1010 /* A tartomány vége. */ void main(void){ int i; long kob(); /* Függvénydeklaráció. */ printf(" Szám|%11s\n-----+-----------\n", "Köbe"); for(i=TOL; i<=IG; ++i) /* Függvényhívás: */ printf("%5d|%11ld\n", i, kob(i)); } long kob(int a){ /* Függvénydefiníció. */ return (long)a*a*a; }

Vegyük észre rögtön, hogy deklarációs utasításunk szintaktikája ismét módosult! Az azonosítólista nem csak egyszer változók azonosítóiból és tömbazonosító[méret] konstrukciókból állhat, hanem tartalmazhat
függvénynév()

alakzatokat is. Természetesen a teljes függvény prototípus is beírható a deklárációs utasításba,
long kob(int); /* Függvénydeklaráció. */

de ilyenkor a függvény prototípus csak ebben a blokkban lesz érvényben. Lássuk be, hogy az utóbbi módszer nem ajánlható olyan több függvénydefinícióból álló forrásfájlra, ahol a kérdéses függvényt több helyrl is meghívják! Sokkal egyszerbb a forrásszöveg elején megadni egyszer a

32

BEVEZETÉS ÉS ALAPISMERETEK

prototípust, mint minden t hívó függvényben külön deklarálni a függvényt. A függvény definíciója prototípusnak is minsül, ha megelzi a forrásszövegben a függvényhívást.
/* PELDA9.C: Köbtáblázat */ #include #define TOL 1001 /* A tartomány kezdete. */ #define IG 1010 /* A tartomány vége. */ long kob(int a){ /* Függvénydefiníció. */ return (long)a*a*a; } void main(void){ int i; printf(" Szám|%11s\n-----+-----------\n", "Köbe"); for(i=TOL; i<=IG; ++i) /* Függvényhívás: */ printf("%5d|%11ld\n", i, kob(i)); }

C­ben tilos függvénydefiníción belül egy másikat kezdeni, azaz a függvénydefiníciók nem ágyazhatók egymásba! Ugyan a Táblázat készítése fejezetben már rögzítettük a függvény szerkezetét, vagyis a blokkszerkezetet, de itt újra kihangsúlyozzuk, hogy a függvénydefinícióban · elbb a deklarációs utasítások jönnek, s · a végrehajtható utasítások csak ezután következnek, és · a két rész nem keveredhet egymással. Ebben a fejezetben csak az érték szerinti hívásról szóltunk, vagyis amikor a formális paraméterek értékét kapja meg a meghívott függvény. Van természetesen név (cím) szerinti hívás is a C­ben, de ezt most még nem tárgyaljuk! 3.9 Prodzsekt Ha a végrehajtható program forrásszövegét témánként, vagy funkciónként külön­külön forrásfájlokban kívánjuk elhelyezni, akkor C­s programfejleszt rendszerekben ennek semmiféle akadály sincs. Be kell azonban tartani a következ szabályokat: · Egy és csak egy forrásmodulban szerepelnie kell az indító programnak (main). · Prodzsektfájlt kell készíteni, melyben felsorolandók a program teljes szövegét alkotó forrásfájlok.

C programnyelv

33

Szedjük szét két forrásmodulra: FOPROG.C­re és FUGGV.C­re, a PELDA9.C programunkat!
/* FOPROG.C: Köbtáblázat */ #include #define TOL 1001 /* A tartomány kezdete. */ #define IG 1010 /* A tartomány vége. */ long kob(int); /* Függvény prototípus. */ void main(void){ int i; printf(" Szám|%11s\n-----+-----------\n", "Köbe"); for(i=TOL; i<=IG; ++i) /* Függvényhívás: */ printf("%5d|%11ld\n", i, kob(i)); } /* FUGGV.C: A függvénydefiníció. */ long kob(int a){ return (long)a*a*a; }

Hozzunk létre egy új prodzsektet! Soroljuk fel benne, vagy szúrjuk bele a két forrásfájlt, és mentsük el, mondjuk, PRODZSI fájlazonosítóval! A prodzsektfájl névadásánál csak arra vigyázzunk, hogy egyetlen benne felsorolt fájl azonosítójával se egyezzen meg a neve! Azért nem konkretizáljuk a prodzsektfájl kiterjesztését, mert az programfejleszt rendszerenként más­más lehet! A programfejleszt rendszerben kell lennie olyan menüpontoknak, melyekkel új prodzsektet hozhatunk létre, meglévt tölthetünk be, nyithatunk meg, menthetünk el, törölhetünk, zárhatunk le stb. Betöltött, vagy megnyitott prodzsekt esetén azonban a fejleszt rendszer mindaddig a prodzsekt fordításával, kapcsoló­szerkesztésével és futtatásával foglalkozik, míg nem töröljük, nem zárjuk be. Akármilyen más forrásfájlokat is nyitogatnánk meg különféle ablakokban, a programfejleszt rendszer az aktuális prodzsekt bezárásáig nem ezek fordításával, szerkesztésével, vagy futtatásával foglalkozik. Lássuk a prodzsekt fordítását és kapcsoló­szerkesztését! FOPROG.C FUGGV.C fordítás FOPROG.OBJ FUGGV.OBJ

4. ábra: A PRODZSI prodzsekt fordítása

34 FOPROG.OBJ FUGGV.OBJ indító program (OBJ) könyvtárak (LIB)

BEVEZETÉS ÉS ALAPISMERETEK

kapcsoló­ szerkesztés

PRODZSI.EXE

5. ábra: A PRODZSI prodzsekt kapcsoló­szerkesztése

Fedezzük fel, hogy a végrehajtható fájl a prodzsekt nevét kapja meg! A prodzsektet alkotó fájlok között implicit függség van. Ez azt jelenti, hogy a prodzsekt futtatásakor csak akkor történik meg a tárgymodul alakjában is rendelkezésre álló forrásfájl fordítása, ha a forrásfájl utolsó módosításnak ideje (dátuma és idpontja) késbbi, mint a vonatkozó tárgymodulé. A kapcsoló­szerkesztés végrehajtásához az szükséges, hogy a tárgymodulok, ill. a könyvtárak valamelyikének ideje késbbi legyen a végrehajtható fájlénál. Az implicit függség fennáll a forrásfájl és a bele #include direktívával bekapcsolt fájlok között is. A tárgymodult akkor is újra kell fordítani, ha valamelyik forrásfájlba behozott fájl ideje késbbi a tárgymodulénál. Bizonyos programfejleszt rendszereknél elfordulhat, hogy az implicit függségi mechanizmust úgy kell külön aktiválni (menüpont), ill. hogy a forrásfájlok, és a beléjük behozott fájlok közti függséget explicit módon kell biztosítani.
stdio.h foprog.c fuggv.c foprog.obj fuggv.obj indító prog. könyvtárak PRODZSI.EXE

6. ábra: Implicit függség a PRODZSI prodzsektnél

A prodzsektfájlban a forrásmodulokon kívül megadhatók tárgymodulok (OBJ) és könyvtárak (LIB) fájlazonosítói is. A kapcsoló­ szerkeszt a tárgymodulokat beszerkeszti a végrehajtható fájlba. A könyvtárakban pedig függvények tárgykódjait fogja keresni. A prodzsektfájlban tulajdonképpen a gyári indító program és a szabvány könyvtárak is kicserélhetek, de ennek pontos megvalósítása már ,,igazán" a programfejleszt rendszertl függ.

C programnyelv

35

3.10 Karaktertömb és karakterlánc A karaktertömbök definíciója a Tömbök fejezetben ismertetettek szerint:
char tömbazonosító[méret];

Az egész tömb helyfoglalása:
méret*sizeof(char) méret

bájt. A tömbindexelés ebben az esetben is zérustól indul és méret­1­ig tart. A C­ben nincs külön karakterlánc (sztring) adattípus. A karakterláncokat a fordítónak és a programozónak karaktertömbökben kell elhelyeznie. A karakterlánc végét az t tartalmazó tömbben egy zérusérték bájttal ('\0') kell jelezni. Például a "Karakterlánc" karakterláncot így kell letárolni a tomb karaktertömbben: tomb 'K' 'a' 'r' 'a' 'k' 't' 'e' 'r' 'l' 'á' 'n' 'c' '\0' 0 1 2 3 4 5 6 7 8 9 10 11 12

Vegyük észre, hogy a karakterlánc els jele a tomb[0]­ban, a második a tomb[1]­ben, s a legutolsó a tomb[11]­ben helyezkedik el, és az ezt követ tomb[12] tartalmazza a lánczáró zérust! Figyeljünk fel arra is, hogy a karakterlánc hossza (12) megegyezik a lezáró '\0' karaktert magába foglaló tömbelem indexével! Fedezzük még rögtön fel, hogy a zérus egész konstans (0) és a lánczáró '\0' karakter értéke ugyanaz: zérus int típusban! Hiszen a karakter konstans belsábrázolása int. A C­ben nincs külön karakterlánc adattípus, s ebbl következleg nem léteznek olyan sztring mveletek sem, mint a karakterláncok · egyesítése, · összehasonlítása, · hozzárendelése stb. Ezeket a mveleteket bájtról­bájtra haladva kell kódolni, vagy függvényt kell írni rájuk, mint ahogyan azt a következ példában bemutatjuk. Készítsen programot, mely neveket olvas a szabvány bemenetrl EOF­ ig vagy üres sorig! Megállapítandó egy fordítási idben megadott névrl, hogy hányszor fordult el a bemeneten! A feladat megoldásához készítend

36

BEVEZETÉS ÉS ALAPISMERETEK

· Egy int strcmp(char s1[], char s2[]) függvény, mely összehasonlítja két karakterlánc paraméterét! Ha egyeznek, zérust ad vissza. Ha az els hátrébb van a névsorban (nagyobb), akkor pozitív, egyébként meg negatív értéket szolgáltat. · Egy int getline(char s[], int n) függvény, mely behoz a szabvány bemenetrl egy sort! A sor karaktereit rendre elhelyezi az s karaktertömbben. A befejez soremelés karaktert nem viszi át a tömbbe, hanem helyette lánczáró '\0'­t ír a karakterlánc végére. A getline második paramétere az s karaktertömb méreténél eggyel kisebb egész érték, azaz a lánczáró zérus nélkül legfeljebb n karaktert tárol a tömbben a függvény. A getline visszatérési értéke az s tömbben végül is elhelyezett karakterlánc hossza.
/* PELDA10.C: Névszámlálás */ #include #define NEV "Jani" /* A számlált név. */ #define MAX 29 /* A bemeneti sor maximális mérete. Most egyben a leghosszabb név is. */ int getline(char s[],int n); /* Függvény prototípusok. */ int strcmp(char s1[], char s2[]); void main(void){ int db; /* Névszámláló. */ char s[MAX+1]; /* Az aktuális név. */ db=0; /* A számláló nullázása. */ printf( "A(z) %s név leszámlálása a bemeneten.\nAdjon\ meg soronként egy nevet!\nProgramvég: üres sor.\n",NEV); /* Sorok olvasása üres sorig a bemenetrl: */ while(getline(s,MAX)>0) /* Ha a sor épp a NEV: */ if(strcmp(s,NEV)==0) ++db; /* Az eredmény közlése: */ printf("A nevek közt %d darab %s volt.\n",db,NEV); } int strcmp(char s1[], char s2[]){ int i; for(i=0; s1[i]!=0&&s1[i]==s2[i]; ++i); return(s1[i]-s2[i]);} int getline(char s[],int n){ int c,i; for(i=0;i
Ha a forráskód egy sorát lezáró soremelés karaktert közvetlenül \ jel elzi meg, akkor mindkét karaktert elveti a fordító, s a két fizikai sort egyesíti, és egynek tekinti. Az ilyen értelemben egyesített sorokat már a másodiktól kezdve folytatássornak nevezik.

C programnyelv A main els printf­jét folytatássorral készítettük el.

37

A két elkészített függvény prototípusából és definíciójából vegyük észre, hogy a formális paraméter karaktertömb (és persze más típusú tömb is) méret nélküli! Az strcmp megírásakor abból indultunk ki, hogy két karakterlánc akkor egyenl egymással, ha ugyanazon pozícióikon azonos karakterek állnak, és ráadásul a lánczáró zérus is ugyanott helyezkedik el. Az i ciklusváltozót zérusról indítva, s egyesével haladva, végigindexeljük az s1 karaktertömböt egészen a lánczáró nulláig (s1[i]!=0) akkor, ha közben minden i­ re az s1[i]==s2[i] is teljesült. Ebbl a ciklusból tehát csak két esetben van kilépés: · Ha elértünk s1 karaktertömb lánczáró zérusáig (s1[i]==0). · Ha a két karakterlánc egyazon pozícióján nem egyforma érték áll (s1[i]!=s2[i]). A visszaadott s1[i]­s2[i] különbség csak akkor: · Zérus, ha s1[i] zérus és s2[i] is az, azaz a két karakterlánc egyenl. · Negatív, ha s1 kisebb (elbbre van a névsorban), mint s2. · Pozitív, ha s1 nagyobb (hátrébb van a névsorban), mint s2. Az strcmp­t már megírták. Tárgykódja benne van a szabvány könyvtárban. Nem fáradtunk azonban feleslegesen, mert a szabvány könyvtári változat ugyanúgy funkcionál, mint a PELDA10.C­beli. Használatához azonban be kell kapcsolni a prototípusát tartalmazó STRING.H fejfájlt. Írjuk be a PELDA10.C #include direktívája után a
#include

, és töröljük ki a forrásszövegbl az strcmp prototípusát és definícióját! Végül próbáljuk ki! Feltéve, hogy s1 és s2 karaktertömbök, lássuk be, hogy C programban, ugyan szintaktikailag nem helytelen, semmi értelme sincs az ilyen kódolásnak, hogy:
s1==s2, s1!=s2, s1>=s2, s1>s2, s1<=s2, s1
E módon ugyanis s1 és s2 memóriabeli elhelyezkedését hasonlítjuk össze, azaz bizton állíthatjuk, hogy az s1==s2 mindig hamis, és az s1!=s2 pedig mindig igaz.

38

BEVEZETÉS ÉS ALAPISMERETEK

A getline ciklusváltozója ugyancsak zérusról indul, és a ciklus végéig egyesével halad. A ciklusmagból látszik, hogy a szabvány bemenetrl érkez karakterekkel tölti fel az s karaktertömböt. A ciklus akkor áll le, · ha az s tömb kimerült, és nem tölthet tovább (i>=n), vagy · ha a beolvasott karakter EOF jelzés (c==EOF), vagy · ha a bejöv karakter soremelés (c=='\n'). Ezután kitesszük a tömb i­ik elemére a lánczáró zérust, s visszaadjuk i­t, azaz a behozott karakterlánc hosszát. getline függvényünk jó próbálkozás az egy sor behozatalára a szabvány bemenetrl, de van néhány problémája, ha a bemenet a billentyzet: 1. A bemeneti pufferben lev karakterek csak soremelés (Enter) után állnak rendelkezésére, s addig nem. 2. Az EOF karakter (Ctrl+Z) érkezése nem jelenti tulajdonképpen a bemenet végét, mert utána még egy soremelést is végre kell hajtani, hogy észlelni tudjuk. 3. Ha a bemeneti pufferben az Enter megnyomásakor n­nél több karakter van, akkor a puffer (az egy sor) csak több getline hívással olvasható ki. Írjuk csak át a PELDA10.C main­jében a
while(getline(s,MAX)>0)

sort a következre
while(getline(s,4)>0)

, és a program indítása után gépeljük be a
JenJaniJojóJani

sorokat! Az els két probléma az operációs rendszer viselkedése miatt van, s jelenleg sajnos nem tudunk rajta segíteni. A második gondhoz még azt is hozzá kell fznünk, hogy fájlvéget a billentyzet szabvány bemeneten a getline­nak csak üres sor megadásával tudunk elidézni. A harmadik probléma viszont könnyedén orvosolható, csak ki kell olvasni a bemeneti puffert végig a getline­ból való visszatérés eltt. Tehát:
int getline(char s[],int n){ int c,i;

C programnyelv

39

for(i=0;i
Megoldandó feladatok: Készítse el a következ függvényeket, és próbálja is ki ket egy rövid programmal! · A void strcopy(char cél[], char forrás[]) átmásolja a cél karaktertömbbe a forrás karakterláncot a lezáró nullájával egyetemben. · A void strct(char s1[], char s2[]) egyesíti s1­be a két paraméter karakterláncot. Javasoljuk, hogy az algoritmus indexeljen elre s1 lánczáró zérusáig, s ettl kezdve csak ide kell másolni s2­t! · Az unsigned strlen(char s[]) visszaadja az s karakterlánc hosszát. Emlékezzünk rá, hogy a lánczáró zérus indexe a karaktertömbben egyben a karakterlánc hossza is! · A void strrv(char s[]) megfordítja a saját helyén a paraméter karakterláncot. Például a "Jani"­ból "inaJ"­nak kell születnie. Írjon programot, mely a getline segítségével sorokat olvas üres sorig a szabvány bemenetrl, és megkeresi a legrövidebbet, s persze ki is jelzi a hosszával együtt! Javasoljuk, hogy a pillanatnyi minimum mentéséhez használja fel az strcopy függvényt! 3.11 Lokális, globális és bels, küls változók Ha alaposan áttanulmányozzuk a PELDA10.C­t, akkor észrevehetjük, hogy mind az strcmp­ben, mind a getline­ban van egy­egy, int típusú i változó. Feltehetnénk azt a kérdést, hogy mi közük van egymáshoz? A rövid válasz nagyon egyszer: semmi. A hosszabb viszont kicsit bonyolultabb: · Mindkét i változó lokális a saját függvényblokkjára. · A lokális változó hatásköre az a blokk, amiben definiálták. · A lokális változó élettartama az az id, míg saját függvényblokkja aktív, azaz benne van a vezérlés. Tehát a két i változónak a névegyezésen túl nincs semmi köze sem egymáshoz. A változó hatásköre az a programterület, ahol érvényesen hivatkozni lehet rá, el lehet érni. Nevezik ezt ezért érvényességi tartománynak is.

40

BEVEZETÉS ÉS ALAPISMERETEK

A lokális változó hatásköre az a blokk, és annak minden beágyazott blokkja, amiben definiálták. A blokkon belülisége miatt a lokális változót bels változónak is nevezik. A változó élettartama az az idszak, amíg memóriát foglal. A lokális változó akkor jön létre (rendszerint a veremben), amikor a vezérlés bejut az t definiáló blokkba. Ha a vezérlés kikerül onnét, akkor a memóriafoglalása megsznik, helye felszabadul (a veremmutató helyrejön). A függvényekben, így a main­ben is, definiált változók mind lokálisak arra a függvényre, amelyben deklarálták ket. Csak az adott függvényen belül lehet hozzájuk férni. Ez a lokális hatáskör. Futási idben a függvénybe való belépéskor jönnek létre, és kilépéskor meg is semmisülnek. Ez a lokális élettartam. A lokális változó kezdértéke ebbl következleg ,,szemét". Pontosabban az a bitkombináció, ami azokban a memóriabájtokban volt, amit most létrejövetelekor a rendszer rendelkezésére bocsátott. Tehát a bels változónak nincs alapértelmezett kezdértéke. Az alapértelmezett kezdértéket implicit kezdértéknek is szokták nevezni. Mi dönti el, hogy a bels változó melyik blokkra lesz lokális? Nyilvánvalóan az, hogy az t definiáló deklarációs utasítást melyik blokk elején helyezik el. Tisztázzuk valamennyire a változó deklarációja és definíciója közti különbséget! Mindkét deklarációs utasításban megadják a változó néhány tulajdonságát, de memóriafoglalásra csak definíció esetén kerül sor. A lokális változó az elzekben ismertetetteken kívül auto tárolási osztályú is. Egészítsük ki újból a deklarációs utasítás szintaktikáját!
azonosítólista;

A tárolási­osztály a következ kulcsszavak egyike lehet: auto, register, static, vagy extern. E fejezet keretében nem foglalkozunk a register és a static tárolási osztállyal! A szintaktikát betartva most felírható, hogy
auto i;

, ami az auto signed int típusú, i azonosítójú változó definíciója. Hogyan lehet auto tárolási osztályú a lokális változó? Nyilván úgy, hogy a lokális helyzet deklarációs utasításban az auto tárolási osztály

C programnyelv

41

alapértelmezés. Az auto kulcsszó kiírása ezekben az utasításokban teljesen felesleges. Foglaljuk össze a lokális változóval, vagy más néven bels változóval, kapcsolatos ismereteinket! Neve: Típusa: Hatásköre: Élettartama: Alapértelmezett tárolási osztálya: Alapértelmezett kezdértéke: Deklarációs utasításának helye: Bármilyen azonosító. Valamilyen típus. Az a blokk, ahol definiálták. Amíg a blokk aktív, azaz a vezérlés benne van. auto Nincs. Annak a blokknak a deklarációs része, ahol a változót használni kívánjuk. Futási idben, többnyire a veremben.

Memóriafoglalás:

Meg kell említeni, hogy a függvények formális paraméterei is lokális változóknak minsülnek, de · deklarációjuk a függvénydefiníció fej részében és nem a blokkjában helyezkedik el, és · értékük az aktuális paraméter értéke. Az elfeldolgozáson átesett forrásmodult fordítási egységnek nevezik. A fordítási egység függvénydefiníciókból és küls deklarációkból áll. Küls deklaráció alatt a minden függvény ,,testén" kívüli deklarációt értjük. Az így definiált változót küls változónak, vagy globális változónak nevezik. Az ilyen változó: · Hatásköre a fordítási egységben a deklarációs utasításának pozíciójától ­ az ún. deklarációs ponttól ­ indul, és a modul végéig tart. Ezt a hatáskört fájl hatáskörnek, vagy globális hatáskörnek nevezik. · Élettartama a program teljes futási ideje. A memória hozzárendelés már fordítási idben megtörténik. Rendszerint az elsdleges adatterületen helyezkedik el, és biztos, hogy nem a veremben. Ez a statikus élettartam.

42

BEVEZETÉS ÉS ALAPISMERETEK

· Alapértelmezett (implicit) kezdértéke: minden bitje zérus. Ez az aritmetikai típusoknál zérus. Karaktertömb esetében ez az üres karakterlánc, hisz rögtön a lánczáró nullával indul. · Alapértelmezett tárolási osztálya extern. Bánjunk csínján az extern kulcsszó explicit használatával, mert deklarációs utasításban való megadása éppen azt jelenti, hogy az utasítás nem definíció, hanem csak deklaráció! Más fordítási egységben definiált küls változót kell ebben a forrásmodulban így deklarálni a rá való hivatkozás eltt. /* Forrásmodul 1 */ extern int i; extern char t[]; /* . . . */ /* Forrásmodul 2 */ int i; char t[10]; /* . . . */
7. ábra: extern deklaráció

Prodzsektünk álljon e két forrásmodulból! Az int típusú i változót és a t karaktertömböt Forrásmodul 2­ben definiálták. Itt történt tehát meg a helyfoglalásuk. Ezek a változók Forrásmodul 1­ben csak akkor érhetk el, ha a rájuk történ hivatkozás eltt extern kulcsszóval deklarálják ket. Vegyük észre, hogy a Forrásmodul 1 elején elhelyezett deklarációs utasítások egyrészt globális szinten vannak, másrészt valóban csak deklarációk, hisz a tömb deklarációjában méret sincs! Persze azt, hogy a tömb mekkora, valahonnét a Forrásmodul 1­ben is tudni kell! Írjuk át úgy a PELDA10.C­t, hogy a beolvasott sor tárolására használatos tömböt globálissá tesszük, majd nevezzük át PELDA11.C­re!
/* PELDA11.C: Névszámlálás */ #include #define NEV "Jani" /* A számlált név. */ #define MAX 29 /* A bemeneti sor maximális mérete. Most egyben a leghosszabb név is. */ int getline(void); /* A függvény prototípusok. */ int strcmp(char s2[]); void main(void){ int db; /* Névszámláló. */ db=0; /* A számláló nullázása. */ printf("A(z) %s név leszámlálása a bemeneten.\nAdjon\ meg soronként egy nevet!\nProgramvég: üres sor.\n",NEV); /* Sorok olvasása üres sorig a bemenetrl: */

C programnyelv

43

while(getline()>0) /* Ha a sor épp a NEV: */ if(strcmp(NEV)==0) ++db; /* Az eredmény közlése: */ printf("A nevek közt %d darab %s volt.\n",db,NEV); } char s[MAX+1]; /* Az aktuális név */ int strcmp(char s2[]){ int i; for(i=0; s[i]!=0&&s[i]==s2[i]; ++i); return(s[i]-s2[i]);} int getline(void){ int c,i; for(i=0;i
Vegyük észre, hogy a PELDA10.C­s verzióhoz képest a getline paraméter nélkülivé vált, és az strcmp­nek meg egyetlen paramétere maradt! Miután a globális s karaktertömb deklarációs pontja megelzi a két függvény definícióját, a függvényblokkok s hatáskörében vannak. Bellük tehát a tömb egyszer hivatkozással elérhet, s nem kell paraméterként átadni. A getline második paramétere tulajdonképpen a MAX szimbolikus konstans volt, s ezt beépítettük a for ciklusba. Elemezgessük egy kicsit a ,,küls változó, vagy paraméter" problémát! · Globális változókat használva megtakarítható függvényhíváskor a paraméterek átadása, vagyis kevesebb paraméterrel oldható meg a feladat. · Küls változókat manipuláló függvények másik programba viszont csak akkor másolhatók át és hívhatók meg változtatás nélkül, ha ezeket a változókat is velük visszük. Látszik, hogy az ilyen függvények mobilitását, újrafelhasználhatóságát csökkentik a járulékos, globális változók. · A csak paramétereket és lokális változókat alkalmazó függvények átmásolás után változtatás nélkül használhatók más programokban. Legfeljebb az aktuális paraméterek lesznek mások a hívásban. Világos, hogy nincs egyértelmen és általánosan ajánlható megoldás a problémára. A feladat konkrét sajátosságai döntik el, hogy mikor milyen függvényeket kell írni, kellenek­e küls változók stb. Summázzuk a globális változóval, vagy más néven küls változóval, kapcsolatos ismereteinket!

44 Neve: Típusa: Hatásköre: Élettartama:

BEVEZETÉS ÉS ALAPISMERETEK Bármilyen azonosító. Valamilyen típus. Globális, vagy fájl. A deklarációs ponttól a forrásmodul végéig tart. Statikus. A program teljes futási ideje alatt él. extern Minden bitje zérus. A forrásmodul minden függvénydefinícióján kívül. Fordítási idben, vagy legalább is az indító program kezddése eltt.

Alapértelmezett tárolási osztálya: Alapértelmezett kezdértéke: Deklarációs utasításának helye: Memóriafoglalás:

3.12 Inicializálás Az inicializálás kezdérték adást jelent a deklarációban, azaz az inicializátorok kezdértékkel látják el az objektumokat (változókat, tömböket stb.). A statikus élettartamú objektumok egyszer a program indulásakor inicializálhatók. Implicit (alapértelmezett) kezdértékük tiszta zérus, ami: · Zérus az aritmetikai típusoknál. · Üres karakterlánc karaktertömb esetén. A lokális élettartamú objektumok inicializálása minden létrejövetelükkor megvalósul, de nincs implicit kezdértékük, azaz ,,szemét" van bennük. Módosítsuk újra a deklarációs utasítás szintaktikáját változókra és tömbökre!
típus azonosító<=inicializátor>; típus tömbazonosító[]<={inicializátorlista}>;

, ahol az inicializátorlista inicializátorok egymástól vesszvel elválasztott sorozata, és a típus a ­t helyettesíti. A változókat egyszeren egy kifejezéssel inicializálhatjuk, mely kifejezés opcionálisan {}-be is tehet. Az objektum kezdeti értéke a kifejezés értéke lesz. Ugyanolyan korlátozások vannak a típusra, és ugyanazok a

C programnyelv

45

konverziók valósulnak meg, mint a hozzárendelés operátornál. Magyarán az inicializátor hozzárendelés-kifejezés. Például:
char y = 'z', k; int a = 10000; /* y 'z' érték, és a k­nak meg */ /* nincs kezdértéke. */ /* a 10000 kezdérték. */

C-ben a statikus élettartamú változók inicializátora csak konstans kifejezés lehet, ill. csak ilyen kifejezések lehetnek tömbök inicializátorlistájában. A lokális objektumok inicializátoraként viszont bármilyen legális kifejezés megengedett, mely hozzárendelés kompatibilis értékké értékelhet ki a változó típusára.
#define N 20 int n = N*2; /* Statikus objektum inicializátora csak konstans kifejezés lehet. */

/* . . . */ void fv(int par){ int i = N/par; /* A lokális objektumé viszont bármilyen legális kifejezés. . . . */ }

Emlékezzünk vissza, hogy globális változók csak egyszer kapnak kezdértéket: a program indulásakor. A lokális objektumok viszont mindannyiszor, valahányszor blokkjuk aktívvá válik. A szövegben használatos objektum fogalom nem objektum­ orientált értelm, hanem egy azonosítható memória területet takar, mely konstans vagy változó érték(ek)et tartalmaz. Minden objektumnak van azonosítója (neve) és adattípusa. Az adattípus rögzíti az objektumnak · lefoglalandó memória mennyiségét és · a benne tárolt információ belsábrázolási formáját. Tömbök esetén az inicializátorlista elemeinek száma nem haladhatja meg az inicializálandó elemek számát!
float tomb[3] = {0., 1., 2., 3.}; /* HIBÁS: több inicializátor van, mint tömbelem. */

Ha az inicializátorlista kevesebb elem, mint az inicializálandó objektumok száma, akkor a maradék objektumok a statikus élettartamú implicit kezdérték adás szabályai szerint kapnak értéket, azaz nullázódnak:
float tmb[3] = {0., 1.}; /* tmb[0]==0.0, tmb[1]==1.0 és tmb[2]==0.0. */

Az inicializálandó objektum lehet ismeretlen méret is, ha az inicializátorlistából megállapítható a nagyság. Például:

46

BEVEZETÉS ÉS ALAPISMERETEK
/* Az itmb három elem lesz. */ /* A lezáró '\0' karakter */ /* miatt 5 elemek a tömbök.*/

int itmb[] = { 1, 2, 3}; char nev[] = "Lali", csaladnev[] = "Kiss";

Megoldandó feladatok: Írja át az eddig elkészített PELDAn.C­ket, és a megoldott feladatok programjait úgy, hogy a változók kezdértéket mindig inicializálással kapjanak!

C programnyelv

47

4 TÍPUSOK ÉS KONSTANSOK
A fordító a forráskódot szintaktikai egységekre, vagy más elnevezéssel szimbólumokra, és fehér karakterekre tördeli. A több egymást követ fehér karakterbl csak egyet tart meg. Ebbl következleg: · Egyetlen C utasítás akár szimbólumonként külön­külön sorba írható. · Egy sorban azonban több C utasítás is megadható. Pontosan hat szimbólum (int i ; float f ;) lesz a következkbl:
int i ; float

f

;

vagy
int i; float f;

A karakter, vagy karakterlánc konstansokban elforduló, akárhány fehér karaktert változatlanul hagyja azonban a fordító. Említettük már, hogy a programnyelv szimbólumokból (token) áll. Most ismertetjük a szimbólum definícióját módosított Backus­Naur, metanyelvi leírással:
szimbólum (token): operátor kulcsszó elválasztó-jel azonosító konstans karakterlánc (string literal)

Az értelmezés nagyon egyszer: a felsorolt hat fogalom mindegyike szimbólum. Az operátorokkal nem ebben a szakaszban foglalkozunk, hanem a MVELETEK ÉS KIFEJEZÉSEKben! A kulcsszavak: auto break case char const continue double else enum extern float for int long register return short signed struct switch typedef union unsigned void

48 default do goto if sizeof static

TÍPUSOK ÉS KONSTANSOK volatile while

Vannak ezeken kívül még nem szabványos kulcsszavak és más védett azonosítók, de ezek mindig megtudhatók a programfejleszt rendszer segítségébl! Ilyenekre kell gondolni, mint a cdecl, a pascal, az stdcall, vagy egy­két aláhúzás karakterrel kezddkre, mint például az __STDC__ stb. ANSI C kompatibilis fordítást elírva mindig kideríthet, hogy az illet fordító mely kulcsszavai, operátorai, vagy elválasztó­jelei nem szabványosak. 4.1 Elválasztó-jel A szintaktikai egységeket (a szimbólumokat) egymástól legalább egy fehér karakterrel el kell választani. Nincs szükség azonban az elválasztó fehér karakterre, ha a két nyelvi egység közé a szintaktikai szabályok szerint egyébként is valamilyen elválasztó-jelet kell írni. Az operátorok is elválasztó­jelnek minsülnek kifejezésekben.
elválasztó-jel: (a következk egyike!) [](){}*,:=;...#

Nézzük meg néhány elválasztó­jel funkcióját! Az utasítást záró pontosvessz minden példában benne van. A kerek zárójeleknek csoportosító funkciója van kifejezésekben. Van, amikor a szintaktika része. Függvényhívásnál az aktuális paramétereket, függvénydeklarációban és definícióban a formális paramétereket ebbe kell tenni.
d = c*(a+b); if(d==z) ++x; fv(akt, par); void fv2(int n);

A szögletes zárójelek tömbök deklarációjában és indexel operátorként használatosak.
char kar, lanc[] = "Sztan és Pan."; kar = lanc[3];

A kapcsos zárójelekbe tett több utasítás szintaktikailag egyetlen utasításnak minsül. A dolgot összetett utasításnak, blokknak nevezzük. Az összetett utasítás (blokk) záró kapcsos zárójele után tilos pontosvesszt tenni!

C programnyelv

49

if(a
A csillag elválasztó-jelnek többféle szerepe van a nyelvben. Eddig csak a szorzás operátor funkcióját ismerjük!
a = 3.14*b;

Az egyenlség jel hozzárendelés operátor, és deklarációs utasításban elválasztja a változót az inicializátortól, vagy inicializátorlistától.
char tomb[5] = {0, 1, 2, 3, 4}; int x=5, b, c=4; b = x+c;

A ketts kereszt elfeldolgozó (preprocessor) direktíva kezdete. A sorbeli els nem fehér karakternek kell annak lennie.
#include #define TRUE 1 #define FALSE 0

4.2

Azonosító

azonosító: nem-számjegy azonosító nem-számjegy azonosító számjegy nem-számjegy: (a következk egyike!) a ... z A ... Z _ számjegy: (a következk egyike!) 0123456789

Az azonosító változóknak, függvényeknek, felhasználó definiálta adattípusoknak stb. adott, a következ pontokban pontosított név: · Kisbetvel, nagybetvel vagy aláhúzás (_) karakterrel köteles kezddni. · A további karakterek lehetnek számjegyek is. · Az azonosító nem lehet kulcsszó vagy valamilyen elredefiniált, védett azonosító. · Az azonosítók kis- és nagybet érzékenyek, azaz az Osszeg, az osszeg vagy az osszeG három különböz azonosító. · Kerülni kell a két és az egy aláhúzás karakterrel kezdd nevek használatát is!

50

TÍPUSOK ÉS KONSTANSOK

· Az azonosítók els, mondjuk, 31 karaktere szignifikáns. Ez azt jelenti, hogy hosszabb nevek is használhatók, de az els 31 karakterükben nem különböz azonosítók ugyanannak minsülnek. Az, hogy az azonosító els hány karaktere szignifikáns, a fordítótól függ. Másrészt a programfejleszt rendszerben egy és ezen érték között változtatható is! Bizonyos küls kapcsolódású azonosítókra, melyeket a kapcsoló­ szerkeszt illeszt, eltér megszorítások lehetnek érvényben. Például kevesebb karakterbl állhatnak, vagy nem kis és nagybet érzékenyek stb. Megoldandó feladat: A felsorolt példaazonosítók közül az els három hibás. Magyarázza meg, hogy mi a probléma velük! · 6os_villamos · Moszer Aranka · Nagy_János · Nagy_Jani · puffer 4.3 Típusok és konstansok a nyelvben A nyelvben összesen négy típuskategória van: · A függvény a nyelv kódgeneráló egysége. Az összes többi kategória csak memóriát foglal az adatoknak. · A void típus többnyire valaminek a meg nem létét jelzi. Például nincs paramétere és visszaadott értéke a void main(void) függvénynek. · Skalár az aritmetikai típus, mely tovább bontható fixpontos egész és lebegpontos valós ábrázolású típusokra. Ilyen a felsorolás (enum) típus és a mutató is. · Aggregátum a tömb, a struktúra és az unió. A mutatókkal, a struktúrákkal és az uniókkal késbbi szakaszokban foglalkozunk! A típusokat úgy is csoportosíthatnánk, hogy vannak · alaptípusok és

C programnyelv · származtatott típusok

51

Az alaptípusok a void, a char, az int, a float és a double. A származtatás pedig a short, a long, a signed és az unsigned ún. típusmódosítókkal történhet. A short és a long, valamint a signed és az unsigned egymást kizáró módosító párok, de a két pár egyazon alaptípusra egyszerre is alkalmazható, azaz létezik · unsigned long int vagy · signed short int stb. A signed és az unsigned módosító azonban csak egész típusokra (char és int) alkalmazható, lebegpontos valós és a void alaptípusra nem. A származtatott típusokba mindig beleértendk az ilyen típusú értékkel visszatér függvények, az ilyen típust paraméterként fogadó függvények, a tömbök stb. Ha programunkban valamilyen azonosítót használni kívánunk, akkor elbb deklarálni (definiálni) kell. A deklaráció teremti meg a kapcsolatot az azonosító és az objektum között, és rögzíti legalább az objektum adattípusát. A származtatott típust is szokás egyszeren típusnak nevezni. ,,Készpénzre váltva" az elz bekezdésben mondottakat: ha a típus valamilyen nem void adattípus, akkor a deklarációk következképp szemléltethetk:
/* Három típus típusú objektum. */ /* Típus típusú értéket visszaadó, paraméter nélküli függvény. */ void fv(típus i); /*Típus típusú paramétert fogadó eljárás. */ típus tt[10]; /* 10 elem, típus típusú tömb. Az elemek rendre: tt[0], ..., tt[9]. */ típus t, t1, t2; típus f(void);

Feltétlenül említést kell tennünk még két, definícióban használható módosítóról, melyek alaposan megváltoztatják a deklarált objektum tulajdonságait. A const nem módosítható objektumot definiál, azaz meggátolja a hozzárendelést az objektumhoz, és az olyan mellékhatásokat, mint az inkrementálás, dekrementálás stb. Magyarán: nem engedi meg az azonosító elérését balértékként. A const a deklaráció elején bármilyen alaptípussal, aggregátum típussal stb. állhat, de tilos többtételes deklaráció els vesszje után kiírni:
float f = 4.5, const cf = 5.6; /* HIBÁS. */

52

TÍPUSOK ÉS KONSTANSOK

Ha a const objektum nem lehet balérték, akkor hogyan lehet valamilyen értékkel ellátni? A const típusú objektum értékkel való ellátásának egyetlen módja az inicializálás. Tehát az ilyen objektum definíciójában kötelez neki kezdértéket adni, mert ez késbb már nem tehet meg. Például:
const float pi = 3.1415926; const max2int = 32767;

Az el nem írt alaptípus miatt a deklaráció specifikátorként egyedül álló const tulajdonképpen const int, s ezért a max2int is az. E deklarációk után ,,botor dolgok" a következ utasítások:
pi = 3.; int i = max2int++; /* Szintaktikai hiba. */ /* Szintaktikai hiba. */

A volatile szó jelentése elpárolgó, illékony, állhatatlan. Módosítóként azt jelzi, hogy az illet változó értéke program végrehajtáson kívüli okból is megváltozhat. Mi lehet a program végrehajtásán kívüli ok? Például megszakítás, B/K port, konkurensen futó végrehajtási szál stb. A volatile kulcsszó deklaráción belüli elhelyezésének szabályai egyeznek a const­éival. A fordító az ilyen változót nem helyezheti el regiszterben, ill. az ilyen objektumot is tartalmazó kifejezés kiértékelése során nem indulhat ki abból, hogy az érték közben nem változik meg. Szóval az ilyen változó minden hivatkozásához elérési kódot kell generálnia akkor is, ha az látszólag hatástalannak tnik. Például:
volatile ticks; /* volatile int a típus. */ void interrupt timer(void) {++ticks;} void varj(int ennyit){ ticks =0; while( ticks < ennyit ); } /* Ne tegyen semmit. */

A while-beli feltétel kiértékelésekor a ciklus minden ütemében tölteni kell ticks értékét. Láttuk, hogy egy objektumot egész élettartamára volatile­lá tehetünk deklarációval, de explicit típusmódosító szerkezettel a változó egyetlen hivatkozását is volatile­lá minsíthetjük.
int ticks; void interrupt timer(void) {++ticks;} void varj(int ennyit){ ticks =0; while( (volatile)ticks < ennyit ); }

C programnyelv

53

Egy objektum egyszerre lehet const és volatile, amikor is az t birtokló program nem módosíthatja, de megváltoztathatja az értékét bármely aszinkron program vagy végrehajtási szál. Az adattípusok és konstansok tárgyalásának megkezdése eltt lássuk a konstans definícióját!
konstans: egész-konstans enum-konstans lebegpontos-konstans karakter-konstans

Tudjuk, hogy az objektum is lehet konstans, de itt most nem ilyen állandóról van szó. Az ,,igazi" konstans nem azonosítható memória területet takar, mely fix, a program futása alatt meg nem változtatható értéket tartalmaz. A konstansnak nincs szoftveresen is használható címe, de van adattípusa, mely meghatározza az állandónak · lefoglalandó memória mennyiségét és · a benne tárolt érték belsábrázolási formáját. 4.3.1 Egész típusok és konstansok Már említettük, hogy az egész típusok a char és az int alaptípusból a signed - unsigned, valamint a short - long módosító párok alkalmazásával állíthatók el, azaz a deklaráció írásszabálya eltekintve a tárolási osztálytól és az inicializálástól:
azonosítólista;

A típusmódosítók alaptípus párost azonban a továbbiakban is típusnak fogjuk nevezni. A szabályok a következk: · Mind az alaptípus, mind a típusmódosítók elhagyható. A kett együtt azonban nem. · Ha elhagyjuk az alaptípust, alapértelmezés az int. · A short - long módosítók elhagyásakor nincs rájuk vonatkozó alapértelmezés. · Ha elhagyjuk a signed - unsigned módosítót, alapértelmezés a signed.

54 Típus char, signed char unsigned char short, short int, signed short int unsigned short, unsigned short int int, signed int unsigned, unsigned int long, long int, signed long int unsigned long, unsigned long int

TÍPUSOK ÉS KONSTANSOK Méret Minimális érték Maximális érték bájtban 1 1 2 2 -128 0 -32768 0 127 255 +32767 65535 short vagy long ugyanígy, de unsigned +2147483647 4294967295

2 vagy 4 short vagy long 2 vagy 4 4 4 ugyanígy, de unsigned -2147483648 0

8. ábra: Egész típusok

· A char alaptípussal kapcsolatban kiegészítésre szorul a signed char alapértelmezés! A programfejleszt rendszerben ugyanis unsigned char is beállítható a char típus alapértelmezéseként. A lehetséges karaktertípusok ilyenkor így változnak: Típus char, unsigned char signed char Méret Minimális érték Maximális érték bájtban 1 1 0 ­128 255 127

· A táblázatból (8. ábra) is jól látszik, hogy az ANSI szabvány nem ír el pontos méretet az int típusra, csupán csak annyit, hogy: short <= int <= long. · A belsábrázolás fixpontos, bináris egész, ezért signed típusokra a negatív számokat kettes komplemensük alakjában tárolja a fordító.

C programnyelv

55

· A szabványos LIMITS.H fejfájlban találunk szimbolikus állandókat (CHAR_MIN, UCHAR_MAX, SHRT_MIN, stb.) az ábrázolási korlátokra! A nyelv szerint nincs sem egész túlcsordulás, sem alulcsordulás. Az egész konstansnál láthatunk erre is egy rövid példát!
egész-konstans: decimális-konstans oktális-konstans hexadecimális-konstans

A metanyelvben a < ... > az elhagyhatóságot jelöli!
egész-konstans: decimális-konstans oktális-konstans hexadecimális-konstans decimális-konstans: nemzérus-számjegy decimális-konstans számjegy oktális-konstans: 0 oktális-konstans oktális-számjegy hexadecimális-konstans: 0xhexadecimális-számjegy 0Xhexadecimális-számjegy hexadecimális-konstans hexadecimális-számjegy nemzérus-számjegy: (a következk egyike!) 123456789 oktális-számjegy: (a következk egyike!) 01234567 hexadecimális-számjegy: (a következk egyike!) 0123456789abcdefABCDEF egész-utótag: unsigned-utótag long-utótag unsigned-utótag: (a következk egyike!) uU long-utótag: (a következk egyike!) lL

A definíció szerint: · Az egész konstans lehet decimális, oktális és hexadecimális. · Az egész konstansnak nincs eljele (pozitív), azaz a negatív egész konstans eltt egy egyoperandusos mínusz operátor áll.

56

TÍPUSOK ÉS KONSTANSOK

· A decimális egész konstans int, long int, vagy unsigned long int belsábrázolású a konstans értékétl függen alapértelmezés szerint. Az oktális és hexadecimális egész konstans az értékétl függen int, unsigned int, long int, vagy unsigned long int belsábrázolású. Decimális 0­32767 Oktális 0­077777 0100000­ 01777777 32767­ 02000000­ 2147483647 017777777777 Hexadecimális 0X0­0X7FFF 0X8000­ 0XFFFF 0X10000­ 0X7FFFFFFF int unsigned long unsigned long Típus

2147483648­ 020000000000­ 0X80000000­ 4294967295 037777777777 0XFFFFFFFF

9. ábra: Egész konstansok belsábrázolása

Lássunk néhány példát a decimális, az oktális és a hexadecimális egész konstansokra!
int int int int i j k l = = = = 10; 010; 0; 0XFF; /* j kezdetben decimálisan 8. */ /* k decimálisan és oktálisan is 0. */ /* k decimálisan 255. */

· Explicit egész utótagot a konstans utána írva megváltoztathatjuk az alapértelmezett belsábrázolást. Az inicializátor konstansok típusegyeztetésével elkerülhet a szükségtelen közbens konverzió. Például:
unsigned ui=2u; long li=16l; unsigned long uli=17lu;

Az egész konstans 0 és 4294967295 értékhatárok közt megengedett. Ez azonban a programozó felelssége, mert a 4294967295-nél nagyobb érték egyszeren csonkul. Legfeljebb egy ,,halk" figyelmeztet üzenet jöhet a fordítótól. Például a
#include void main(void){ unsigned long pipipp; pipipp = 4300000000; printf("Pipipp = %ld\n", pipipp); }

hatására 5032704 jelenik meg, ami egy híján 4300000000 - 4294967295.

C programnyelv

57

4.3.2 Felsorolás (enum) típus és konstans Az enum (enumerate) adattípus mnemonikussá tesz egy sorozat egész értékre való hivatkozást. Például az
enum napok{ vasar, hetfo, kedd, szerda, csut, pentek, szomb} nap;

deklaráció létrehoz egy egyedi enum napok egész típust. Helyet foglal egy ilyen típusú, nap azonosítójú változónak, és definiál hét, konstans egész értéket reprezentáló enumerátort: az enumerátor készletet. Felsorolás típusú változóban a típushoz definiált enumerátor készlet egyik értékét tarthatjuk. Az enumerátort enum konstansnak is nevezik. Felsorolás típusú változókat használhatunk index kifejezésben, minden aritmetikai és relációs operátor operandusaként, stb. Tulajdonképpen az enum a #define direktíva alternatívájának is tekinthet. ANSI C­ben az enumerátor értékét definiáló kifejezés csak egészérték konstans kifejezés lehet, és típusa mindig int. Az enum változó tárolásához használt memória is annyi, mint az int típushoz használt. enum típusú konstans vagy érték a C­ben ott használható, ahol egész kifejezés is. Lássuk a szintaktikát!
enum-specifikátor: enum {enumerátorlista} enum azonosító enumerátorlista: enumerátor enumerátorlista, enumerátor enumerátor: enum-konstans enum-konstans = konstans-kifejezés enum-konstans: azonosító

Az enum-specifikátor definíciójában az enumerátorlistával definiált enum típus opcionális azonosítóját enum címkének (tag) nevezzük. Ha ezt elhagyjuk a definícióból, névtelen enum típust kapunk. Ennek az az ódiuma, hogy késbb nincs lehetségünk ilyen felsorolás típusú változók definíciójára, hisz nem tudunk a névtelen enum­ra hivatkozni. Névtelen enum típusú változók tehát csak a névtelen enum deklarációjában definiálhatók, máshol nem. Az
enum {jan, feb, marc, apr, maj, jun, jul, aug, szep, okt, nov, dec};

így teljesen használhatatlan, hisz képtelenség ilyen típusú változót deklarálni. A probléma az azonosítólista megadásával elkerülhet:

58

TÍPUSOK ÉS KONSTANSOK

enum {jan, feb, marc, apr, maj, jun, jul, aug, szep, okt, nov, dec} ho=jan, honap;

Foglaljuk össze az enum deklarációval kapcsolatos tapasztalatainkat:
enum <{enumerátorlista}> ;

, ahol a < > most is az elhagyhatóságot jelöli. Az enum címke megadása tehát azt biztosítja, hogy az "enum enum címke" után azonosítólistát írva késbb ilyen felsorolás típusú változókat definiálhatunk. A fejezet elején említett deklarációs példában a napok azonosító az opcionálisan megadható enum címke, mely késbb enum napok típusú változók definícióiban használható fel. Például:
enum napok fizetes_nap, unnepnap;

Térjünk egy kicsit vissza az enumerátorokhoz! Láttuk, hogy az enum konstans a felsorolás típus deklarációjában definiált azonosító. Miután az enumerátor egész adattípusú, kifejezésekben ott használható, ahol egyébként egész konstans is megadható. Az enumerátor azonosítójának egyedinek kell lennie az enum deklaráció hatáskörében beleértve a normál változók neveit és más enumerátorlisták azonosítóit. Az enum konstansok az egyszer változók névterületén helyezkednek el tehát. Az enum címkéknek viszont az enum, a struktúra és az uniócímkék névterületén kell egyedinek lenniük. A névterület olyan azonosító csoport, melyen belül az azonosítóknak egyedieknek kell lenniük. A C­ben több névterület van, amibl itt kettt meg is említettünk. Két különböz névterületen létez, ugyanazon azonosítónak semmi köze nincs egymáshoz. A névterületeket majd egy késbbi fejezetben tárgyaljuk! Lássunk példákat ezen közbevetés után!
int hetfo = 11; /* A hetfo egyszer int típusú változó.*/ { enum napok{ vasar, hetfo, kedd, szerda, csut, pentek, szomb}; /* A hetfo enumerátor ebben a blokkban elrejti a hetfo int típusú változót. */ double csut; /* HIBÁS, mert ezen a névterületen van már egy ,,csut" enumerátor. /* . . . */ } hetfo+=2; /* OK, mert itt már csak a hetfo int változó létezik. */

Az enum napok deklarációban szerepl enum konstansok implicit módon kaptak értéket zérustól indulva és mindig eggyel növekedve. A vasar így 0, a hetfo 1, ..., és a pentek 6. Az enum konstansok azonban explici-

C programnyelv

59

ten is inicializálhatók. Akár negatívak is lehetnek, ill. több enumerátor lehet azonos érték is. Például:
enum ermek{ egyes=1, kettes, otos=5, tizes=2*otos, huszas=kettes*tizes, szazas=otos*huszas};

Vegyük észre, hogy az enumerátor definiált már az enumerátorlista következ tagjához érve! Mondottuk, hogy a felsorolás típusok mindenütt feltnhetnek, ahol egyébként az egész típusok megengedettek. Például:
enum napok{ vasar, hetfo, kedd, szerda, csut, pentek, szomb}; enum napok fizetes_nap = szerda, nap; int i = kedd; /* OK */ nap = hetfo; /* OK */ hetfo = kedd; /* HIBÁS, mert a hetfo konstans. */

enum típusú változóhoz egész érték hozzárendelése megengedett, de explicit típusmódosító szerkezet használata javallott. Tehát a
fizetes_nap = 5;

helyett
fizetes_nap = (enum napok) 5;

írandó. A fordítóban nincs mechanizmus a konvertálandó egész érték érvényességének ellenrzésére! Pontosabban a következ lehetetlenség elkerülése a programozó felelssége:
n = (napok) 958;

Az enum típus egész (integral) típus. Implicit módon konvertál így egésszé bármely enumerátort a fordító, de a másik irányban az explicit típusmódosítás javasolt:
int i; enum napok n = szomb; i = n; n = 5; n = (napok) 5; /* /* /* /* OK */ OK */ Nem javasolt! */ OK */

Egész típusnak az egész értékek tárolására alkalmas, aritmetikai adattípusokat (char, short, int, long és enum) nevezzük.

60

TÍPUSOK ÉS KONSTANSOK

4.3.3 Valós típusok és konstans Az IEEE lebegpontos belsábrázolású valós alaptípusok a float és a double. Egyedül a long módosító használata megengedett, s az is csak a double eltt, azaz van még long double származtatott típus. A float típusú, egyszeres pontosságú lebegpontos ábrázolás 4 bájtot foglal, melybl 8 bit a 128 többletes, bináris exponens, 1 bit a mantissza eljele (1 a negatív!) és 23 bit a mantissza. A mantissza 1.0 és 2.0 közötti szám. Miután a mantissza legnagyobb helyiérték bitje mindig egy, az ábrázolás ezt nem tárolja. eljel eltérített karakterisztika mantissza 22 - 0 bitek mantissza 51 - 0 bitek 31. bit 30 ­ 23 bitek eljel eltérített karakterisztika

A double exponens 11-, s a mantissza 52 bites. 63. bit 62 ­ 52 bitek

A lebegpontos belsábrázolás méretei, megközelít határai és decimális jegyben mért pontossága a következ táblázatban látható: Típus float double long double Méret bájtban 4 8 10 Határok: ±3.4*10-38 - ±3.4*10+38 Pontosság: 6­7 decimális jegy

±1.7*10-308 - ±1.7*10+308 15­16 decimális jegy ±1.2*10-4932 - ±1.2*10+4932 19 decimális jegy
10. ábra: Lebegpontos típusok

Ehhez már csak annyit kell hozzáfzni, hogy lebegpontos zérus az, amikor a belsábrázolás minden bitje zérus. A leírás az IEEE szabványos lebegpontos ábrázolásról szól, de elképzelhet, hogy a konkrét fordító más formával dolgozik. A szabványos FLOAT.H fejfájlban azonban mindig megtalálhatók szimbolikus állandók (FLT_MIN, DBL_MAX stb.) alakjában az ábrázolási határok és más konkrétumok az aktuális belsábrázolásról.
lebegpontos-konstans: tört-konstans számjegy-sor exponens-rész

C programnyelv
tört-konstans: . számjegy-sor számjegy-sor . exponens-rész: e számjegy-sor E számjegy-sor eljel: (a következk egyike!) +számjegy-sor: számjegy számjegy-sor számjegy float-utótag: (a következk egyike!) flFL

61

· A mantisszából elhagyható a decimális egész rész vagy a tört rész, de a kett együtt nem. · Elhagyható a tizedes pont vagy az exponens rész, de mindkett nem. · A lebegpontos konstans eljeltelen (pozitív). A negatív lebegpontos konstans eltt egy egyoperandusos mínusz operátor áll. · Float utótag nélkül a lebegpontos konstans double belsábrázolású. Az utótag megadásával kikényszeríthetjük a float (f vagy F), ill. a long double (l vagy L) belsábrázolást. double belsábrázolású lebegpontos konstansok:
-.5e35, 5., 5E-4, 3.4

Ugyanezek float és long double belsábrázolásban:
-.5e35f, 5.L, 5E-4F, 3.4l

Az egyszeri programozó beírta a forrásszövegébe a
float v=143736120; /* . . . */ printf("%9.0f\n", v);

sorokat, és meglepdött a 143736128­as eredményen. Ha v értékét eggyel csökkentette, akkor meg 143736112­t kapott. Mi lehet a probléma? A 143736120 (0X8913D38) binárisan
1000 1001 0001 0011 1101 0011 1000

, de csak 24 bit fér el a belsábrázolás mantisszájában. A fordító a legmagasabb helyiérték, lecsorduló bit értékével megnöveli a mantisszát. Most, a karakterisztikával nem foglalkozva, az új érték:
1000 1001 0001 0011 1101 0100 0000

62

TÍPUSOK ÉS KONSTANSOK

, ami éppen 143736128 (0X8913D40). A 143736119 (0X8913D37) binárisan
1000 1001 0001 0011 1101 0011 0111

esetében az eredmény
1000 1001 0001 0011 1101 0011 0000

lesz, ami143736112 (0X8913D30). Komolyan kell tehát venni a lebegpontos ábrázolások decimális jegyben mért pontosságát, és az ábrázolási határokat. Még két dologra feltétlenül oda kell figyelni: · A decimális véges tizedes tört többnyire nem véges bináris tört, azaz az átalakítás oda­vissza nem teljesen pontos. · Matematikai iterációt, ami addig tart, míg két, számolt, valós érték különbsége zérus nem lesz, nem szabad egy az egyben megvalósítani, mert az esetek többségében végtelen ciklushoz vezet. 4.3.4 Karakter típus és konstans A karakter típusról már szó esett az egész típusok tárgyalásánál. Tudjuk, hogy három karakter típus van: char, signed char és unsigned char. A karakter típusú objektum helyfoglalása 1 bájt mindenképp. A karakter konstans típusára és helyfoglalására rögtön kitérünk definíciója után!
karakter-konstans: 'c-karakter-sor' c-karakter-sor: c-karakter c-karakter-sor c-karakter c-karakter: bármilyen karakter aposztróf ('), fordított per jel (\) és soremelés (\n) kívételével escape-szekvencia

C programnyelv Szekvencia Érték \0 \a \b \t \n \v \f \r \" \' \? \\ \ooo \xhh \Xhh 0X00 0X07 0X08 0X09 0X0A 0X0B 0X0C 0X0D 0X22 0X27 0X3F 0X5C bármi bármi bármi Karakter NUL BEL BS HT LF VT FF CR " ' ? \ bármi bármi bármi Funkció karakterlánc vége fütty visszatörlés vízszintes tab soremelés függleges tab lapdobás kocsi vissza macskaköröm aposztróf kérdjel fordított per jel max. 3 oktális számjegy max. 3 hexadec. jegy max. 3 hexadec. jegy

63

11. ábra: Escape szekvenciák

A definícióból következleg a karakter konstans aposztrófok közt álló egy vagy több karakter, ill. escape szekvencia. Ezen az elven beszélhetünk egykarakteres és többkarakteres karakter konstansról. · A karakter konstans adattípusa mindenképpen int. Tudjuk, hogy az int helyfoglalása 2 vagy 4 bájt, így maximum négy karakteres karakter konstans létezhet. Például:
'An', '\n\r', 'Alfi', 'tag', '\007\007\007'

Tudjuk, hogy az int belsábrázolású karakter konstans esetében is érvényben van a signed char alapértelmezés. Ennek következtében az int méreténél kevesebb karakterbl álló karakter konstansok eljel kiterjesztéssel alakulnak int belsábrázolásúvá. Konkrét példaként tekintsük a 2 bájtos int-et, és a 852­es kódlapot! Az é bet kódja 130 (0X82) és az a beté 97 (0X61). Az 'é'-bl így 0XFF82 és az 'a'-ból 0X0061 lesz a memóriában, s így igaz lesz a következ reláció:

64
'é' < 'a'

TÍPUSOK ÉS KONSTANSOK

Ha unsigned char az alapértelmezés, akkor a fels bájt(ok)at bizonyosan 0X00-val tölti fel a fordító (az 'é'-bl 0X0082 lesz), és nem jön el az elbb taglalt probléma. A következ kis példa szemlélteti a tárgyalt karakter típus és karakter konstans méreteket!
#include #include #define CH 'x' /* Egykarakteres karakter konstans. */ void main(void) { char ch = CH; /* Karakter típusú változó. */ printf("Az int mérete :\t\t\t%d\n", sizeof(int)); printf("A char mérete :\t\t\t%d\n", sizeof(char)); printf("A karakter konstans mérete:\t %d\n", sizeof(CH)); printf("A karakter változó mérete:\t%d\n", sizeof(ch)); }

Szólnunk kell még néhány szót a pontosítás kedvéért az escape szekvenciáról! A \ jelet követheti egy a táblázatban (11. ábra) ismertetett karakter, vagy egy legfeljebb háromjegy oktális szám, vagy x után hexadecimális számjegyek. Például a '\03' a Ctrl+C, a '\x3F' a ? karakter, stb. Szóval az escape szekvencia leírhat vezérl és ,,igazi" karaktereket egyaránt. Nézzük még a szabályokat! · Ha az escape szekvencia fordított per jelét nem legális karakter követi, akkor legfeljebb figyelmeztet üzenetet kapunk, és a fordító elhagyja a \ jelet, de megtartja az illegális karaktert. Például a \z­bl a z marad meg. · Az oktálisan vagy hexadecimálisan adott escape szekvencia végét az els nem oktális, ill. hexadecimális karakter is jelzi. Például a '\518'-ból két karakteres karakter konstans lesz, ahol a karakterek rendre a '\51' és a '8', azaz ')8'. · Vigyázzunk a nem egyértelm megadásra is! Például a "\712 kmves." karakterláncból látszik a programozó törekvése, azaz hogy a BEL karakter után a 12 kmves szöveg következne. Ez azonban így szintaktikai hiba. A helyes eredményhez például "\7" "12 kmves." módon juthatunk. Miután az escape szekvenciával tulajdonképpen egyetlen karaktert adunk meg, a \ jelet követ oktális számérték nem haladhatja meg a 0377­et, a hexadecimális a 0XFF­et, vagyis a 255­öt!

C programnyelv

65

Igazából a karakter a végrehajtáskor érvényes gépi karakterkészletbl vett érték. A könyvben végig bájtos (char) kódkészletet (ASCII) tételezünk fel, s nem foglalkozunk a széles karaktertípussal (wchar_t) és az Unicode kódkészlettel! Az eddig tárgyalt adattípusokat összefoglaló névvel aritmetikai adattípusoknak is nevezhetjük. Az aritmetikai adattípusok ,,zavarba ejt" bsége a nyelvben azonban nem arra szolgál, hogy a programozó · csak úgy ,,ukk-mukk-fukk" kiválasszon valamilyen adattípust adatai és (rész)eredményei tárolására, hanem arra, hogy · a számábrázolási korlátokat tekintetbe véve alaposan végiggondolja, hogy érték és pontosság vesztés nélkül milyen adattípust kell használnia az egyes adataihoz, (rész)eredményeihez. Esetleg milyen explicit konverziókat kell elírnia a részletszámítások végzése során a pontosság vesztés elkerüléséhez. Készítsen szorzat piramist! A piramis alsó sora 16 darab 100­tól 115­ig terjed egész szám! A következ sorokat úgy kapjuk, hogy az alattuk lev sor elemeit páronként összeszorozzuk. A második sor így néz ki tehát: 100*101 = 10100, 102*103 = 10506, ..., 114*115 = 13110. A harmadik sorban a következk állnak: 10100*10506 = 106110600, 10920*11342 = 126854640, ..., 12656*13110 = 165920160, és így tovább. Figyeljünk arra, hogy az eredményül kapott számokat mindig a lehet legkisebb helyen tároljuk, de az értékeknek nem szabad csonkulniuk!
/* PELDA12.C: Szorzat piramis */ #include #define N 16 void main(void){ int i; /* Az alsó sor: 100-s nagyságrend. */ char n[N]={100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115}; short n1[N/2]; /* 2. sor: 10000-s nagyságrend. */ long n2[N/4]; /* 3. sor: 100000000 kb. */ long double n3[N/8]; /* 4. sor: 10 a 16-n kb. */ printf("Szorzat piramis: els szint\n"); for(i=0; i
66

TÍPUSOK ÉS KONSTANSOK

for(i=0; i
Már a PELDA7.C példában találkoztunk az egészekre vonatkozó h és l hosszmódosítókkal a formátumspecifikációkban. Most a long double típusú paraméter jelzésére való L hosszmódosítót ismerhettük meg. Ha a szorzat piramis második szintjének kiszámítását végz
n2[i/2]=(long)n1[i]*(long)n1[i+1];

kifejezésbl elhagyjuk a két, explicit (long) típusmódosítót, akkor a listarész a következképp módosul:
Szorzat piramis: második szint 10100*10506 = 7816 10920*11342 = -8400 . . .

Ez vitathatatlanul rossz, de miért? · Az n1 tömb short típusú. · A kétoperandusos szorzási mvelet operandusai azonos típusúak, tehát semmiféle implicit konverzió nem történik, és az eredmény típusa is short. · A 32 bites regiszterben a 10100*10506­os szorzás végrehajtása után ugyan ott van a helyes eredmény, a 106110600 (0X6531E88)
0000 0110 0101 0011 0001 1110 1000 1000

, de a 16 bites short típus miatt csak az alsó két bájt képezi az eredményt. Az meg 0X1E88, vagyis 7816. Megemlítjük még, hogy a ,,csúcs" értékének (n3[0]*n3[1]) pontosság vesztés nélküli tárolásához még a long double típus is kevés! Megoldandó feladatok: Készítsünk programot, mely megállapítja az ÓÓ:PP:MM alakú karakterláncról, hogy érvényes id­e! Az óra zérus és 23, a perc, valamint a másodperc 0 és 59 közötti érték lehet csak. Jelezze ki a szoftver, ha a karakterlánc formailag hibás, és kérjen másikat! Ha érvényes az id, akkor határozza meg és jelezze ki értékét másodpercben!

C programnyelv

67

Egy másik szoftver fogadjon a szabvány bemenetrl egész számokat mindaddig, míg üres sort nem adnak meg! Képezze rendre az egész számok és két szomszédjuk négyzetösszegét! A szám szomszédja a nálánál eggyel kisebb és eggyel nagyobb érték. A képernyn jelenítse meg fejléccél ellátva, táblázatos formában a számhármasokat (a bevitt értékek álljanak középen) és a négyzetösszegeket! Legvégül adja meg a négyzetöszszegek összegét is. A számok legyenek jobbra igazítottak! 4.4 Karakterlánc (string literal): A karakterláncokról már szó volt a BEVEZETÉS ÉS ALAPISMERETEK szakaszban! A definíció:
karakterlánc: "" s-karakter-sor: s-karakter s-karakter-sor s-karakter s-karakter: bármilyen karakter idézjel ("), fordított per jel (\) és a soremelés (\n) kivételével escape-szekvencia (11. ábra)

A karakterlánc adattípusa char tömb. A tömb mérete mindig eggyel hosszabb, mint ahány karakterbl a karakterlánc áll, mert a nyelv a lánc végét '\0' karakterrel jelzi. A karakterlánc konstansokat mindig a statikus adatterületen helyezi el a fordító. 'L' 0 'a' 1 'l' 2 'i' 3 '\0' 4 Például a "Lali" karakterlánc egymást követ, növekv cím memória bájtokon így helyezkedik el.

A karakterlánc karaktereiként használható az escape szekvencia is. Például a
"\t\t\"Név\"\\\tCím\n\n

karakterláncot printf függvény paramétereként megadva
"Név"\ Cím

jelenik meg utána két soremeléssel. A csak fehér karakterekkel elválasztott karakterlánc konstansokat a fordító elemzési fázisa alatt egyetlen karakterlánccá egyesíti:
"Egy " "kett, " "három..." "Egy kett, három..."

Tudjuk, hogy a karakterlánc konstans a folytatássor (\) jelet használva több sorba is írható:

68

TÍPUSOK ÉS KONSTANSOK

printf("Ez igazából egyetlen \ karakterlánc lesz.\n");

Írjunk meg néhány karakterláncot kezel függvényt! Az unsigned strlen(char s[]) a paraméter karakterlánc hosszát szolgáltatja. Indítsunk egy változót zérustól, és indexeljünk elre vele a karakterláncon, míg a lánczáró nullát el nem érjük! Ez az index a karakterlánc hossza is egyben.
unsigned strlen(char s[]){ unsigned i=0u; while(s[i]!='\0') ++i; return i; }

A void strcopy(char cél[], char forrás[]) a hozzárendelést valósítja meg a karakterláncok körében azzal, hogy a forrás karakterláncot átmásolja a cél karaktertömbbe. Indexeljünk most is végig egy változóval a forrás karakterláncon a lezáró zérusig! Közben minden indexre rendeljük hozzá a forrás tömbelemet a cél tömbelemhez! Vigyázzunk, hogy a lánczáró nulla is átkerüljön!
void strcopy(char cél[], char forrás[]){ int i=0; while((cél[i]=forrás[i])!=0) ++i; }

Javítsunk kicsit ezen a megoldáson! A while­beli reláció elhagyható, hisz a hozzárendelés is mindig igaz (nem zérus), míg a forrás[i] nem lánczáró zérus. Belátható, hogy a '\0' karakter is átkerül, mert a while csak a hozzárendelés végrehajtása után veszi csak észre, hogy kifejezése hamissá (zérussá) vált. Tehát:
while(cél[i]=forrás[i]) ++i;

Lássuk be, hogy a
while(cél[i++]=forrás[i]);

is tökéletesen funkcionál, hisz · Elállítja a forrás tömb i­ik elemét. · Ezt hozzárendeli cél tömb i­ik eleméhez. · Az utótag ++ miatt mellékhatásként közben i értéke is n egyet. A legjobb megoldásunk tehát:
void strcopy(char cél[], char forrás[]){ int i=0;

C programnyelv
while(cél[i++]=forrás[i]); }

69

A void strct(char s1[], char s2[]) egyesíti a két paraméter karakterláncot s1­be. Indexeljünk elre az s1­en a lánczáró nulláig, s ezután ide kell másolni az s2 karakterláncot! Belátható, hogy a helyes megoldáshoz két indexváltozó szükséges, mert a másolásnál az egyiknek s1 lánczáró nullájának indexétl kell indulnia, míg a másiknak s2­n zérustól.
void strct(char s1[], char s2[]){ int i=0, j=0; while(s1[i]) ++i; while(s1[i++]=s2[j++]); }

A void strup(char s[]) nagybetssé alakítja saját helyén az s karakterláncot. Indexeljünk végig az s karaktertömbön a lánczáró zérusig! Egy­egy pozíción meg kell vizsgálni a tömbelem értékét. Ha nem kisbet, akkor nem kell tenni semmit. Ha kisbet, át kell írni nagybetvé. Ha csak az angol ábécé betivel foglalkozunk, akkor meg kell határozni a kisbet tömbelem eltolását 'a'­hoz képest, s ezt az eltolást hozzá kell adni 'A'­hoz.
void strup(char s[]){ int i; for(i=0; s[i]; ++i) if(s[i]>='a'&&s[i]<='z')s[i]=s[i]-'a'+'A'; }

Feltétlenül meg kell említeni, hogy a most megírt karakterlánc kezel függvények kicsit más névvel, de azonos funkcióval, és paraméterezéssel léteznek a szabvány könyvtárban. Használatukhoz azonban a STRING.H fejfájlt be kell kapcsolni. A névlista:
strlen strcopy strct strup strlen strcpy strcat strupr

Próbáljuk ki a megírt függvényeinket egy rövid programmal! Kérjünk be egy sort a szabvány bemenetrl! Alakítsuk át nagybetssé! Tegyük elé az ELEJE:, és mögé a :VÉGE szöveget! Jelentessük meg az eredményt! Írjuk még ki az eredeti és az egyesített karakterlánc hosszát! Helyszke miatt a függvények teste helyett csak /* ... */­eket közlünk. A valóságban oda kell másolni a függvénydefiníció forrásszövegét is.

70

TÍPUSOK ÉS KONSTANSOK

/* PELDA13.C: Karakterlánc kezelése */ #include #define SOR 80 /* Bemeneti sor max. hossza. */ int getline(char s[],int n){ /* ... */ } unsigned strlen(char s[]){ /* ... */ } void strcopy(char cél[], char forrás[]){ /* ... */ } void strct(char s1[], char s2[]){ /* ... */ } void strup(char s[]){/* ... */ } void main(void){ int n; /* A bemeneti sor hossza. */ char sor[SOR+1], /* A bemeneti sor. */ egy[SOR*2]; /* Az egyesített karakterlánc. */ printf( "Adjon meg egy sort!\n" "Nagybetssé alakítva megjelentetjük\n" "ELEJE: :VÉGE szövegekbe zárva.\n" "Közöljük az eredeti és a végs hosszt is.\n"); n=getline(sor,SOR); strcopy(egy, "ELEJE:"); strct(egy, sor); strup(egy); strct(egy, ":VÉGE"); printf("%s\nEredeti hossz:%3d\nMostani hossz:%3d\n", egy, n, strlen(egy)); }

Megoldandó feladatok: Készítse el a következ függvényeket, és próbálja is ki ket egy rövid programmal! · A void strlw(char s[]) kisbetssé alakítja saját helyén az s karakterláncot. · Az int toup(int c) nagybetssé alakítva visszaadja c értékét. · Az int tolw(int c) visszatérési értéke c kisbetssé alakítva. · A void chdel(char s[], int c) kitörli az s karakterláncból a saját helyén a benne elforduló c karaktereket. Másoljuk át az s karakterláncot egy segéd tömbbe úgy, hogy az aktuális karakter csak akkor kerüljön át, ha nem c! Az eredmény karakterlánc aztán visszamásolandó s­be! Sokkal jobb, ha a feladatot segéd tömb nélkül oldjuk meg, s a másolást a c­k kihagyásával rögtön az s tömbbe végezzük. 4.5 Deklaráció A deklaráció megteremti a kapcsolatot az azonosító és az objektum között, ill. rögzíti például többek között az objektum adattípusát. Kétféle deklarációról beszélhetünk:

C programnyelv

71

· A definíciós deklarációval (definíció) helyet foglaltatunk az objektumnak a memóriában, és esetleg kezdértékkel is inicializáljuk. Ilyen deklaráció egy azonosítóra csak egyetlen egy létezhet az egész forrásprogramban. · A referencia deklaráció (deklaráció) nem foglal memóriát az objektumnak, de tudatja a fordítóval, hogy van egy ilyen azonosítójú, adattípusú, stb. objektum a programban. Ebbl a deklarációból egymásnak ellent nem mondóan - több is létezhet ugyanarra az objektumra. · Szólnunk kell még deklarációs pontról! Ez az azonosító deklarációjának helye a forrásprogramban. Az azonosító deklarációs pontja eltt legálisan nem érhet el a forráskódban. Tudjuk azt is, hogy a deklaráció elhelyezése és maga a deklaráció meghatározza az objektum attribútumait: · típus, · tárolási osztály, · hatáskör, · élettartam stb. A kísérleti definíció fogalmát az ANSI szabvány vezette be. Bármely küls adatdeklaráció, melynek nincs tárolási osztály specifikátora és nincs explicit inicializátora, kísérleti definíciónak tekintend. Ha a deklarált azonosító aztán feltnik egy késbbi definícióban, akkor a kísérleti definíciót extern tárolási osztályúnak kell venni. Más szóval, a kísérleti definíció egyszer referencia deklaráció. Ha a fordítási egység végéig sem találkozunk definícióval a kísérleti definíciós azonosítóra, akkor a kísérleti definíció teljes definícióvá válik tiszta zérus kezdérték objektummal.
int int int int x; x; /* OK legális, hisz nem mondtunk újat. */ y; y = 4; /* OK. Most specifikáltuk, hogy y-t 4-re kell inicializálni. */ int z = 4; int z = 6; /* HIBÁS, mert mindkett inicializálna. */

A deklarálható objektumok a következk: · változók,

72 · függvények, · típusok, · típusok tömbjei, · enum konstansok és címkék.

TÍPUSOK ÉS KONSTANSOK

Deklarálhatók még a következ késbbi fejezetekben tárgyalt objektumok is: struktúra, unió és utasítás címkék, struktúra és uniótagok, valamint elfeldolgozó makrók. A deklarátor szintaktika rekurzivitása miatt egészen komplex deklarátorok is megengedettek. Ezt el szokás kerülni típusdefinícióval (typedef). A deklaráció metanyelvi leírása a következ:
deklaráció: deklaráció-specifikátorok deklaráció-specifikátorok: tárolási-osztály-specifikátor típusspecifikátor init-deklarátorlista: init-deklarátor init-deklarátorlista, init-deklarátor init-deklarátor: deklarátor deklarátor=inicializátor inicializátor: hozzárendelés-kifejezés {inicializátorlista} {inicializátorlista ,} inicializátorlista: inicializátor inicializátorlista, inicializátor tárolási-osztály-specifikátor: auto register extern static typedef típusspecifikátor: void char short int long float double

C programnyelv
signed unsigned const volatile struktúra-vagy-unió-specifikátor enum-specifikátor typedef-név typedef-név: azonosító deklarátor: direkt-deklarátor direkt-deklarátor: azonosító (deklarátor) direkt-deklarátor [] direkt-deklarátor (paraméter-típus-lista) direkt-deklarátor () konstans-kifejezés: feltételes-kifejezés azonosítólista: azonosító azonosítólista, azonosító

73

A mutatókkal, a struktúrával, az unióval, a függvényekkel, az utasítás címkékkel és a makrókkal késbbi szakaszokban és fejezetekben foglalkozunk, így a vonatkozó metanyelvi leírások is ott találhatók. A tárolási-osztály-specifikátorokat teljes részletességgel majd egy késbbi fejezetben taglaljuk, de közülük kettrl (auto és extern) már szó volt a BEVEZETÉS ÉS ALAPISMERETEK szakaszban. A következ fejezet némi elzetes képet ad a typedef­rl. A típusspecifikátorokat alaptípusokra és típusmódosítókra bontottuk korábban. A típusmódosítók és alaptípusok megengedett, együttes használatát e szakasz elz fejezeteiben tisztáztuk. A const és a volatile viszont bármely alaptípussal és típusmódosítóval együtt szinte korlátozás nélkül használható. A deklarátor egyedi azonosítót határoz meg. Mikor az azonosító megjelenik egy vele egyeztípusú kifejezésben, akkor a vele elnevezett objektum értékét eredményezi. A konstans-kifejezés mindig fordítási idben is meghatározható konstans értékké értékelhet ki, azaz értéke bele kell hogy férjen a típus ábrázolási határaiba. A konstans kifejezés bárhol alkalmazható, ahol konstans egyébként használható. A konstans kifejezés operandusai lehetnek egész konstansok, karakter konstansok, lebegpontos konstansok, enumeráto-

74

TÍPUSOK ÉS KONSTANSOK

rok, típusmódosító szerkezetek, sizeof kifejezések, stb. A konstans kifejezés azonban a sizeof operátor operandusától eltekintve nem tartalmazhatja a következ operátorok egyikét sem: · hozzárendelés, · vessz, · dekrementálás, · inkrementálás és · függvényhívás. 4.5.1 Elemi típusdefiníció (typedef) Nem tartozik igazán ide a típusdefiníció tárgyalása, de miután a typedef kulcsszót a tárolási osztály specifikátor helyére kell írni, itt adunk róla egy rövid ismertetést. A typedef kulcsszóval nem új adattípust, hanem új adattípus specifikátort definiálunk. Legyen szó például az
auto long int brigi;

definícióról, mely szerint a brigi egy 32 bites, signed long int típusú, lokális élettartamú objektum azonosítója. Használjuk most az auto kulcsszó helyett a typedef-et, és az azonosítót írjuk át nagybetsre!
typedef long int BRIGI;

, ahol a BRIGI azonosító nem képez futásidej objektumot, hanem egy új típusspecifikátor csak. A programban ezután a BRIGI típusspecifikátorként alkalmazható deklarációkban. Például az
extern BRIGI fizetni;

ugyanolyan hatású, mint az
extern long int fizetni;

Ez az egyszer példa megoldható lenne a
#define BRIGI long

módon is, a typedef-fel azonban az egyszer szöveghelyettesítésnél komplexebb alkalmazások is áthidalhatók. A typedef nem hoz létre új típust tulajdonképpen, csak létez típusokra kreálható vele új kulcsszó. Komplexebb deklarációk egyszersítésére való.

C programnyelv

75

A típusdefiníció ,,megbonyolítására" a késbbiekben még visszatérünk! Meg kell jegyeznünk azonban annyit, hogy a typedef-fel létrehozott típusspecifikátor nem használható a deklarációban más típusspecifikátorokkal együtt! Legfeljebb a const és a volatile módosítók alkalmazhatók rá! Például
unsigned BRIGI keresni; const BRIGI kaba = 2; /* HIBÁS. */ /* OK */

76

MVELETEK ÉS KIFEJEZÉSEK

5 MVELETEK ÉS KIFEJEZÉSEK
A mveleteket a nyelv operátorokkal (mveleti jelekkel) valósítja meg. A mveletek lehetnek: · Egyoperandusosak. Alakjuk ,,operátor operandus", ahol az operandus az a kifejezés, melyen az egyoperandusos mveletet el kell végezni. Például: ­6, vagy a sizeof(int) stb. · Kétoperandusosak, azaz ,,operandus1 operátor operandus2" formájúak. Például: a + b. · Háromoperandusosak: A C-ben egyetlen ilyen mvelet van, az ún. feltételes kifejezés. Például: (a > b) ? a : b értéke a, ha a > b és b máskülönben.
operátor: (a következk egyike!) [ ] ( ) . -> ++ -- & * + - ~ ! sizeof / % << >> < > <= >= == != = ^ | && || ?: *= /= += -= %= <<= >>= &= ^= |= ,

A kifejezés operátorok, operandusok (és elválasztó-jelek) sorozata, mely az alábbi tevékenységek valamilyen kombinációját valósítja meg: · Értéket számít ki. · Objektumot vagy függvényt ér el. · Mellékhatást generál. A kifejezésbeli operandusokat elsdleges kifejezésnek nevezik.
elsdleges-kifejezés: azonosító konstans karakterlánc (kifejezés) kifejezés: hozzárendelés-kifejezés kifejezés, hozzárendelés-kifejezés

A konstansokat, a karakterláncot tárgyaltuk a TÍPUSOK ÉS KONSTANSOK szakaszban, a hozzárendelés-kifejezést definiálni fogjuk a hozzárendelés operátoroknál. Az azonosító lehet bármilyen egész vagy lebegpontos típusú. Lehet enum, tömb, mutató, struktúra, unió, vagy függvény típusú. Lehet tehát: · változó azonosító beleértve az indexel operátort is, azaz az azonosító[kifejezés]-t is, és az unió, ill. a struktúratagokat, vagy

C programnyelv

77

· függvényhívás, azaz azonosító a függvényhívás operátorral ( azonosító() ), melynek típusa mindig a függvény által visszaadott érték típusa lesz. Összesítve: az azonosítónak balértéknek vagy függvényhívásnak kell lennie. A kifejezés kiértékelése bizonyos · konverziós, · csoportosító, · asszociatív és · prioritási (precedencia) szabályokat követ, mely függ · a használt operátoroktól, · a ( ) párok jelenlététl és · az operandusok adattípusától. A kifejezések különfélék lehetnek: · elsdleges kifejezés (primary), · utótag kifejezés (postfix), · egyoperandusos kifejezés (unary), · eltag kifejezés (cast), · hozzárendelés kifejezés stb. Figyeljük meg, hogy a kifejezések elnevezése - az elsdleges kifejezéstl eltekintve - a vele használatos operátorok szerint történik!
utótag-kifejezés: elsdleges-kifejezés utótag-kifejezés[kifejezés] utótag-kifejezés() utótag-kifejezés.azonosító utótag-kifejezés->azonosító utótag-kifejezés++ utótag-kifejezés-- kifejezéslista: hozzárendelés-kifejezés kifejezéslista , hozzárendelés-kifejezés

78

MVELETEK ÉS KIFEJEZÉSEK

egyoperandusos-kifejezés: utótag-kifejezés ++ egyoperandusos-kifejezés -- egyoperandusos-kifejezés egyoperandusos-operátor eltag-kifejezés sizeof(egyoperandusos-kifejezés) sizeof(típusnév) egyoperandusos-operátor: ( a következk egyike!) &*+-~! eltag-kifejezés: egyoperandusos-kifejezés (típusnév) eltag-kifejezés típusnév: típusspecifikátor-lista absztrakt-deklarátor: mutató direkt-absztrakt-deklarátor: (absztrakt-deklarátor) [] ()

A típusnév az adattípus típusneve. Szintaktikailag az adott típusú objektum olyan deklarációja, melybl hiányzik az objektum neve. A hozzárendelés-kifejezést, melyrl most csak annyit jegyzünk meg, hogy nem balérték, majd a hozzárendelési mveleteknél ismertetjük! 5.1 Aritmetikai mveletek (+, -, *, / és %) Közülük a legmagasabb prioritási szinten az egyoperandusos, jobbról balra köt eljel operátorok vannak. Létezik a
- eltag-kifejezés

és a szimmetria kedvéért a
+ eltag-kifejezés.

Az eljel operátort követ eltag kifejezésnek aritmetikai típusúnak kell lennie, s az eredmény az operandus értéke (+), ill. annak -1-szerese (-). A + mvelet egész operandusát egész­elléptetésnek (integral promotion) veti alá a fordító, s így az eredmény típusa az egész­elléptetés végrehajtása után képzett típus. A ­ mveletet megelzheti implicit típuskonverzió, és egész operandus esetén az eredmény az operandus értékének kettes komplemense. Az egész­elléptetéssel az implicit típuskonverzió kapcsán rögtön foglalkozunk!

C programnyelv

79

A többi aritmetikai operátor mind kétoperandusos, melyek közül a szorzás (*), az osztás (/) és a modulus (%) magasabb prioritási szinten van, mint az összeadás (+) és a kivonás (-). A szorzást, az osztást és a modulust multiplikatív operátoroknak, az összeadást és a kivonást additív operátoroknak is szokás nevezni. 5.1.1 Multiplikatív operátorok (*, / és %)

multiplikatív-kifejezés: eltag-kifejezés multiplikatív-kifejezés * eltag-kifejezés multiplikatív-kifejezés / eltag-kifejezés multiplikatív-kifejezés % eltag-kifejezés

Nézzük a mveletek pontos szabályait! · A multiplikatív operátorok mind balról jobbra csoportosítanak. · Mindhárom operátor operandusainak aritmetikai típusúaknak kell lenniük. A % operátor operandusai ráadásul csak egész típusúak lehetnek. · Ha az operandusok különböz aritmetikai típusúak, akkor a mvelet elvégzése eltt implicit konverziót hajt végre a fordító. Az eredmény típusa ilyenkor a konvertált típus. Miután a konverziónak nincsenek túl vagy alulcsordulási feltételei, értékvesztés következhet be, ha az eredmény nem fér el a konverzió utáni típusban. · A / és a % második operandusa nem lehet zérusérték, mert ez fordítási vagy futásidej hibához vezet. · Ha a / és a % mindkét operandusa egész, de a hányados nem lenne az, akkor: · Ha a két operandus - mondjuk op1 és op2 - értéke azonos eljel vagy unsigned, akkor az op1/op2 hányados az a legnagyobb egész, ami kisebb, mint az igazi hányados és az op1%op2 osztási maradék op1 eljelét örökli meg:
3 % 2 1 (-3) % (-2) -1

3 / 2 1 (-3) / (-2) 1

·

Ha op1 és op2 ellenkez eljel, akkor az op1/op2 hányados az a legkisebb egész, ami nagyobb az igazi hányadosnál. Az op1%op2 osztási maradék most is op1 eljelét örökli meg:
(-3) / 2 -1 3 / (-2) -1 (-3) % 2 -1 3 % (-2) 1

80

MVELETEK ÉS KIFEJEZÉSEK

Készítsünk programot, ami beolvas egy négyjegy évszámot, és eldönti róla, hogy szökév­e, vagy sem! A Gergely­naptár szerint szökév minden, néggyel maradék nélkül osztható év. Nem szökév a kerek évszázad, de a 400­zal maradék nélkül oszthatók mégis azok. 1. Olvassunk be a szabvány bemenetrl egy maximálisan négy karakteres sort! 2. Ha a bejött karakterlánc hossza nem pontosan négy, akkor kérjük be újra! 3. Ellenrizzük le, hogy a karakterlánc minden pozíciója numerikus­e! Ha nem, újra bekérend. Írjunk int nume(char s[]) függvényt, mely 1­et (igazat) ad vissza, ha a paraméter karakterlánc tiszta numerikus, és zérust (hamisat), ha nem!
int nume(char s[]){ int i; for(i=0; s[i]; ++i) if(s[i]<'0'||s[i]>'9') return 0; return 1; }

4. Át kéne konvertálni a numerikus karakterláncot fixpontos belsábrázolású egésszé (int n­né)! A módszer a következ:
n=(s[0]-'0')*1000+(s[1]-'0')*100+(s[2]-'0')*10+ (s[3]-'0');

Ezt ugye ciklusban, ahol i és n zérustól indul, és i egyesével haladva végigjárja a numerikus karakterláncot, így kéne csinálni:
n=n*10+(s[i]-'0');

Írjunk int atoi(char s[]) függvényt, mely megvalósítja ezt a konverziót, s n lesz a visszaadott értéke! Az átalakítást végezze az els nem konvertálható karakterig! Engedjük meg, hogy a numerikus karakterlánc elején fehér karakterek és eljel is lehessen! Ha az eljelet elhagyják, akkor legyen a szám pozitív!
int atoi(char s[]){ int i=0, n=0; int elojel=1; /* Alapértelmezés: pozitív. */ /* A karakterlánc eleji fehér karakterek átlépése: */ while(s[i]==' '||s[i]=='\n'||s[i]=='\t') ++i; /* Eljel: */ if(s[i]=='+'||s[i]=='-') if(s[i++]=='-') elojel=-1; /* Konverzió: */ for(;s[i]>='0'&&s[i]<='9';++i) n=10*n+s[i]-'0'; return(elojel*n); }

C programnyelv

81

Megemlítend, hogy pontosan ilyen prototípusú, nev és funkciójú függvény létezik a szabvány könyvtárban is, de bekapcsolandó hozzá a szabványos STDLIB.H fejfájl. A könyvtárban van atol rutin, mely long­ gá, és van atof, mely double­lé alakítja numerikus karakterlánc paraméterét. Jöhet a program, de helyszke miatt a függvények definícióit nem ismételjük meg!
/* PELDA14.C: A négyjegy évszám szökév-e? */ #include #define MAX 80 int getline(char s[], int n); int nume(char s[]); int atoi(char s[]); void main(void){ int ev = 0; /* Elfogadhatatlan értékrl indul. */ char s[MAX+1]; /* Az input puffer. */ printf("A négyjegy évszám szökév-e?\n"); while(ev<1000 || ev>9999){ printf("Adjon meg egy évszámot!\n"); if(getline(s,4)==4&&nume(s)) ev=atoi(s); else printf("Formailag hibás bemenet!\n"); } if(ev%4==0&&ev%100!=0||ev%400==0) printf("%4d szökév.\n", ev); else printf("%4d nem szökév.\n", ev); }

Megoldandó feladatok: Készítsen számot leíró karakterláncok formai ellenrzését végz függvényeket az atoi alapján, melyek helyes esetben 1­et (igaz) adnak vissza, és a hibát zérussal (hamis) jelzik! A lánc eleji fehér karaktereket át kell lépni. A szám végét a karakterlánc vége, vagy újabb fehér karakter következése mutatja. · Az int egesze(char s[]) a decimális egész konstans írásszabályát ellenrzi a paraméter karaktertömbön. · Az int hexae(char s[]) megvizsgálja, hogy paramétere hexadecimális szám­e. · Az int ell210e(char s[]) teszteli, hogy s 2 és 10 közötti alapú szám­ e. A számrendszer alapját fordítási idben változtatni (#define) lehet! · Az int ellae(char s[], int alap) ugyanazt teszi, mint az ell210e, de a 2 és 10 közötti alapot futási idben paraméterként kapja meg.

82

MVELETEK ÉS KIFEJEZÉSEK

· Az int ella36e(char s[], int alap) egyezik ellae­vel, de az alap 2 és 36 közötti lehet. A tíznél nagyobb alapú számrendszerek esetében a számjegyeket az angol ábécé betivel jelöljük rendre, vagyis 10=A, 11=B stb. A 36­os korlátozás ebbl fakad. Készítsen konverziós függvényeket is a leellenrzött karakterláncokra az atoi mintájára, és a konvertált érték legyen a rutinok visszatérési értéke! · A double atofix(char s[]) az eljeles, legfeljebb egész és tört részbl álló valós értéket alakítja double­lé. · A long atoh(char s[]) hexadecimális karakterláncot alakít egésszé. · A long ato36(char s[], int alap) a legfeljebb 36 alapú számrendszerbeli láncot konvertálja egésszé. 5.1.2 Additív operátorok (+ és -)

additív-kifejezés: multiplikatív-kifejezés additív-kifejezés + multiplikatív-kifejezés additív-kifejezés - multiplikatív-kifejezés

Az additív operátorok csoportosítása is balról jobbra történik. Operandusaik az aritmetikai értékeken túl mutatók is lehetnek. A mutatóaritmetikát majd a mutatók kapcsán ismertetjük! Aritmetikai operandusok esetén az eredmény a két operandus értékének összege (+), ill. különbsége (­). Egész vagy lebegpontos operanduson a mvelet implicit típuskonverziót is végezhet, ha szükséges. Ilyenkor az eredmény típusa a konvertált típus. Miután a konverziónak nincsenek túl vagy alulcsordulási feltételei, értékvesztés következhet be, ha az eredmény nem fér el a konverzió utáni típusban. 5.1.3 Matematikai függvények A matematikai függvények nem részei a C nyelvnek. Nyilvánvaló viszont, hogy kifejezések képzésekor szükség lehet rájuk. A C filozófiája szerint a matematikai függvények családját is a szabvány könyvtárban kell elhelyezni, mint ahogyan a szabvány bemenet és kimenet kezelését végz rutinokat. Az ANSI szabvány pontosan rögzíti ezeket a könyvtári funkciókat, így bármilyen szabványos C fordító és operációs rendszer számára kompatibi-

C programnyelv

83

lis formában létezniük kell. Magyarán: azok a programok, melyek az operációs rendszerrel való kapcsolatukat a szabvány könyvtáron át valósítják meg, minden változtatás nélkül átvihetk az egyik számítógéprl a másikra, az egyik operációs rendszerbl a másikba. Ezek az úgy nevezett portábilis programok. A szabvány könyvtár függvényeit, típusait és makróit szabványos fejfájlokban deklarálták. Ezek közül néhánnyal már találkoztunk, másokkal meg még nem: ASSERT.H CTYPE.H LIMITS.H TIME.H ERRNO.H FLOAT.H STDLIB.H ISO646.H STRING.H LOCALE.H MATH.H WCHAR.H WCTYPE.H SETJMP.H SIGNAL.H

STDARG.H STDDEF.H STDIO.H

A matematikai függvények prototípusai a MATH.H fejfájlban helyezkednek el, így használatuk eltt ez a fejfájl bekapcsolandó!
#include

Nem kívánjuk felsorolni és részletezni az összes fejfájlt, az összes függvényt, csak néhány fontosabbat említünk meg közülük. Az olvasótól azonban elvárjuk, hogy a programfejleszt rendszere segítségébl a további fejfájlokról és rutinokról is tájékozódjék. A matematikai függvények double értéket szolgáltatnak, s néhány kivételtl eltekintve, paramétereik is double típusúak. A matematikából ismeretes korlátozások természetesen érvényben maradnak rájuk. A trigonometrikus függvények paramétere, ill. inverzeik visszaadott értéke radiánban értend. Néhányat felsorolunk a teljesség igénye nélkül! sin(x) cos(x) tan(x) asin(x) acos(x) atan(x) exp(x) log(x) x szinusza. x koszinusza. x tangense. ­1<=x<=1 árkusz szinusza. Az értékkészlet: [­u/2, u/2]. ­1<=x<=1 árkusz koszinusza. Az értékkészlet: [0, u]. x árkusz tangense. Az értékkészlet: [­u/2, u/2]. Az ex exponenciális függvény. x>0 természetes alapú logaritmusa. (ln(x)).

84 log10(x) pow(x, y) sqrt(x) floor(x) fabs(x) fmod(x, y)

MVELETEK ÉS KIFEJEZÉSEK x>0 tízes alapú logaritmusa. (lg(x)). Az xy hatványfüggvény. Hiba, ha x=0 és y<=0, ill. ha x<0 és y értéke nem egész. x>=0 négyzetgyöke. Az x­nél nem nagyobb, legnagyobb egész szám. Az x abszolút értéke. y!=0 estén x/y osztás lebegpontos maradéka, mely x­szel egyez eljel.

A szabvány könyvtári függvények, így a matematikaiak is, a hibát úgy jelzik, hogy valamilyen speciális értéket (HUGE_VAL, zérus stb.) adnak vissza, és beállítják a UNIX­tól örökölt, globális
extern int errno;

(hibaszám) változót a hiba kódjára. A hibakódok az ERRNO.H fejfájlban definiált, egész, nem zérusérték szimbolikus állandók. A HUGE_VAL a legnagyobb, pozitív, még ábrázolható double érték. A matematikai rutinok az értelmezési tartomány hibát EDOM érték errno­val, és a fordítótól is függ függvény visszatérési értékkel jelzik. Értékkészlet probléma esetén az errno ERANGE. A függvény visszatérési érték túlcsorduláskor eljel helyes HUGE_VAL, ill. alulcsorduláskor zérus. Az értelmezési tartomány hiba akkor fordul el, ha a függvény aktuális paraméterének értéke nincs benn az értelmezési tartományban. Értékkészlet hiba egyértelmen az, ha az eredmény nem ábrázolható double értékként. Például az sqrt(­1.) hatására az errno EDOM, és a visszakapott érték negatív HUGE_VAL. Megoldandó feladatok: Készítend a középiskolás függvénytáblázatok mintájára lapozhatóan: · egy logaritmustábla és · egy szinusztábla. 5.2 Reláció operátorok ( >, >=, <, <=, == és !=) A reláció operátorok prioritása - eltekintve az egyoperandusos mveletektl - az aritmetikai és a logikai operátorok között helyezkedik el. A

C programnyelv

85

reláció operátorok két prioritási szintet képeznek, ahol az ,,igazi" relációk (>, >=, < és <=) prioritása magasabb az egyenlségi relációkénál (== és !=). Az összes reláció az els operandus értékét hasonlítja a másodikéhoz, és a reláció érvényességét vizsgálja. Az eredmény logikai érték (int típusú), mely 1, ha a reláció igaz és 0, ha nem. A definíciók:
relációs-kifejezés: eltolás-kifejezés relációs-kifejezés < eltolás-kifejezés relációs-kifejezés > eltolás-kifejezés relációs-kifejezés <= eltolás-kifejezés relációs-kifejezés >= eltolás-kifejezés egyenlségi-kifejezés: relációs-kifejezés egyenlségi-kifejezés == relációs-kifejezés egyenlségi-kifejezés != relációs-kifejezés

Az eltolás-kifejezést a bitenkénti eltolás operátoroknál definiáljuk! A relációk operandusai egész, lebegpontos, vagy mutató típusúak. Az operandusok típusa különbözhet. Az operátorok implicit típuskonverziót is végrehajthatnak aritmetikai operandusaikon a mvelet elvégzése eltt. Ne feledjük, hogy a
kifejezés != 0

reláció mindig rövidíthet
kifejezés

módon, mert a nyelvben a nem zérus érték logikai igaznak minsül. Példaként tekintsük meg újra a korábbi szakaszokban ismertetett atoi és getline függvényeket! 5.3 Logikai mveletek ( !, && és ||) Közülük a legmagasabb prioritási szinten az egyoperandusos, jobbról balra köt, logikai nem operátor van, melynek alakja:
! eltag-kifejezés

, ahol az eltag-kifejezés operandusnak egész, lebegpontos, vagy mutató típusúnak kell lennie. Az eredmény mindenképpen int típusú, s az operandus logikai negációja. Az eredmény 0, ha az operandus értéke nem zérus, ill. 1, ha az operandus értéke zérus. Ez utóbbi mondatrész biztosítja, hogy a
kifejezés == 0

mindenkor rövidíthet

86
! kifejezés

MVELETEK ÉS KIFEJEZÉSEK

módon. Például a multiplikatív operátoroknál ismertetett program részlet
if( ev%4 == 0 && ev%100 != 0 || ev%400 == 0)

utasítása így rövidíthet:
if( !(ev%4) && ev%100 || !(ev%400))

Két kétoperandusos logikai mvelet van a nyelvben a logikai és (&&) és a logikai vagy (||), melyek prioritása alacsonyabb a relációkénál és a bit szint mveleteknél. A logikai és prioritása ráadásul magasabb, mint a logikai vagyé. Mindkét mvelet balról jobbra csoportosít. Egyik operátor sem hajt végre implicit típuskonverziót operandusain, ehelyett zérushoz viszonyítva értékeli ki ket. Az eredmény int típusú (1 - igaz és 0 - hamis).
logikai-és-kifejezés: vagy-kifejezés logikai-és-kifejezés && vagy-kifejezés logikai-vagy-kifejezés: logikai-és-kifejezés logikai-vagy-kifejezés || logikai-és-kifejezés

A vagy-kifejezés definícióját a bit szint mveleteknél találjuk meg! A K1&&K2 kifejezés eredménye igaz (1), ha K1 és K2 egyike sem zérus. A K1||K2 kifejezés igaz (1), ha K1 és K2 valamelyike is nem zérus. Máskülönben K1&&K2 és K1||K2 eredménye hamis (0). Mindkét operátor esetében garantált a balról jobbra történ végrehajtás. Elször K1­et értékeli ki a fordító az esetleges összes mellékhatásával együtt, de: · K1&&K2 esetén, ha K1 zérus, az eredmény hamis (0), és K2 kiértékelése nem történik meg. · K1||K2 kifejezésnél, ha K1 nem zérus, az eredmény igaz (1) lesz, és K2 kiértékelése itt sem zajlik le. Ha valami elbbre való, vagy mindenképp szeretnénk, hogy megtörténjen, akkor azt a bal oldali operandusba kell beépíteni. Például a PELDA10.C­ben megírt getline for ciklusának feltétele nem véletlenül
i
sorrend, hisz elször azt kell biztosítani, hogy a paraméter karaktertömböt ne írhassa túl a függvény. Ez nem kerülhet hátrébb a kifejezésben. Aztán a következ karaktert elbb be kell olvasni a bemenetrl, de min-

C programnyelv

87

mindennek vége van fájlvég esetén. Itt sincs értelme a felcserélésnek, mert felesleges vizsgálgatni a fájlvéget, hogy soremelés­e. A relációk közti és mveletek miatt látszik, hogy balról jobbra történik az operandusok kiértékelése, és ha eközben az egyik hamis lesz, teljesen felesleges lenne továbbfolytatni a kiértékelést. 5.4 Implicit típuskonverzió és egész­elléptetés Ha kétoperandusos (például aritmetikai) mveleteknél különbözik a két operandus típusa, akkor a mvelet elvégzése eltt a fordító bels konverziót (átalakítást) hajt végre. Általában a pontosabb operandus típusára konvertálja a másikat. A kétoperandusos mvelet eredményének típusa a konvertált típus lesz. Ezt a fajta konverziót szabványosnak, szokásosnak is nevezik. A szabályok nem prioritási sorrendben a következk: 1. Ha az egyik operandus típusa long double, akkor a másikat is long double típusúvá konvertálja a fordító. 2. Ha az elz pont nem teljesedik, s az egyik operandus double, akkor a másik is az lesz. 3. Ha az elz két feltétel egyike sem valósul meg, és az egyik operandus float, akkor a másikat is azzá konvertálja a fordító. 4. Ha az elz három feltétel egyike sem teljesül (egyik operandus sem lebegpontos!), akkor egész­elléptetést hajt végre a fordító az operandusok értékén, ha szükséges, és aztán: · Ha az egyik operandus unsigned long, akkor a másik is azzá alakul. · Ha az elz pont nem teljesül, és az egyik operandus long, a másik pedig unsigned int, akkor mindkét operandus értékét long, vagy unsigned long típusúvá konvertálja a fordító. Ha az unsigned int teljes értéktartománya ábrázolható long­ként, akkor a választás long, máskülönben pedig unsigned long. Ha az int 16, s a long 32 bites, akkor ­1L<1U. Hisz az elmondottak szerint az unsigned int long­gá alakul, s ­1L<1L. Ha az int 32 bites, akkor ­1L>1UL, mert a ­1L 111111111111111111111111111111112 binárisan, unsigned long­gá alakítva ugyanez marad, és ez sokkal nagyobb 000000000000000000000000000000012­nél. · Ha az elz pontok nem teljesülnek, és az egyik operandus long, akkor a másik is az lesz. · Ha nem teljesülnek az elz pontok, és az egyik operandus unsigned int, akkor a másik is azzá alakul.

88

MVELETEK ÉS KIFEJEZÉSEK

· Ha az elz pontok nem teljesülnek, akkor minkét operandus int az érvénybe lépett az egész­elléptetés (integral promotion) miatt. A signed vagy unsigned char, short int, vagy bitmez objektumok, ill. az enum típusúak használhatók kifejezésben ott, ahol bennük egész állhat. Ha az eredeti típus minden lehetséges értékét int képes reprezentálni, akkor az értéket int típusúvá konvertálja a fordító, máskülönben unsigned int-té. Az egész­elléptetési folyamat garantálja, hogy a konverzió eltti és utáni érték ugyanaz marad. A konvertált érték unsigned eredeti típusból 0X00 feltöltéssel, signed eredeti típusból viszont eljel kiterjesztéssel készül a fels bájtokba. Típus char Konvertálva int Módszer Az alapértelmezett char típustól függen eljel kiterjesztés van (signed) vagy 0X00 kerül a magasabb helyiérték bájt(ok)ba (unsigned). A fels bájt(ok) 0X00 feltöltések. Eljel kiterjesztés van a fels bájt(ok)ba. Ugyanaz az érték eljel kiterjesztéssel. Ugyanaz az érték.

unsigned char signed char short int enum

int int int int

unsigned short unsigned int Ugyanaz az érték 0X00 feltöltéssel.

12. ábra: Egész­elléptetés

Ne feledjük azonban el, hogy a konverzió mindig függvényhívást jelent (gépid!), azaz szükségtelenül ne alkalmazzuk! Csak ,,értelmes" típuskonverziókat valósít meg a fordító. Például az f + i összeadás végrehajtása eltt - feltéve, hogy f float és i int típusú - i értéke (és nem i maga!) float-tá alakul. Az ,,értelmetlen" lebegpontos kifejezés indexben még csak megvalósul úgy, hogy a kifejezés értéke tört részét levágja a fordító
#include void main(void){ int t[] = {2,3,4,5,6,7}; float f=1.75; printf("%d\n",t[f]); }

, azaz 3 lesz az eredmény.

C programnyelv

89

Numerikus karakterlánc azonban sohasem alakul automatikusan aritmetikai értékké. Ehhez valamilyen konverziós függvényt kell használni. Az STDLIB.H­beli atoi­ról, atol­ról és atof­ról volt már szó! 5.5 Típusmódosító szerkezet Az implicit (szokásos, szabványos stb.) konverziókon túl magunk is kikényszeríthetünk (explicit) típuskonverziót a
(típusnév) eltag-kifejezés

alakú, a BEVEZETÉS ÉS ALAPISMERETEK szakaszban megismert típusmódosító szerkezet alkalmazásával. Látjuk, hogy a típusmódosító szerkezet egyoperandusos, s ez által magas prioritású mvelet. A definícióban a típusnév a céltípus, és az eltag-kifejezés értékét erre a típusra kell konvertálni. Az eltag-kifejezést úgy konvertálja a fordító, mintha az értéket egy típusnév típusú változó venné fel. Az explicit típuskonverzió tehát a hozzárendelési konverzió szabályait követi. A legális típusmódosítások: Céltípus egész void Potenciális források bármilyen egész vagy lebegpontos típus, vagy mutató bármilyen típus

lebegpontos bármilyen aritmetikai típus Példaként vegyük a matematikai függvények közül a négyzetgyököt, azaz:
#include double sqrt(double x);

Programunkban van egy int típusú n változó, akkor az n+26 pozitív gyökét az
sqrt(double(n+26))

függvényhívással kaphatjuk meg. Bármilyen azonosító, vagy kifejezés típusa módosítható void­dá. A típusmódosításnak alávetett azonosító, vagy kifejezés nem lehet azonban void. A void függvény hívását például hiába típusmódosítjuk int-re, a semmibl nem lehet egészet csinálni. A void­dá módosított kifejezés értéke nem képezheti hozzárendelés tárgyát. Hasonlóan az explicit típusmódosítás eredménye nem fogadható el balértékként hozzárendelésben.

90

MVELETEK ÉS KIFEJEZÉSEK

Kifejezést csak olyan helyen módosíthatunk void-dá, ahol az értékére nincs szükség. Például nincs szükség a bejöv gombnyomásra:
printf("A folytatáshoz üssön Entert-t! "); (void)getchar();

5.6 sizeof operátor A BEVEZETÉS ÉS ALAPISMERETEK szakaszból ismert sizeof egyoperandusos, jobbról balra köt, magas prioritású mvelet, mely mindig az operandusa tárolásához szükséges memória mennyiségét szolgáltatja bájtban. Az eredmény size_t típusú egész érték. Az STDDEF.H fejfájlban megtekintve a típust többnyire azt látjuk, hogy unsigned int. típus értelmezése fordítótól függ tulajdonképpen! A size_t Két különböz alakja van az operátornak:
sizeof(egyoperandusos-kifejezés) sizeof(típusnév)

sizeof(egyoperandusos-kifejezés) esetén az egyoperandusos kifejezés típusát a fordító a kifejezés kiértékelése nélkül határozza meg, azaz ha az operandus tömbazonosító, az egész tömb bájtokban mért helyfoglalásához jutunk. Például a tomb tömb elemszáma a következ konstrukcióval állapítható meg:
sizeof(tomb) / sizeof(tomb[0])

A sizeof nem használható függvényre, vagy nem teljes típusú kifejezésre, ilyen típusok zárójelbe tett nevére, vagy olyan balértékre, mely bitmez objektumot jelöl ki. A sizeof azonban bátran alkalmazható elfeldolgozó direktívákban is!
#define MERET sizeof(int)*3

5.7 Inkrementálás (++), dekrementálás (--) és mellékhatás Ezek az operátorok mind egyoperandusosak, s ezért magas prioritásúak. Mindkét operátor létezik utótag
utótag-kifejezés++ utótag-kifejezés--

és eltag mveletként:
++ egyoperandusos-kifejezés -- egyoperandusos-kifejezés

C programnyelv

91

Az inkrementáló és dekrementáló kifejezésben az utótag- vagy az egyoperandusos-kifejezésnek skalár (aritmetikai vagy mutató) típusúnak és módosítható balértéknek kell lenniük, de az eredmény nem balérték. Az inkrementálásnál (++) a balérték eggyel nagyobb, dekrementálásnál (--) viszont eggyel kisebb lesz. Eltag operátornál a ,,konstrukció" értéke egyezik az új balértékkel, míg utótag operátornál a ,,konstrukció" értéke az inkrementálás vagy dekrementálás végrehajtása eltti érték. Az eredmény típusát az operandus típusa határozza meg. Például:
int x = x = x = x, i = 3, j = 4, n = 5; n++; /* x == 5 és n == 6 */ ++n; /* x == 7 és n == 7 */ --( n - j + i +6); /* x == 11 */

A kifejezés produkálhat · balértéket, · jobbértéket vagy · nem szolgáltat értéket. A kifejezésnek ezen kívül lehet mellékhatása is. Például a TÍPUSOK ÉS KONSTANSOK szakaszban megírt strcopy záró sorában
while(cél[i++]=forrás[i])

a hozzárendelés mellékhatásaként az i végrehajtás utáni értéke is eggyel nagyobb lesz. A mellékhatást a kifejezés kiértékelése okozza, s akkor következik be, ha megváltozik egy változó értéke. Minden hozzárendelés operátornak van mellékhatása. Láttuk, hogy a balértékre alkalmazott inkrementálási és dekrementálási mveletnek is van. Függvényhívásnak is lehet azonban mellékhatása, ha globális hatáskör objektum értékét változtatja meg. Írjuk meg a void chdel(char s[], int c)­t, mely saját helyen törli a benne elforduló c karaktereket az s karakterláncból! Itt is másolni kell a forrásból a célba bájtról­bájtra haladva, de a c érték karaktereket ki kell ebbl hagyni. Két indexre van szükség. Az egyik az i, mely végigjárja a forrást. A másik a j, mely a célban mindig a következ szabad helyet éri el. A nem c érték karaktert a következ szabad helyre kell másolni, s a célbeli indexnek az ezután következ szabad helyre kell mutatnia.
void chdel(char s[], int c){ int i, j; for(i=j=0; s[i]; ++i) if(s[i] != c) s[j++] = s[i];

92
s[j] = 0; }

MVELETEK ÉS KIFEJEZÉSEK

Bit szint operátorok ( ~, <<, >>, &, ^ és |) A bit szint operátorok csak signed és unsigned egész típusú adatokra: char, short, int és long használhatók. Legmagasabb prioritási szinten az egyoperandusos, jobbról balra köt egyes komplemens operátor (~) van, melynek definíciója:
~ eltag-kifejezés

5.8

Az operátor elbb végrehajtja az egész­elléptetést, ha szükséges. Az eredmény típusa az operandus konverzió utáni típusa. Az eredmény maga a bit szint egyes komplemens, azaz ahol az operandus bit 1 volt, ott az eredmény bit 0 lesz, és ahol az operandus bit 0 volt, ott az eredmény bit 1 lesz. Feltéve, hogy az egész­elléptetés 16 bites, és hogy:
unsigned short x = 0XF872, maszk = 0XF0F0; /* 1111100001110010 */ /* 1111000011110000 */

, akkor a ~x 00000111100011012, és a ~maszk 00001111000011112. A balról jobbra csoportosító eltolás operátorok (<< és >>) prioritása alacsonyabb az aritmetikai mveletekénél, de magasabb, mint a reláció operátoroké. Az eltolás operátorok els operandusuk értékét balra (<<) vagy jobbra (>>) tolják annyi bitpozícióval, mint amennyit a második operandus meghatároz. A definíció a következ:
eltolás-kifejezés: additív-kifejezés eltolás-kifejezés << additív-kifejezés eltolás-kifejezés >> additív-kifejezés

A K1<>K2 kifejezések esetében minkét operandus egész típusú kell, legyen. Az operátorok egész­elléptetést is megvalósíthatnak. Az eredmény típusát K1 konvertált típusa határozza meg. Ha K2 negatív vagy értéke nem kisebb K1 bitszélességénél, akkor az eltolási mvelet eredménye határozatlan. Miután a C­ben nincs egész alul vagy túlcsordulás, a mveletek értékvesztést is okozhatnak, ha az eltolt eredmény nem fér el az els operandus konvertált típusában. A K1<
C programnyelv

93

A K1>>K2 mvelet K1 értékét K2 bitpozícióval tolja jobbra. Ha K1 valamilyen unsigned típusú, akkor balról 0 bitek jönnek be. Ha K1 signed, akkor az operátor az eljel bitet sokszorozza. unsigned, nem negatív K1 esetén a jobbra tolás K1/2K2 hányados egész részeként is interpretálható. Folytatva az egyes komplemensképzésnél megkezdett példát, az x<<2 11100001110010002, ill. a maszk>>5 00000111100001112. A bit szint logikai operátorok prioritásuk csökken sorrendjében az és (&), a kizáró vagy (^), valamint a vagy (|). A többi mveletre való tekintettel prioritásuk magasabb a kétoperandusos logikai operátorokénál, de alacsonyabb a relációkénál. Lássuk a definíciót!
és-kifejezés: egyenlségi-kifejezés és-kifejezés & egyenlségi-kifejezés kizáró-vagy-kifejezés: és-kifejezés kizáró-vagy-kifejezés ^ és-kifejezés vagy-kifejezés: kizáró-vagy-kifejezés vagy-kifejezés | kizáró-vagy-kifejezés

Az egyenlségi-kifejezés definíciója a relációknál megtalálható! Ha szükséges, akkor a mvelet elvégzése eltt a fordító implicit típuskonverziót hajt végre az egész típusú operandusok értékén. Az eredmény típusa az operandusok konverzió utáni típusa. A mvelet bitrl-bitre valósul meg az operandusok értékén, s egy bitre vonatkoztatva az eredmény így néz ki: K1 0 1 0 1 K2 0 0 1 1 K1 & K2 0 0 0 1 K1 ^ K2 0 1 1 0 K1 | K2 0 1 1 1

Befejezve az egyes komplemensnél megkezdett példát, az x|maszk értéke 11111000111100102. Állíthatjuk, hogy az eredményben minden olyan bit egy, ami a maszk­ban az volt. Az x^x eredménye biztosan tiszta zérus. Az x&~maszk értéke 00001000000000102. Megemlítjük, hogy az eredmény minden olyan bitpozícióján 0 van, ahol a maszk bitje 1. Tehát a kifejezés azokat a biteket bizonyosan törölte, ahol a maszk bit 1 volt.

94

MVELETEK ÉS KIFEJEZÉSEK

Nem szabad összekeverni a logikai vagy (||) és a bitenként vagy (|), ill. a logikai és (&&) és a bit szint és (&) mveleteket! Feltéve, hogy két, egész típusú változó értéke: a=2 és b=4, akkor az
a && b 1 (igaz)

de az
a & b 0

Az ANSI szabvány szerint a bit szint mveletek signed egészeken implementációfüggk. A legtöbb C fordító azonban signed egészeken ugyanúgy dolgozik, mint unsigned­eken. Például short int­ben gondolkodva a ­16 & 99 eredménye 96, mert:
1111111111110000&0000000001100011 0000000001100000

A fájl utolsó módosításának dátumát és idejét egy-egy szóban, azaz C nyelvi fogalmakkal: egy-egy unsigned short int-ben tároljuk. A két szó bitfelosztása legyen a következ: dátum id év ­ 1980 óra 15 14 13 12 11 10 9 8 hónap perc 7 6 5 4 3 nap 2 másodperc 2 1 0

A két szó azonosítója legyen datum és ido! Tételezzük fel, hogy az id részeit az ora, a perc és az mp unsigned short változókban tároljuk! Az ugyanilyen unsigned short változók a dátum részeire: ev, ho és nap. Akkor az oda­visszaalakítás a következ:
/* Részeibl az id szó elállítása: */ ido = ora << 11 | perc << 5 | mp >> 1; /* Az id szóból a részei: */ ora = ido >> 11; perc = ido >> 5 & 0X3F; mp = (ido & 0X1F) << 1; /* Részeibl a dátum szó elállítása: */ datum = ev - 1980 << 9 | ho << 5 | nap; /* A datum szóból a részei: */ ev = (datum >> 9) + 1980; ho = datum >> 5 & 0XF; nap = datum & 0X1F;

Írjunk unsigned long invertal(unsigned long x, int p, int n) függvényt egy rövid kipróbáló programmal, mely az x paramétere értékét a p.­ik bitpozíciójától n hosszban invertálja (az 1­eseket 0­kra, s a 0­kat 1­esekre cseréli)! A nem említett bitpozíciók értéke maradjon változatlan! Az invertált eredmény a függvény visszatérési értéke.

C programnyelv n
31 29 27 25 23 21p 19 17 15 13 11 9 7 5 3

95

1 0

Az ábra x paramétert, a zérustól induló, 20 érték p bitpozíciót és az n=5 bitszélességet szemlélteti. Készítsünk elbb egy ugyancsak unsigned long maszkot, mely p pozíciótól n szélességben 1­es biteket tartalmaz, és az ezen kívüli pozíciók mind nullák!
~0: ~0<
Ezután x egyes komplemensébl bitenkénti éssel kivágjuk a kérdéses invertált bitpozíciókat, azaz: ~x&maszk. Ezt aztán bitenkénti vagy kapcsolatba hozzuk az eredeti x egy olyan változatával, melyben kinulláztuk az érdekes biteket, azaz: x&~maszk. Tehát:
/* PELDA15.C: Bitpozíciók invertálása. */ #include unsigned long invertal(unsigned long x, int p, int n){ unsigned long maszk=~(~0<>1) if(maszk&x) putchar('1'); else putchar('0'); } void main(void){ unsigned long x=0X4C59E9FA; int p=20, n=5; printf("Az eredeti értéket: "); binaris(x); printf("\nEzt invertáljuk %d bitpozíciótól %d " "bitszélességben.\n", p, n); printf("Az invertált érték: "); binaris(invertal(x, p, n)); putchar('\n'); }

Vegyük észre, hogy a binaris függvény segítségével unsigned long értéket jelentetünk meg binárisan! A maszk változó 31­es bitpozíciójától indítunk egy 1­est, s a ciklusmag végrehajtása után mindig eggyel jobbra toljuk. A maszk és az érték bit szint és kapcsolata akkor szolgáltat nem zérust (igazat), ha a kérdéses bitpozíción az értékben 1 van. Megoldandó feladatok:

96

MVELETEK ÉS KIFEJEZÉSEK

Készítse el a következ függvényeket, és próbálja is ki ket egy rövid programmal! · Az unsigned long getbits(unsigned long x, int p, int n) x értékét szolgáltatja a p.­ik bitpozíciótól n bitszélességben. · Az unsigned long rotl(unsigned long x) 1 bittel balra forgatja paramétere értékét. Tehát a 31. pozícióról kicsorgó bit lesz az eredmény 0. bitje. · Az unsigned long rotr(unsigned long x) 1 bittel jobbra forgat. · Az unsigned long tobbrotl(unsigned long x, int n) n bittel forgat körbe balra. · Az unsigned long tobbrotr(unsigned long x, int n) n bittel forgat körbe jobbra. 5.9 Feltételes kifejezés ( ? :) Ez a nyelvben az egyetlen három operandusos mvelet, melynek prioritása alacsonyabb a kétoperandusos logikai operátorokénál. A definíció:
feltételes-kifejezés: logikai-vagy-kifejezés logikai-vagy-kifejezés ? kifejezés : feltételes-kifejezés kifejezés: hozzárendelés-kifejezés kifejezés , hozzárendelés-kifejezés

A logikai-vagy-kifejezésnek egész, lebegpontos vagy mutató típusúnak kell lennie, s kiértékelése zérushoz való hasonlítását jelenti: · Ha logikai-vagy-kifejezés nem zérus, a kifejezést értékeli ki a fordító. Ez azt jelenti, hogy a kifejezés kiértékelése csak akkor történik meg, ha a logikai-vagy-kifejezés igaz. · Ha logikai-vagy-kifejezés zérus, a feltételes-kifejezést határozza meg a fordító, azaz a feltételes-kifejezés kiértékelése csak akkor történik meg, ha a logikai-vagy-kifejezés hamis. A logikai-vagy-kifejezést mindenképpen kiértékeli a kód, de a kifejezés és a feltételes-kifejezés közül csak az egyik kiszámítása történik meg. A K1 ? K2 : K3 feltételes kifejezésben K1 értékétl függen K2­t vagy K3­at értékeli ki a fordító. A konstrukció eredményének típusa ilyen alapon K2 vagy K3 operandusok típusától függ a következképp:

C programnyelv

97

· Ha mindkett aritmetikai típusú, akkor az esetleg szükséges implicit típuskonverzió után az eredmény típusa a konvertált típus. · Ha a két operandus ugyanolyan struktúra, unió, vagy mutató típusú, akkor az eredmény is a közös típusú. · Ha mindkett void típusú, akkor az eredmény is az. Például az
if( a > b ) z = a; else z = b;

utasítás helyettesíthet a
z = a > b ? a : b;

feltételes kifejezéssel. Ha például egy int típusú a tömb els N elemét szeretnénk megjelentetni úgy, hogy egy sorba egymástól szóközzel elválasztva 10 elem kerüljön, akkor ezt ,,tömör" kódot alkalmazva így is megtehetjük:
for(i=0; i
Javítsunk ki néhány eddig megírt függvényt a feltételes kifejezés felhasználásával! · A PELDA15.C binaris függvényében az
if(maszk&x) putchar('1'); else putchar('0');

most így is írható:
putchar((maszk&x)? '1': '0');

· A PELDA14.C atoi rutinjában az
if(s[i]=='+'||s[i]=='-') if(s[i++]=='-') elojel=-1;

sor átírható a következre:
if(s[i]=='+'||s[i]=='-') elojel=(s[i++]=='-')? -1: 1;

Mindkét példában a feltételes kifejezés logikai-vagy-kifejezése köré zárójelet tettünk. Lássuk azonban be, hogy erre semmi szükség sincs, hisz ennél alacsonyabb prioritású mvelet már csak kett van: a hozzárendelés és a vessz operátor! A felesleges zárójel legfeljebb a jobban olvashatóságot biztosítja. 5.10 Hozzárendelés operátorok A jobbról balra csoportosító hozzárendelés prioritása alacsonyabb, mint a feltételes kifejezésé, s ennél alacsonyabb prioritású mvelet már csak a

98

MVELETEK ÉS KIFEJEZÉSEK

vessz operátor. A mvelet a jobb oldali operandus értékét rendeli a bal oldali operandushoz, melybl következleg a bal oldali operandusnak módosítható balértéknek kell lennie. A hozzárendelés kifejezés értéke ugyan egyezik a bal oldali operandus hozzárendelés végrehajtása utáni értékével, de nem balérték. A jobb oldali operandus értékét a fordító a bal oldali operandus típusára konvertálja a bal operandusba való letárolás eltt a hozzárendelési konverzió szabályai szerint. A bal oldali operandus nem lehet tömb, függvény vagy konstans. Nem lehet természetesen nem teljes (még nem teljesen deklarált) típusú sem. A definíció:
hozzárendelés-kifejezés: feltételes-kifejezés egyoperandusos-kifejezés hozzárendelés-operátor hozzárendelés-kifejzés hozzárendelés-operátor: ( a következk egyike!) = *= /= %= += -= &= ^= |= <<= >>=

Van tehát egyszer hozzárendelés operátor (=) és vannak összetettek vagy kombináltak (ezek a többiek). Foglalkozzunk elbb az egyszer hozzárendeléssel! A K1 = K2 kifejezésben K1-nek módosítható balértéknek kell lennie. K2 értéke - esetlegesen a K1 típusára történt konverzió után - felülírja a K1 által kijelölt objektum értékét. Az egész ,,konstrukció" értéke K2 értéke az esetleg a K1 típusára szükségessé vált hozzárendelési konverzió végrehajtása után. Reméljük, hogy nem felejtették még el, hogy a balérték (K1) és jobbérték (K2) fogalom éppen az egyszer hozzárendelésbl származik. A balérték a hozzárendelés operátor bal oldalán, a jobbérték pedig a jobb oldalán állhat. Tudjuk, ha egy kifejezésben hozzárendelés operátor is van, akkor annak a kifejezésnek bizonyosan van mellékhatása. Emlékezzünk vissza, hogy a definíció megengedi a hozzárendelés operátor K1 = K2 = K3 = ... = Kn = kifejezés formájú használatát is, amikor is a kifejezés kiértékelése után jobbról balra haladva az operandusok felveszik a kifejezés értékét. Az egész konstrukció értéke most is a kifejezés értéke lesz. Például
a = b= c = d + 6;

A kombinált hozzárendelés operátorok a K1 = K1 operátor K2

C programnyelv kifejezést K1 operátor = K2

99

módon rövidítik, és K1 kiértékelése csak egyszer történik meg. A megengedett operátorok a definícióban láthatók! Mindegyik megvalósítja azt a mveletet, konverziót és korlátozást, amit a kétoperandusos operátor egyébként realizál, és végrehajtja a hozzárendelést is. A kombinált hozzárendelés operátor operandusai egész vagy lebegpontos típusúak lehetnek általában. A += és ­= bal oldali operandusa mutató is lehet, amikor is a jobb oldali operandus köteles egész típusú lenni. Például:
x = x * ( y + 6); x *= y + 6;

Az összetett operátorokat használva kevesebbet kell írni. Például:
t[ t1[i3 + i4] + t2[i1 - i2]] += 56;

Használjunk kombinált hozzárendelés operátorokat néhány eddig már megírt függvényben! · A PELDA15.C binaris rutinja:
void binaris(unsigned long x){ unsigned long maszk; for(maszk=0X80000000; maszk; maszk>>=1) putchar((maszk&x)? '1': '0'); }

· A PELDA4.C­beli
for(ft=ALSO; ft<=FELSO; ft=ft+LEPES)

most így írható:
for(ft=ALSO; ft<=FELSO; ft+=LEPES)

5.11 Hozzárendelési konverzió Hozzárendelésnél a hozzárendelend érték típusát a hozzárendelést fogadó változó típusára konvertálja a fordító. A C megengedi a hozzárendelési konverziót lebegpontos és egész típusok között azzal, hogy a konverziónál értékvesztés történhet. A használatos konverziós módszer követi az implicit típuskonverzió szabályait és ezen túl még a következket: · Konverzió signed egész típusokról: Nem negatív signed egész nem kisebb méret unsigned egésszé alakításakor az érték változatlan. Például signed char unsigned char­rá válásakor a bitminta változatlan, de a legmagasabb helyiérték bit elveszti az eljelbit funkcióját. A konverzió különben a signed egész eljel kiterjesztésével

100

MVELETEK ÉS KIFEJEZÉSEK történik. Például signed char unsigned long­gá úgy válik, hogy eljel kiterjesztéssel elbb long lesz belle, s aztán ez a bitminta megtartásával, és eljelbit funkcióvesztéssel lesz unsigned long. Hosszabb egész típus rövidebbé alakulásakor az egész típus maradó alsó bitjei változatlanok, s a fölösleg egyszeren levágódik még akkor is, ha értékvesztés történne. long int érték float lebegpontossá alakításakor nincs értékvesztés, de pontosságvesztés lehet. Ilyen átalakításkor a rövidebb signed egészbl elbb long lesz eljel kiterjesztéssel, s csak ezután jön a lebegpontos konverzió.

Miután az enum típus int definíció szerint, a felsorolás típusra és típusról való konverzió egyezik az int­ével. · Konverzió unsigned egész típusokról: Rövidebb unsigned vagy signed egésszé alakításkor a maradó alsó bitek változatlanok, s a fölösleg egyszeren levágódik még akkor is, ha értékvesztés történik. Az eredmény legfels bitje felveszi az eljelbit funkciót, ha signed­ dé konvertálás volt. Hosszabb unsigned vagy signed egésszé alakításkor a bejöv magasabb helyiérték bitek nulla feltöltések. Lebegpontos konverziónál a rövidebb unsigned egészbl elbb long lesz nulla feltöltéssel, s csak ezután jön az igazi konverzió. Megállapíthatjuk itt is, hogy lehet pontosságvesztés float­tá alakításkor. · Konverzió lebegpontos típusokról: A rövidebb lebegpontos ábrázolás hosszabbá konvertálásakor nem változik meg az érték. A hosszabb lebegpontos ábrázolás float­tá alakítása is pontos, ha csak lehetséges. Pontosságvesztés is bekövetkezhet, ha értékes jegyek vesznek el a mantisszából. Ha azonban az eredmény a float ábrázolási korlátain kívül esne, akkor a viselkedés definiálatlan. Ez a definiálatlanság tulajdonképpen a fordítótól függ viselkedést takar! A lebegpontos értéket úgy konvertál egésszé a fordító, hogy levágja a törtrészt. Az eredmény elre megjósolhatatlan, ha a lebegpontos érték nem fér be az egész ábrázolási korlátaiba. Különösen definiálatlan a negatív lebegpontos érték unsigned­dé alakítása. Konverzió más típusokról: Nincs konverzió a struktúra és az unió típusok között. Explicit típusmódosítással bármilyen érték konvertálható void típusúvá, de csak abban az értelemben, hogy a kifejezés értékét elvetjük. A void típusnak definíció szerint nincs értéke. Ebbl következleg nem konvertálható más típusúra, s más típus sem konvertálható void­ra hozzárendeléssel.

C programnyelv 5.12 Vessz operátor Ez a legalacsonyabb prioritású mvelet.
kifejezés: hozzárendelés-kifejezés kifejezés , hozzárendelés-kifejezés

101

A K1, K2, ..., Kn esetén balról jobbra haladva kiértékeli a fordító a kifejezéseket úgy, hogy a bennük foglalt minden mellékhatás is megvalósul. Az els n - 1 kifejezés void­nak tekinthet, mert az ,,egész konstrukció" típusát és értékét a legjobboldalibb kifejezés típusa és értéke határozza meg. Például a
regikar = kar, kar = getchar()

esetén regikar felveszi kar pillanatnyi értékét, aztán kar és az egész kifejezés értéke a szabvány bemenetrl beolvasott karakter lesz. Tipikus példa még a több kezdérték adás és léptetés a for utasításban: Írjuk meg egy rövid kipróbáló programmal a void strrv(char s[]) függvényt, mely saját helyén megfordítja a paraméter karakterláncot! Az algoritmusról annyit, hogy a karakterlánc els karakterét meg kell cserélni az utolsóval, a másodikat az utolsó elttivel, és így tovább. Két indexet kell indítani a karaktertömbben: egyet alulról és egyet felülrl. Az alsót minden csere után meg kell növelni eggyel, a felst pedig ugyanenynyivel kell csökkenteni. A ciklus tehát addig mehet, míg az alsó index kisebb a felsnél.
/* PELDA16.C: Karakterlánc megfordítása. */ #include #include /* Az strlen miatt! */ #define INP 66 /* Az input puffer mérete. */ void strrv(char s[]){ int also, felso, csere; for(also=0, felso=strlen(s)-1; also
102

MVELETEK ÉS KIFEJEZÉSEK
printf("Megfordítva: %s\n", s); } }

A STRING.H fejfájlt bekapcsolva rendelkezésre áll az strrv szabvány könyvtári változata, mely ugyanilyen paraméterezés és funkciójú, de strrev a neve, s más egy kicsit a visszatérési értékének a típusa. Vegyük észre, hogy a main­beli while­ban is vesszs kifejezést használtunk! Olyan szövegkörnyezetben, ahol a vessz szintaktikai jelentés, a veszsz operátort csak zárójelbe tett csoporton belül szabad használni. Ilyen helyek: az inicializátorlista például, vagy a függvény paraméterlistája. A
fv(b, (a = 2, t += 3), 4);

kitnen mutatja, hogy az fv függvény három paraméterrel rendelkezik. A hívásban a második paraméter vesszs kifejezés, ahol a elbb 2 érték lesz, aztán t megn hárommal, s ezt az értéket kapja meg a függvény is második aktuális paraméterként. Ha a függvény prototípusa mást nem mond, akkor a második paraméter típusa t típusa. 5.13 Mveletek prioritása A mveletek prioritását nevezik precedenciának, vagy rendségnek is. Mindhárom esetben arról van szó, hogy zárójel nélküli helyzetben melyik mveletet kell végrehajtani elbb a kifejezés kiértékelése során. A következ táblázat csökken prioritással haladva mutatja az egyes operátorok asszociativitását. A többször is elforduló operátorok közül mindig az egyoperandusos a magasabb prioritású. Abban a rovatban, ahol több operátor van együtt, a mveletek azonos prioritásúak, és asszociativitásuknak megfelelen hajtja ket végre a fordító. Minden operátor kategóriának megvan a maga asszociativitása (balról jobbra vagy jobbról balra köt), mely meghatározza zárójel nélküli helyzetben a kifejezés csoportosítását azonos prioritású mveletek esetén.

C programnyelv Operátorok () [] -> . ! ~ + - ++ -- & * sizeof */% +<< >> < <= > >= == != & ^ | && || ?: = *= /= %= += -= &= ^= |= <<= >>= , Asszociativitás balról jobbra 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 jobbról balra balról jobbra

103

13. ábra: Mveletek prioritása

A táblázatban felsoroltakon kívül van még a # és a ## operátor, melyek az elfeldolgozónak szólnak. A prioritási táblázat (13. ábra) természetesen tartalmaz eddig még nem ismertetett mveleteket is. Vannak többjelentés operátorok is, melyek értelmezése a helyzettl függ. Például:
cimke: a?x:y a=(b+c)*d; void fv(int n); a, b, c; fv(a, b, c); /* /* /* /* /* /* utasítás címke */ feltételes kifejezés */ zárójeles kifejezés */ függvénydeklaráció */ vesszs kifejezés */ függvényhívás */

A kifejezés kiértékelési sorrendje nem meghatározott ott, ahol a nyelvi szintaktika errl nem gondoskodik. A fordító a generált kód hatékonyságának javítása érdekében átrendezheti a kifejezést különösen az asszocia-

104

MVELETEK ÉS KIFEJEZÉSEK

tív és kommutatív operátorok (*, +, &, ^ és |) esetén, hisz azt feltételezi, hogy a kiértékelés iránya nem hat a kifejezés értékére. Fedezzük fel, hogy itt nem arról van szó, hogy a fordító a következ fordításkor másként rendezi át a kifejezést, hisz a fordítás determinisztikus dolog, hanem arról, hogy különböz C fordítók más­más eredményre juthatnak! Probléma lehet az olyan kifejezéssel, · melyben ugyanazt az objektumot egynél többször módosítjuk, ill. · melyben ugyanazon objektum értékét változtatjuk és felhasználjuk. Például:
i = v[i++]; /* Döntsük el, mit is akarunk i-vel! */ i = a+++b[a]; /* b indexe attól függ, hogy az összeadás melyik tagját értékeli ki elbb a fordító. */

A fentiek akkor is igazak, ha kifejezésünket ,, jól összezárójelezzük":
int osszeg=0; sum = (osszeg=3)+(++osszeg);/* sum == 4 vagy 7 ? */

Segédváltozók bevezetésével a dolgok midig egyértelmsíthetk:
int seged, osszeg=0; seged = ++osszeg; sum = (osszeg=3)+seged; /* seged == 1, osszeg == 1. */ /* sum == 4. */

Ha a szintaktika rögzíti a kiértékelési sorrendet (az &&, a ||, a ?: és a vessz operátor esetében ez így van), akkor ott ,,bármit" megtehetünk. Például:
sum = (i=3, i++, i++); /* OK, sum == 4 és i == 5. */

A kifejezés kiértékelési sorrendjét ugyan () párokkal befolyásolhatjuk:
f = a*(b+c);

, de asszociatív és kommutatív operátorok operandusait még ,,agyonzárójelezve" is összecserélheti a fordító, hisz feltételezheti, hogy a kifejezés értékét ez nem befolyásolja:
f = (a+b) + (c+d);

Határozatlan a függvény aktuális paramétereinek kiértékelési sorrendje is:
printf("%d %d\n", ++n, fv(n));

A bitenkénti operátorok prioritása alacsonyabb a relációkénál, így a
c & 0XF == 8

C programnyelv

105

mindig hamis, mert az elbb kiértékelésre kerül egyenlségi reláció sohasem lehet igaz. A kifejezés helyesen:
(c & 0XF) == 8.

Vigyázzunk a hozzárendelés (=) és az egyenl reláció (==) operátor felcserélésére, mert például az
if( i = 2 ) utasítás1; else utasítás2;

else ágára sohasem jut el a vezérlés tekintettel arra, hogy a 2 hozzárendelése ,,ebben az életben" sem válik hamissá. Ügyeljünk azokkal a kifejezésekkel is, melyekben csak logikai és (&&) vagy csak logikai vagy (||) mveletek vannak, mert ezeket balról jobbra haladva · && esetén csak az els hamis tagig, ill. · || operátornál csak az els igaz tagig fogja kiértékelni a fordító! Az
x && y++

kifejezésben y növelése csak akkor történik meg, ha x nem zérus. Kifejezés kiértékelése közben adódhatnak ,,áthidalhatatlan" szituációk. Ilyenek · a zérussal való osztás és · a lebegpontos túl vagy alulcsordulás. Újra felhívjuk azonban a figyelmet arra, hogy a nyelvben nem létezik sem egész túl, sem alulcsordulás!

106

UTASÍTÁSOK

6 UTASÍTÁSOK
Az utasításokat ­ ha mást nem mondanak ­ megadásuk sorrendjében hajtja végre a processzor. Az utasításoknak nincs értéke. Végrehajtásuk hatással van bizonyos adatokra, vagy programelágazást valósítanak meg stb. Definíciójuk a következ:
utasítás: összetett-utasítás címkézett-utasítás kifejezés-utasítás szelekciós-utasítás iterációs-utasítás ugró-utasítás

6.1

Összetett utasítás

összetett-utasítás: { } deklarációlista: deklaráció deklarációlista deklaráció utasításlista: utasítás utasításlista utasítás

Az összetett utasítás utasítások (lehet, hogy üres) listája { }-be téve. Az összetett utasítást blokknak is nevezik, mely szintaktikailag egy utasításnak minsül, és szerepet játszik az azonosítók hatáskörében és láthatóságában. Ha a deklarációlistában elforduló azonosítót már korábban az öszszetett utasításon kívül is deklarálták, akkor a blokkra lokális azonosító elfedi a blokkon kívülit a blokk teljes hatáskörében. Tehát az ilyen blokkon kívüli azonosító a blokkban nem látható. C blokkban elbb a deklarációs, s csak aztán a végrehajtható utasítások következnek. Az összetett utasítások akármilyen mély szinten egymásba ágyazhatók, s az elbbi szerkezet a beágyazott blokkra is vonatkozik. Tudjuk, hogy az auto tárolási osztályú objektumok inicializálása mindannyiszor megtörténik, valahányszor a vezérlés a ,,fejen át" kerül be az összetett utasításba. Ez az inicializálás azonban elmarad, ha a vezérlés ugró utasítással érkezik a blokk ,,közepére". Tilos ;­t írni az összetett utasítás záró }­e után!

C programnyelv 6.2 Címkézett utasítás

107

címkézett-utasítás: azonosító : utasítás case konstans-kifejezés : utasítás default : utasítás

A case és a default formák csak switch utasításban használatosak. Ezek ismertetésével majd ott foglalkozunk! Az
azonosító : utasítás

alakban az azonosító (címke) célja lehet például egy feltétlen elágaztató
goto azonosító;

utasításnak. A címkék hatásköre mindig az ket tartalmazó függvény. Nem deklarálhatók újra, de külön névterületük van, és önmagukban nem módosítják az utasítások végrehajtási sorrendjét. C-ben csak végrehajtható utasítás címkézhet meg, azaz
{ /* . . . cimke: } /* HIBÁS */

A megoldás helyessé válik, ha legalább egy üres utasítást teszünk a cimke után:
{ /* . . . cimke: ; } /* OK */

6.3

Kifejezés utasítás

kifejezés-utasítás: ;

Ha a kifejezést pontosvessz követi, kifejezés utasításnak minsül. A fordító kiértékeli a kifejezést, s eközben minden mellékhatás érvényre jut, mieltt a következ utasítás végrehajtása elkezddne. Például az
x = 0, i++, printf("Hahó!\n")

kifejezések, s így válnak kifejezés-utasításokká:
x = 0; i++; printf("Hahó!\n");

A legtöbb kifejezés utasítás hozzárendelés vagy függvényhívás. Üres utasítást (null statement) úgy kapunk, hogy kifejezés nélkül pontosvesszt teszünk. Hatására természetesen nem történik semmi, de ez is

108

UTASÍTÁSOK

utasításnak minsül, azaz szintaktikailag állhat ott, ahol egyébként utasítás állhat. 6.4 Szelekciós utasítások
szelekciós-utasítás: if(kifejezés) utasítás if(kifejezés) utasítás else utasítás switch(kifejezés) utasítás

Látszik, hogy két szelekciós utasítás van: az if és a switch. A feltételesen elágaztató
if(kifejezés) utasítás1 else utasítás2

utasításban a kifejezésnek aritmetikai, vagy mutató típusúnak kell lennie. Ha értéke zérus, akkor a logikai kifejezés hamis, máskülönben viszont igaz. Ha a kifejezés igaz, utasítás1 következik. Ha hamis, és az else ág létezik, akkor utasítás2 jön. Mind utasítás1, mind utasítás2 lehet összetett utasítás is! A nyelvben nincs logikai adattípus, de cserében minden egész és mutató típus annak minsül. Például:
if(ptr == 0) if(ptr != 0) /* Rövidíthet ,,if(!ptr)"-nek. */ /* Rövidíthet ,,if(ptr)"-nek. */

Az else ág elhagyhatósága néha problémákhoz vezethet. Például a
if( x == 1) if( y == 1 ) puts( "x = 1 és y = 1\n"); else puts( "x != 1\n");

forrásszövegbl a programozó ,,elképzelése" teljesen világos. Sajnos azonban egyet elfelejtett. Az else mindig az ugyanazon blokk szinten lev, forrásszövegben megelz, else ág nélküli if-hez tartozik. A megoldás helyesen:
if( x == 1) { if( y == 1 ) puts( "x = 1 és y = 1\n"); } else puts( "x != 1\n");

Az if utasítások tetszleges mélységben egymásba ágyazhatók, azaz mind az if, mind az else utasítása lehet újabb if utasítás is. Például:
if( x == 1) { if( y == 1 ) puts( "x = 1 és y = 1\n"); else puts( "x = 1 és y != 1\n");} else { if( y == 1 ) puts( "x != 1 és y = 1\n");

C programnyelv
else puts( "x != 1 és y != 1\n");}

109

Többirányú elágazást (szelekciót) valósít meg a következ konstrukció:
if( kifejezés1 ) utasítás1 else if( kifejezés2 ) utasítás2 else if ( kifejezés3 ) utasítás3 /* . . . */ else utasításN

Ha valamelyik if kifejezése igaz, akkor az azonos sorszámú utasítást hajtja végre a processzor, majd a konstrukciót követ utasítás jön. Ha egyik if kifejezése sem igaz, akkor viszont utasításN következik. Ha egy n elem, növekvleg rendezett t tömbben keresünk egy x értéket, akkor ezt bináris keresési algoritmust alkalmazva így interpretálhatjuk:
int binker(int x, int t[], int n){ int also=0, felso=n-1, kozep; while(also<=felso){ kozep=(also+felso)/2; if(xt[kozep]) also=kozep+1; else return kozep; } /* Megvan. */ return (-1); } /* Nincs meg. */

A binker háromirányú elágazást realizál. x t tömbbeli elfordulásának indexét szolgáltatja, ill. ­1­et, ha nincs is x érték elem a tömbben. A bináris keresés növekvleg rendezett tömbben úgy történik, hogy elször megállapítjuk, hogy a keresett érték a tömb alsó, vagy fels felébe esik. A módszert aztán újraalkalmazzuk az aktuális félre, s így tovább. A dolognak akkor van vége, ha a keres tartomány semmivé szkül (ilyenkor nincs meg a keresett érték), vagy a felez tömbelem egyezik a keresett értékkel. Rendezett sorozatban egy érték keresésének ez a módja, és nem a minden elemhez történ hasonlítgatás! Megoldandó feladatok: Készítsen olyan binker függvényt, mely: · csökkenleg rendezett tömbben keres! · pótlólagos paraméterben kapja meg, hogy csökken vagy növekv a rendezettség!

110

UTASÍTÁSOK

· plusz paraméter nélkül is eldönti, hogy csökkenleg, vagy növekvleg keressen a tömbben! A többirányú programelágaztatás másik eszköze a
switch(kifejezés) utasítás

, melyben a kifejezésnek egész típusúnak kell lennie. Az utasítás egy olyan speciális összetett utasítás, mely több case címkét
case konstans-kifejezés : utasítás

és egy elhagyható default címkét
default : utasítás

tartalmazhat. A vezérlést azon case címkét követ utasítás kapja meg, mely konstans-kifejezésének értéke egyezik a switch­beli kifejezés értékével. A végrehajtás aztán itt addig folytatódik, míg break utasítás nem következik, vagy vége nincs a switch blokkjának. A case címkebeli konstans-kifejezésnek is egész típusúnak kell lennie. Ez a TÍPUSOK ÉS KONSTANSOK szakasz Deklaráció fejezetében írottakon túl további korlátozásokat ró a konstans kifejezésre. Operandusai csak egész, felsorolás, karakteres és lebegpontos állandók lehetnek, de a lebegpontos konstanst explicit típuskonverzióval egésszé kell alakítani. Operandus lehet még a sizeof operátor is, aminek operandusára természetesen nincsenek ilyen korlátozások. A végrehajtás során a kifejezés és a case konstans-kifejezések értékén is végbemegy az egész­elléptetés. A kifejezés az összes esetleges mellékhatásával egyetemben valósul meg, mieltt az értékhasonlítás megkezddne. Ha nincs a switch kifejezés értékével egyez case címke, akkor a vezérlést a default címke utasítása kapja meg. Ha nincs default címke sem, akkor vége van a switch­nek. Az utasítás használatához még két megjegyzést kell fzni: · Több case címke is címkézhet egy utasítást. · Egyazon switch utasításban viszont nem fordulhat el két azonos érték case konstans-kifejezés. A switch utasítások egymásba is ágyazhatók, s ilyenkor a case és a default címkék mindig az ket közvetlenül tartalmazó switch­hez tartoznak. A switch utasítást szemléltetend írjuk át a szabvány bemenet karaktereit kategóriánként leszámláló PELDA7.C­t!

C programnyelv
/* PELDA17.C: A bemenet karaktereinek leszámlálása kategóriánként */ #include void main(void){ short k, num=0, feher=0, egyeb=0; printf("Bemeneti karakterek leszámlálása\n" "kategóriánként EOF-ig, vagy Ctrl+Z-ig.\n"); while((k=getchar())!=EOF) switch(k){ case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': ++num; break; case ' ': case '\n': case '\t': ++feher; break; default: ++egyeb; } printf("Karakter számok:\n----------------\n" "numerikus: %5hd\nfehér: %5hd\n" "egyéb: %5hd\n----------------\n" "össz: %10ld\n", num, feher, egyeb, (long)num+feher+egyeb); }

111

Belátható, hogy a switch utasítás használhatóságát szegényíti, hogy a kifejezés csak egész típusú lehet és, hogy a case címkék csak egészérték konstans-kifejezések lehetnek. Az if­nél említett többirányú elágaztatási konstrukció így jóval általánosabban használható. 6.5 Iterációs utasítások
iterációs-utasítás: while(kifejezés) utasítás do utasítás while(kifejezés); for(; ; ) utasítás

Szemmel láthatóan háromféle iterációs utasítás van, amibl kett elöltesztel ciklusutasítás. A
while(kifejezés) utasítás

112

UTASÍTÁSOK

kifejezésérl ugyanaz mondható el, mint az if utasítás kifejezésérl. Lépésenként a következ történik: 1. Kiértékeli a fordító a kifejezést, melynek során minden esetleges mellékhatás is megvalósul. Ha hamis (zérus) az értéke, akkor vége a ciklusnak, s a while-t követ utasítás jön a programban. 2. Ha a kifejezés igaz (nem zérus), akkor az utasítás végrehajtása, és újból az 1. pont következik. Világos, hogy a kifejezés értékének valahogyan változnia kell az utasításban, különben a ciklusnak soha sincs vége. Az utasítás állhat több utasításból is, azaz összetett utasítás is lehet. Ha az utasítások közt ugró utasítás is van, akkor ezen mód is kiléphetünk a ciklusból. A
for(; ; ) utasítás

elöltesztel, iteratív ciklusutasítás, melynek megvalósulása a következ lépésekbl áll: 1. A fordító végrehajtja az init-kifejezést, ha van. Többnyire egy vagy több változó értékadásáról lehet itt szó. 2. Kiértékeli a kifejezést. Ha hamis (zérus), akkor vége a ciklusnak, s a for-t követ utasítás jön a programban. Látható, hogy a szintaktika szerint ez a kifejezés is elhagyható. Ilyenkor 1 (igaz) kerül a helyére, azaz a végtelen ciklus könnyedén felírható:
for( ; ; ); for( ; 1; );

3. Ha a kifejezés igaz (nem zérus), akkor az utasítás jön, 4. aztán a léptet-kifejezés, majd újból a 2. pont következik. Ha a for utasítást while-lal szeretnénk felírni, akkor azt így tehetjük meg, ha a ciklusmagban nincs continue: ; while() { utasítás; ; } A szintaktikai szabályt összefoglalva: a for-ból akár mindegyik kifejezés is elhagyható, de az els kettt záró pontosvesszk nem! A vessz operátor alkalmazásával mind az init-, mind a léptetkifejezés több kifejezés is lehet. Ezt szemlélteti az elz szakaszban a PELDA16.C­ben megírt strrv függvény.

C programnyelv

113

Írjunk void rendez(int a[], int n) függvényt, mely növekvleg rendezi saját helyén az n elem a tömböt! · Indulva az els elemtl megkeressük a tömb minimális elemét, és kicseréljük az els elemmel. · A 2. elemtl kezdve a hátra lev tömbrészben keressük meg a legkisebb elemet, s ezt kicseréljük a 2. elemmel, s így tovább. · Látszik, hogy minimumkeresést utoljára a tömb utolsó eltti és utolsó elemén kell elvégezni, s az itteni esetleges csere után rendezett is az egész tömb.
void rendez(int a[],int n){ int i, j, m, cs; for(i=0; i
Ez ugyebár példa az egymásba ágyazott ciklusokra! Készítsük el az int egesze(char s[]) rutint, mely ellenrzi a decimális egész konstans írásszabályát a paraméter karaktertömbön. 1­et szolgáltat, ha a dolog rendben van, s zérust, ha nem!
/* A decimális számjegyek száma maximálisan: */ #define HSZ sizeof(int)/sizeof(short)*5 int egesze(char s[]){ int i = 0, kezd; /* A karakterlánc eleji fehér karakterek átlépése: */ while(s[i]==' ' || s[i]=='\n' || s[i]=='\t') ++i; /* Az eljel átlépése: */ if(s[i]=='+' || s[i]=='-') ++i; kezd=i; /* A számjegyek itt kezddnek. */ /* Elre a következ nem numerikus karakterre: */ while(s[i]>='0' && s[i]<='9' && i-kezd
Látszik, hogy HSZ 16 bites int esetén 5 (32767), és 32 bitesnél 10 (2147483647). Arra való, hogy ennél több decimális számjegyet ne fogadjon el a rutin. Vegyük észre, hogy a függvény visszautasítja, ha egyetlen számjegy karaktert sem tartalmaz a numerikus karakterlánc! Elveti azt is, ha a numerikus lánc rész nem fehér karakterrel, vagy lánczáró zérussal végzdik. Az ellenrzés persze nem tökéletes, hisz a numerikus karakterlánc ábrázolási határok közé férését nem biztosítja. Például 16 bites int­nél minden ötje-

114

UTASÍTÁSOK

gy számot érvényesnek tekint, holott a ­99999 és ­32769, valamint a 32768 és 99999 közötti számok ábrázolhatatlanok. Ez az ellenrzés azonban csak akkor lenne könnyen megvalósítható, ha tesztelés közben konvertálnánk is a számot. Ekkor azonban már ,,többe kerülne a leves, mint a hús". Készítsük programot, mely egész számokat kér be. Meghatározza az átlagukat, a minimumukat, maximumukat, és növekvleg rendezi is ket! Az egészek darabszáma csak futás közben dl el.
/* PELDA18.C: Egész számok átlaga, minimuma, maximuma és rendezése */ #include #include /* Az atoi miatt! */ #include /* INT_MIN és INT_MAX végett! */ #define MAX 100 /* A tömb max. elemszáma. */ #define INP 20 /* Az input puffer mérete. */ int getline(char s[],int lim); /* A decimális számjegyek száma maximálisan: */ #define HSZ sizeof(int)/sizeof(short)*5 int egesze(char s[]); void rendez(int a[],int n); void main(void){ int n=0; /* A rendezend elemek száma. */ char sor[INP+1]; /* Az input puffer. */ int a[MAX]; /* A egészeket tároló tömb. */ int min, max; /* Minimum, maximum. */ double osszeg=0.;/* Az összeg */ int i; while(n<1||n>MAX){ printf("\nHány egész számot rendezünk(1-%d)? ",MAX); getline(sor,INP); if(egesze(sor)) n=atoi(sor);} printf("\n\nKérem a rendezend számokat %d és +%d" " között!\n", INT_MIN, INT_MAX); for(i=0;i0 && egesze(sor)) a[i]=atoi(sor); else --i; } min=max=a[0]; for(i=0;imax) max=a[i]; printf( "\nA számsorozat\tminimuma:%14d.\n" "\t\tmaximuma:%14d.\n" "\t\tátlaga: %17.2f\n", min, max, osszeg/n); printf("\nA rendezett számok:\n"); rendez(a, n);

C programnyelv
for(i=0; i
115

Nem rendezett sorozatban a minimum, vagy maximum megkeresésének az a módja, hogy vesszük a sorozat egy létez elemét (többnyire az elst), és aztán hasonlítgatjuk a többiekhez, hogy vajon van­e nálánál kisebb, ill. nagyobb. Ha van, akkor attól kezdve azzal folytatjuk a hasonlítást. El kell ismerni természetesen, hogy a minimum és maximum megkeresése teljesen felesleges volt, mert a rendezés után ezeket az értékeket a[0] és a[n­1] amúgy is tartalmazza. Vegyük még észre, hogy az egészek összegét a for ciklus léptetkifejezésében számítjuk ki! Megoldandó feladatok: Alakítsa át a PELDA18.C­ben megvalósított programot a következképp: · Ne kérje be elre a rendezend számok darabszámát, hanem az érték bekérésekor jelentse üres sor a bemenet végét! · Dolgozzék nem egész, hanem valós számokkal a program! A
do utasítás while(kifejezés);

hátultesztel ciklusutasítás. A kifejezésre ugyanazon megkötések érvényesek, mint a while­nál. A dolog lényege az, hogy fordító a kifejezés értékétl függetlenül egyszer biztosan 1. végrehajtja az utasítást. 2. Kiértékeli a kifejezést. Ha hamis (zérus), akkor vége a ciklusnak, s a while-t követ utasítás jön a programban. Ha viszont igaz, akkor az 1. pont következik. Összesítve: Az utasítást egyszer mindenképp végrehajtja a proceszszor, s ezt követen mindaddig ismétli, míg a kifejezés hamis nem lesz. A szemléltet példában az itoa függvény az int n paraméterét karakterlánccá konvertálja, és elhelyezi a paraméter s karaktertömbben:
#include #include #include

116
void itoa(int n, char s[]){ int i=0, elojel, j=0; if(n==INT_MIN) { ++n; ++j; } if((elojel=n)<0) n=-n; do s[i++]=n%10+'0'; while(n/=10); if(elojel<0) s[i++]='-'; s[i]=0; s[0]+=j; strrev(s); }

UTASÍTÁSOK

A belsábrázolási formából karakterlánccá alakító itoa rutinban az i ciklusváltozó. A j logikai változó, s induló értéke 0. Egyetlen egy esetben egyérték, ha n éppen INT_MIN. A probléma az ugye, hogy mindenképp pozitív értéket kívánunk konvertálni az esetleges negatív eljelet megjegyezve, de az INT_MIN ­1­szerese nem ábrázolható int­ként, hisz éppen eggyel nagyobb INT_MAX­nál. Ha ilyenkor megnöveljük eggyel n értékét, akkor ­1­szerese épp a fels ábrázolási korlát lesz, amivel már nincs probléma. A j logikai változó tehát akkor 1, ha volt növelés. Az elojel változóra n eredeti eljelének megtartása végett van szükség, és azért, hogy negatív n esetén a keletkez karakterlánc végére ki lehessen tenni a mínuszjelet. Az algoritmus éppen megfordítva állítja el a karakterláncot. Nézzük csak asztali teszttel a 16 bites int esetét! Tegyük fel, hogy n értéke az ominózus ­32768! Amig a do­while utasításig nem érünk, j 1 lesz, n 32767 és i zérus marad. Lejátszva a ciklust a következ történik: j: n: i: s: 1 32767 0 '7' 3276 1 '6' 327 2 '7' 32 3 '2' 3 4 '3' 0 5 '­' 0 6 '\0'

A táblázatban az az állapot látszik, amikor a do­while­t követ két utasítás is lezajlott. Az s[0]­ból, vagyis '7'­bl, '8' lesz, és aztán a szabvány könyvtári strrev megfordítja a saját helyén az eredmény karakterláncot. 6.6 Ugró utasítások
ugró-utasítás: break; continue; goto azonosító; return ;

C programnyelv

117

Csak iterációs (while, do-while és for) vagy switch utasításon belül használható a
break;

, mely befejezi ezeket az utasításokat, azaz hatására a vezérlés feltétel nélkül kilép bellük. Több egymásba ágyazott iterációs utasítás esetén a break csak abból a ciklusból léptet ki, amelyben elhelyezték, azaz a break csak egyszint kiléptetésre képes. Készítend egy int trim(char s[]) függvény, mely a paraméter karaktertömb elejérl és végérl eltávolítja a fehér karaktereket a saját helyén, s visszatér az így letisztított karakterlánc hosszával!
#include int trim(char s[]){ int i=0, n; /* A fehér karakterek eltávolítása a lánc végérl: */ for(n=strlen(s)-1; n>=0; --n) if(s[n]!=' '&&s[n]!='\n'&&s[n]!='\t') break; s[++n]='\0'; /* A fehér karakterek eltávolítása a lánc elejérl: */ while(s[i]==' '||s[i]=='\n'||s[i]=='\t') ++i; if(i) { n=0; while(s[n++]=s[i++]); --n;} return(n); }

Csak iterációs utasításokban (while, do-while és for) alkalmazható a
continue;

, mely a vezérlést a kifejezés kiértékelésére viszi while és do-while estén, ill. hatására a léptet-kifejezés kiértékelése következik for utasításnál. Egymásba ágyazott iterációs utasítások esetén ez is mindig csak az t magába foglaló iterációs utasításra vonatkozik. Ugyebár switch­ben csak iterációs utasításon belül használható a continue! Feltétlen vezérlésátadást hajt végre az azonosítóval címkézett utasításra a
goto azonosító;

Az utasítást bármilyen mély blokk szintrl is végrehajtja a processzor, de a cél címkézett utasításnak ugyanabban a függvényben kell lennie, ahol a goto is van. A void visszatérésektl eltekintve a függvény testében lennie kell legalább egy
return ;

118

UTASÍTÁSOK

utasításnak. Ha a rutin visszatérési típusa típus, akkor a kifejezés típusának is ennek kell lennie, vagy hozzárendelési konverzióval ilyen típusúvá alakítja a kifejezés értékét a fordító. A return hatására visszakapja a vezérlést a hívó függvény, s átveszi a visszatérési értéket is. A típus visszatérés függvényt hívó kifejezés
fv(aktuális-paraméterlista)

típus típusú jobbérték, s nem balérték:
típus t; t=fv(aktuális-paraméterlista); t=++fv(aktuális-paraméterlista); /* OK */ /* OK */

A függvényhívás hatására beindult végrehajtás a return utasítás bekövetkeztekor befejezdik. Ha nincs return, akkor a függvény testét záró }-ig megy a végrehajtás. Ha a függvény által visszaadott érték típusa void, és a függvényt nem a testét záró }-nél szeretnénk befejezni, akkor a függvény belsejébe a kívánt helyre return utasítást kell írni.

C programnyelv

119

7 ELFELDOLGOZÓ (PREPROCESSOR)
A fordító els menete során mindig meghívja az elfeldolgozót a forrásfájlra. Ha szeretnénk tudni, hogy fest az elfeldolgozáson átesett forrásfájl (a fordítási egység), akkor a programfejleszt rendszerben utána kell nézni, hogy tehetjük láthatóvá az elfeldolgozás eredményét. Az elfeldolgozott forrásfájlban aztán megtekinthetjük: · a makrók kifejtését, · a behozott (include) fájlokat, · a feltételes fordítást, · a szomszédos karakterlánc konstansok egyesítését, · a direktívák elhagyását és · a megjegyzések kimaradását (helyettesítését egyetlen szóköz karakterrel). A nem karakter, vagy karakterlánc konstansban elforduló, egymást követ, több fehér karaktert mindig eggyel helyettesíti az elfeldolgozó. Az elfeldolgozó direktívák írásszabálya, mely független a C nyelv többi részétl, a következ: · A sor els, nem fehér karakterének #-nek kell lennie. · A #-et követheti aztán fehér karakter a soremelést kivéve. A sorokra tördelés nagyon lényeges elem, mert az elfeldolgozó sorokra bontva elemzi a forrásszöveget. · A karakter konstansban, a karakterlánc konstansban és a megjegyzésben lev # karakter nem minsül elfeldolgozó direktíva kezdetének. A direktívákat - miután nem C utasítások - tilos pontosvesszvel lezárni! · Ha a direktívában a soremelést \ karakter elzi meg, akkor a következ sor folytatássornak minsül, azaz az elfeldolgozó elhagyja a \­t és a soremelést, s egyesíti a két sort. · Az elfeldolgozó direktívákba megjegyzés is írható.

120

ELFELDOLGOZÓ DIREKTÍVÁK

· Az elfeldolgozó direktívák bárhol elhelyezkedhetnek a forrásfájlban, de csak a forrásfájl ezt követ részére hatnak, egészen a fájl végéig. A teljes szintaktika az elfeldolgozó direktívákra a feltételes fordítástól eltekintve úgy, hogy a már megismert fogalmakat újra nem definiáljuk, a következ:
csoport: csoport-rész csoport csoport-rész csoport-rész: elfeldolgozó-szimbólumok újsor feltételes-fordítás vezérl-sor elfeldolgozó-szimbólumok: elfeldolgozó-szimbólum elfeldolgozó-szimbólumok elfeldolgozó-szimbólum elfeldolgozó-szimbólum: (csak #include direktívában) "fájlazonosító" (csak #include direktívában) azonosító (nincs kulcsszó megkülönböztetés) konstans karakterlánc-konstans operátor elválasztó-jel bármilyen nem fehér karakter, mely az elzek egyike sem vezérl-sor: #include elfeldolgozó-szimbólumok újsor #define azonosító újsor #define azonosító(azonosítólista) elfeldolgozó-szimbólumok újsor #undef azonosító újsor #line elfeldolgozó-szimbólumok újsor #error újsor #pragma újsor # újsor újsor: soremelés

Az elfeldolgozó-szimbólum definíciójában a fájlazonosító körüli <> a szintaktika része, és nem az elhagyhatóságot jelöli, mint máskor. Látszik, hogy a feldolgozásban bármely karakter, ami nem foglalt az elfeldolgozónak, szintén szintaktikai egységet képez. 7.1 Üres (null) direktíva A csak egy # karaktert tartalmazó sor. Ezt a direktívát elhagyja az elfeldolgozó, azaz hatására nem történik semmi.

C programnyelv 7.2 #include direktíva Az #include direktíva lehetséges alakjait:
#include újsor #include "fájlazonosító" újsor

121

már a BEVEZETÉS ÉS ALAPISMERETEK szakaszban részleteztük. Tudjuk, hogy a vezérl-sort az elfeldolgozó a megadott fájl teljes tartalmával helyettesíti. Magát a fájlt "fájlazonosító" esetén elbb az aktuális könyvtárban (abban, ahonnét azt a fájlt töltötte, amelyikben ez az #include direktíva volt), majd és ­s esetben a programfejleszt rendszerben elírt utakon keresi. A <> és az idézjelek között nincs makróhelyettesítés. Ha a fájlazonosító­ban >, ", ', \, vagy /* karakterek vannak, az elfeldolgozás eredménye meghatározatlan. Ha macskakörmös esetben a fájlazonosító elérési utat is tartalmaz, akkor a fájlt a preprocesszor csak abban a könyvtárban keresi, és sehol másutt. A direktíva
#include elfeldolgozó-szimbólum újsor

formáját elbb feldolgozza az elfeldolgozó, de a helyettesítésnek itt is <>, vagy "" alakot kell eredményeznie, s a hatás ennek megfelel. Például:
#define ENYIM "\Cfajlok\Munka16\Pipo.h" /* . . . */ #include ENYIM

Az #include direktívák egymásba ágyazhatók, azaz a behozott fájl újabb #include­okat tartalmazhat, s azok megint újabbakat, és így tovább. Az egymásba ágyazgatásokkal azonban vigyázni kell, mert egyes programfejleszt rendszerek ezek szintjét ­ például 10­ben ­ korlátozhatják! 7.3 Egyszer #define makró A #define direktíva makrót definiál (makródefiníció). A makró szimbólumhelyettesít mechanizmus függvényszer formális paraméterlistával vagy anélkül. Elbb a paraméter nélküli esettel foglalkozunk! Ilyenkor a direktíva alakja:
#define azonosító újsor

122

ELFELDOLGOZÓ DIREKTÍVÁK

Hatására az elfeldolgozó a forráskódban ez után következ minden makróazonosító elfordulást helyettesít a lehet, hogy üres elfeldolgozószimbólumokkal. Ha üres elfeldolgozó-szimbólumokkal történik a helyettesítés, a makróazonosító akkor is definiált, azaz #if defined vagy #ifdef direktívával "rákérdezhetünk" az azonosítóra, de a makróazonosító minden forrásszövegbeli elfordulásának eltávolítását jelenti tulajdonképp. Nem történik meg a helyettesítés, ha a makróazonosító karakter vagy karakterlánc konstansban, vagy megjegyzésben, vagy más makróazonosító részeként található meg. Ezt a folyamatot makrókifejtésnek (expansion) nevezik. A elfeldolgozó-szimbólumokat szokás makrótest névvel is illetni. Például:
#define HI "Jó napot!" #define begin { #define end } #define then #define NIL "" #define EGY 1 int main(void) begin /* Helyettesítés {-re. */ int i=8, k=i+EGY;/* Csere k=i+1-re.*/ puts(HI); /* puts("Jó napot!"); lesz belle */ puts(NIL); /* puts(""); lesz a sorból. */ puts("then"); /* Nincs helyettesítés, mert a makróazonosító karakterlánc konstansban van. */ if(++i
A makrókifejtés utáni makróazonosítókat is helyettesíti a preprocesszor, azaz a makrók is egymásba ágyazhatók. A makróazonosító újradefiniálása csak akkor nem hiba, ha az elfeldolgozó-szimbólumok tökéletesen, pozíció­helyesen azonosak. Ettl eltér újradefiniálás csak a rávonatkozó #undef direktíva után lehetséges. A nyelv kulcsszavait is alkalmazhatjuk makródefiníciókban, legfeljebb kissé értelmetlennek tekinthet az eljárásunk:
#define LACI for #define PAPI while #define int long

, de a következ fejezetben ismertetett, szabványos, elredefiniált makrók nem jelenhetnek meg sem #define, sem #undef direktívákban.

C programnyelv

123

A programfejleszt rendszer segítségében célszer utána nézni, hogy még milyen más, védett makróazonosítók használata tiltott! 7.4 Elredefiniált makrók Néhány makró elredefiniált az elfeldolgozó rendszerben, s kifejtetésükkel speciális információ képezhet. Ezek a makrók mind defined típusúak. Nem tehetk definiálatlanná, és nem definiálhatók újra. A szabványos, elredefiniált makrók és jelentésük a következ: __DATE__: HHH NN ÉÉÉÉ alakú karakterlánc, s az aktuális forrásfájl elfeldolgozása kezdetének dátuma. A HHH hárombets hónapnév rövidítés (Jan, Feb stb.). Az NN 1 és 31 közötti napszám, s így tovább. __FILE__: Karakterláncként a feldolgozás alatt álló forrásfájl azonosítóját tartalmazza. A makró változik #include és #line direktíva hatására, valamint ha a forrásfájl fordítása befejezdik. __LINE__: Decimális értékként a feldolgozás alatti forrásfájl aktuális sorának sorszáma. A sorszámozás 1-tl indul. Módosíthatja például a #line direktíva is. __STDC__: Definiált és 1, ha ANSI kompatibilis fordítás van, máskülönben definiálatlan. __TIME__: OO:PP:MM alakú karakterlánc, s a forrásfájl feldolgozása megkezdésének idejét tartalmazza. 7.5 #undef direktíva
#undef azonosító újsor

A direktíva definiálatlanná teszi a makróazonosítót, azaz a továbbiakban nem vesz részt a makrókifejtésben. Azt, hogy egy makróazonosító definiált-e vagy sem a forráskódban, megtudhatjuk a
#ifdef azonosító #ifndef azonosító

direktívák segítségével, azaz a makródefiníciónál ajánlható a következ stratégia:
#ifndef MAKROKA #define MAKROKA 162 #endif

Az ismeretlen makróazonosítóra kiadott #undef direktívát nem tekinti hibának az elfeldolgozó.

124

ELFELDOLGOZÓ DIREKTÍVÁK

A definiálatlanná tett makróazonosító késbb újradefiniálható akár más elfeldolgozó-szimbólumokkal. A #define-nal definiált és közben #undef-fel definiálatlanná nem tett makróazonosító definiált marad a forrásfájl végéig. Például:
#define BLOKK_MERET 512 /* . . . */ puff = BLOKK_MERET*blkszam; /* Kifejtés: 512*blkszam. */ /* . . . */ #undef BLOKK_MERET /* Innét a BLOKK_MERET ismeretlen makróazonosító. */ /* . . . */ #define BLOKK_MERET 128 /* . . . */ puff = BLOKK_MERET*blkszam; /* Kifejtés: 128*blkszam. */

7.6 Paraméteres #define direktíva A direktíva alakja ilyenkor:
#define azonosító(azonosítólista) elfeldolgozó-szimbólumok újsor

Az azonosítólista egymástól vesszvel elválasztott formális paraméterazonosítókból áll. A makrót hívó aktuális paraméterlistában ugyanannyi paraméternek kell lennie, mint amennyi a formális paraméterlistában volt, mert különben hibaüzenetet kapunk. Ugyan a makróra is a függvénnyel kapcsolatos fogalmakat használjuk képszerségük végett, de ki kell hangsúlyozni, hogy a makró nem függvény! A makróazonosító és a paraméterlistát nyitó kerek zárójel közé semmilyen karakter sem írható, hisz rögtön egyszer #define-ná tenné a paraméteres direktívát! Az elfeldolgozó elbb a makróazonosítót helyettesíti, s csak aztán a zárójelbe tett paramétereket:
Definíció: Forrássor: Kifejtve: #define KOB(x) ((x)*(x)*(x)) n = KOB(y); n = ((y)*(y)*(y));

A látszólag redundáns zárójeleknek nagyon fontos szerepük van:
Definíció: Forrássor: Kifejtve: #define KOB(x) (x*x*x) n = KOB(y + 1); n = (y + 1*y + 1*y + 1);

A küls zárójel pár a kifejezésekben való felhasználhatóságot biztosítja:
Definíció: Forrássor: #define SUM(a,b) (a)+(b) n = 14.5 * SUM(x*y, z-8);

C programnyelv
Kifejtve: n = 14.5 * (x*y)+(z-8);

125

A zárójelbe, vagy aposztrófok, idézjelek közé tett vesszk nem minsülnek a listában azonosító elválasztónak:
Definíció: Forrássor: Kifejtve: Definíció: Forrássor: Kifejtve: #define SUM(a,b) ((a)+(b)) return SUM(f(i,j), g(k,l)); return ((f(i,j))+(g(k,l))); #define HIBA(x,lanc) hibaki("Hiba: ",x,lanc) HIBA(2,"Üssön Enter-t, s Esc-t!"); hibaki("Hiba: ",2,"Üssön Enter-t, s Esc-t!");

Új azonosító generálási céllal a szimbólum1##szimbólum2 alakból szimbólum1szimbólum2-t állít el az elfeldolgozó:
Definíció: Forrássor: Kifejtve: #define VAR(i,j) (i##j) VAR(a,8) (a8)

A formális paraméter elé írt # (úgy nevezett karakterláncosító operátor) az aktuális paramétert karakterlánc konstanssá konvertálja:
Definíció: Forrássor: Kifejtve: #define NYOM(jel) printf(#jel "=%d\n", jel) int kilo = 100; NYOM(kilo); int kilo = 100; printf("kilo" "=%d\n", kilo);

Folytatássor most is sorvégi \ jellel képezhet:
Definíció: Forrássor: Kifejtve: #define FIGYU "Ez igazából egyetlen \ sornak minsül!" puts(FIGYU); puts("Ez igazából egyetlen sornak minsül!");

A makró nem függvény, tehát semmilyen ellenrzés sincs a paraméterek adattípusára! Ha az aktuális paraméter kifejezés, akkor kiértékelése többször is megtörténik:
int kob(int x){ return x*x*x;} #define KOB(x) ((x)*(x)*(x)) /* . . . */ int b = 0, a = 3; b = kob(a++); /* b == 27 és a == 4. */ a = 3; b = KOB(a++); /* Kifejtve: ((a++)*(a++)*(a++)), azaz b == 60 és a == 6. */

7.7 Karaktervizsgáló függvények (makrók) Megismerkedtünk az elz fejezetekben a makrók elnyös, és persze hátrányos tulajdonságaival. Mindezek dacára a makrók használata a C­ ben elég széleskör.

126

ELFELDOLGOZÓ DIREKTÍVÁK

Nézzük csak meg a szabványos STDIO.H fejfájlt, s látni fogjuk, hogy a szabvány bemenetet és kimenetet kezel getchar és putchar rutinok makrók. A szabványos STDIO.H fejfájlban deklarált függvények karakterosztályozást végeznek. A rutinok c paramétere int ugyan, de az értékének unsigned char típusban ábrázolhatónak, vagy EOF­nak kell lennie. A visszatérési értékük ugyancsak int logikai jelleggel, azaz nem zérus (igaz), ha a feltett kérdésre adott válasz igen, ill. zérus (hamis), ha nem. A teljesség igénye nélkül felsorolunk néhány karakterosztályozó függvényt! Függvény islower(c) isalpha(c) isdigit(c) c kisbet­e? islower(c) | isupper(c) c decimális számjegy­e? isupper(c) c nagybet­e? Kérdés

isalnum(c) isalpha(c) | isdigit(c) isxdigit(c) c hexadecimális számjegy­e? isspace(c) isprint(c) c fehér karakter­e? (szóköz, soremelés, lapemelés, kocsi vissza, függleges vagy vízszintes tabulátor) c nyomtatható karakter­e?

Meg kell még említeni két konverziós rutin is: int tolower(c); int toupper(c); , melyek c értékét kisbetvé (tolower), ill. nagybetvé (toupper) alakítva szolgáltatják, ha c bet karakter, de változatlanul adják vissza, ha nem az. Írjuk át PELDA17.C­t úgy, hogy karaktervizsgáló függvényeket használjon!
/* PELDA19.C: A bemenet karaktereinek leszámlálása kategóriánként az is... függvények segítségével. */ #include #include void main(void){ short k, num=0, feher=0, egyeb=0; printf("Bemeneti karakterek leszámlálása\n" "kategóriánként EOF-ig, vagy Ctrl+Z-ig.\n");

C programnyelv
while((k=getchar())!=EOF) if(isdigit(k)) ++num; else if (isspace(k)) ++feher; else ++egyeb; printf("Karakter számok:\n----------------\n" "numerikus: %5hd\nfehér: %5hd\n" "egyéb: %5hd\n----------------\n" "össz: %10ld\n", num, feher, egyeb, (long)num+feher+egyeb); }

127

Írjuk még át ugyanebben a szellemben a PELDA18.C egesze függvényét is!
#include #define HSZ sizeof(int)/sizeof(short)*5 int egesze(char s[]){ int i = 0, kezd; while(isspace(s[i])) ++i; if(s[i]=='+' || s[i]=='-') ++i; kezd=i; /* A számjegyek itt kezddnek. */ while(isdigit(s[i]) && i-kezd
A PELDA13.C­beli strup rutin így módosulna:
#include void strup(char s[]){ int i; for(i=0; s[i]; ++i) s[i]=toupper(s[i]); }

Milyen elnyei vannak a karakterosztályozó függvények használatának? · A kód rövidebb, és ez által gyorsabb is. · A program portábilis lesz, hisz függetlenedik az ASCII (vagy más) kódtábla sajátosságaitól. Az olvasó utolsó logikus kérdése már csak az lehet, hogy miért pont a makrók között tárgyaljuk a karaktervizsgáló rutinokat? Több C implementáció makróként valósítja meg ezeket a függvényeket. Erre mutatunk itt be egy szintén nem teljes kör példát azzal a feltétellel, hogy a CHAR_BIT (bitek száma a char típusban ­ lásd LIMITS.H­t!) makró értéke 8.
/* Bitmaszk értékek a lehetséges karaktertípusokra: */ #define _UPPER 0x1 /* Nagybet. */ #define _LOWER 0x2 /* Kisbet. */ #define _DIGIT 0x4 /* Decimális számjegy. */ #define _SPACE 0x8 /* '\t','\r','\n','\v','\f' */ #define _PUNCT 0x10 /* Elválasztó-jel. */

128

ELFELDOLGOZÓ DIREKTÍVÁK

#define _CONTROL 0x20 /* Vezérl karakter. */ #define _BLANK 0x40 /* Szóköz. */ #define _HEX 0x80 /* Hexadecimális számjegy. */ /* Globális tömb, melyben a rendszer mindenegyes kódtábla pozícióra beállította ezeket a biteket: */ extern unsigned char _ctype[]; /* Néhány makró: */ #define islower(_c) (_ctype[_c]&_LOWER) #define isupper(_c) (_ctype[_c]&_UPPER) #define isalpha(_c) (_ctype[_c]&(_UPPER|_LOWER)) #define isdigit(_c) (_ctype[_c]&_DIGIT) #define isalnum(_c) (_ctype[_c]&(_UPPER|_LOWER|_DIGIT)) #define isxdigit(_c) (_ctype[_c]&_HEX) #define isspace(_c) (_ctype[_c]&(_SPACE|_BLANK)) #define isprint(_c) (_ctype[_c]&(_BLANK|_PUNCT|_UPPER|\ _LOWER|_DIGIT))

Megoldandó feladatok: Készítse el a következ függvények makró változatát! · A fejezetben említett tolower­ét és toupper­ét. · A TÍPUSOK ÉS KONSTANSOK szakaszban megírt strcopy­ét, és más karakterlánc kezelkét. Ha az olvasóban felmerült volna az a gondolat, hogy mi van akkor, ha ugyanolyan nev makró és függvény is létezik, akkor arra szeretnénk emlékeztetni, hogy: · Az elfeldolgozás mindig a fordítás eltt történik meg, s így mindenbl makró lesz. · Ha #undef direktívával definiálatlanná tesszük a makrót, akkor attól kezdve csak függvény lesz a forrásszövegben. · Ha a hívásban redundáns zárójelbe zárjuk a makró vagy a függvény nevét, akkor az elfeldolgozó ezt nem fejti ki, tehát bizonyosan függvényhívás lesz belle.
...(makrónév)(paraméterek)...

7.8

Feltételes fordítás

feltételes-fordítás: if-csoport endif-sor if-csoport: #if konstans-kifejezés újsor #ifdef azonosító újsor #ifndef azonosító újsor

C programnyelv
elif-csoportok: elif-csoport elif-csoportok elif-csoport elif-csoport: #elif konstans-kifejezés újsor else-csoport: #else újsor endif-sor: #endif újsor újsor: soremelés

129

A feltételes direktívák szerint kihagyandó forrássorokat az elfeldolgozó törli a forrásszövegbl, s a feltételes direktívák sorai maguk pedig kimaradnak az eredmény fordítási egységbl. A feltételes direktívák által képzett konstrukciót - melyet rögtön bemutatunk egy általános példán mindenképpen be kell fejezni abban a forrásfájlban, amelyben elkezdték.
#if konstans-kifejezés1 <#elif konstans-kifejezés2 > /* . . . */ <#elif konstans-kifejezésN > <#else > #endif

Lássuk a kiértékelést! 1, Ha a konstans-kifejezés1 értéke nem zérus (igaz), akkor a preprocesszor a szekció1 sorait feldolgozza, és az eredményt átadja a fordítónak. A szekció1 természetesen üres is lehet. Ezután az ezen #if-hez tartozó összes többi sort a vonatkozó #endif-fel bezárólag kihagyja, s az #endif-t követ sorral folytatja a munkát az elfeldolgozó. 2, Ha a konstans-kifejezés1 értéke zérus (hamis), akkor a preprocesszor a szekció1-t teljes egészében elhagyja. Tehát nincs makrókifejtés, és nem adja át a feldolgozott darabot a fordítónak! Ezután viszont a következ #elif konstans-kifejezése kiértékelésébe fog, s így tovább. 3, Összesítve az #if-en és az #elif-eken lefelé haladva az a szekció kerül elfeldolgozásra, s ennek eredménye fordításra, melynek konstanskifejezése igaznak bizonyul. Ha egyik ilyen konstans-kifejezés sem igaz, akkor az #else végs-szekciójára vonatkoznak az elbbiekben mondottak.

130

ELFELDOLGOZÓ DIREKTÍVÁK

Az #if . . . #endif konstrukciók tetszleges mélységben egymásba ágyazhatók. Az #if . . . #endif szerkezetbeli konstans-kifejezéseknek korlátozott, egész típusúaknak kell lenniük! Konkrétabban egész konstanst, karakter állandót tartalmazhat a kifejezés, és benne lehet a defined operátor is. Tilos használni viszont benne explicit típuskonverziót, sizeof kifejezést, enumerátort és lebegpontos konstanst, mint normál egész típusú konstans kifejezésekben! Az elfeldolgozó közönséges makróhelyettesítési menettel dolgozza fel a konstans-kifejezéseket. 7.8.1 A defined operátor Makróazonosítók definiáltságának ellenrzésére való, s csak #if és #elif konstans-kifejezéseiben szerepelhet. A
defined(azonosító)

vagy a
defined azonosító

alak a makróazonosító definiáltságára kérdez rá. Miután a válasz logikai érték a defined szerkezetek logikai mveletekkel is kombinálhatók a konstans-kifejezésekben. Például:
#if defined(makro1) && !defined(makro2)

Ha biztosítani szeretnénk azt, hogy a fordítási egységbe egy bizonyos fejfájl (legyen HEADER.H) csak egyszer épüljön be, akkor a fejfájl szövegét következképp kell direktívákba foglalni:
#if !defined(_HEADERH) #define _HEADERH /* Itt van a fejfájl szövege. */ #endif

Ilyenkor akárhány #include is jön a forrásfájlban a HEADER.H fejfájlra, a behozatala csak elször történik meg, mert a további bekapcsolásokat az _HEADERH makró definiáltsága megakadályozza. Nézzünk csak bele néhány szabványos fejfájlba, ott is alkalmazzák ezt a konstrukciót! 7.8.2 Az #ifdef és az #ifndef direktívák Az #ifdef direktíva egy makróazonosító definiáltságára, s az #ifndef viszont a definiálatlanságára kérdez rá, azaz:
#ifdef azonosító



#if defined(azonosító)

C programnyelv
#ifndef azonosító

131

#if !defined(azonosító)

7.9

#line sorvezérl direktíva

#line egész-konstans <"fájlazonosító"> újsor

Jelzi az elfeldolgozónak, hogy a következ forrássor egész-konstans sorszámú, és a fájlazonosító nev fájlból származik. Miután az aktuálisan feldolgozás alatt álló forrásfájlnak is van azonosítója a fájlazonosító paraméter elhagyásakor a #line az aktuális fájlra vonatkozik. A makrókifejtés a #line paramétereiben is megtörténik. Vegyünk egy példát!
/* PELDA.C: a #line direktívára: */ #include #line 4 "PIPI.C" void main(void) { printf("\nA(z) %s fájl %d sorában vagyunk!", __FILE__, __LINE__); #line 12 "PELDA.C" printf("\n"); printf("A(z) %s fájl %d sorában vagyunk!", __FILE__, __LINE__); #line 8 printf("\n"); printf("A(z) %s fájl %d sorában vagyunk!\n", __FILE__, __LINE__); }

Az elállított standard kimenet a következ lehet:
#line 1 "pelda.c" #line 1 "c:\\msdev\\include\\stdio.h" . . . #line 524 "c:\\msdev\\include\\stdio.h" #line 3 "pelda.c" #line 4 "PIPI.C" void main(void) { printf("\nA(z) %s fájl %d sorában vagyunk!", "PIPI.C", 6); #line 12 "PELDA.C" printf("\n"); printf("A(z) %s fájl %d sorában vagyunk!", "PELDA.C", 13); #line 8 "PELDA.C" printf("\n"); printf("A(z) %s fájl %d sorában vagyunk!\n","PELDA.C", 9); }

132

ELFELDOLGOZÓ DIREKTÍVÁK

A #line direktíva tulajdonképpen a __FILE__ és a __LINE__ elredefiniált makrók értékét állítja. Ezek a makróértékek a fordító hibaüzeneteiben jelennek meg. Szóval a direktíva diagnosztikai célokat szolgál. 7.10 #error direktíva
#error újsor

direktíva üzenetet generál, és befejezdik a fordítás. Az üzenet alakja lehet a következ:
Error: fájlazonosító sorszám: Error directive: hibaüzenet

Rendszerint #if direktívában használatos. Például:
#if (SAJAT!=0 && SAJAT!=1) #error A SAJAT 0-nak vagy 1-nek definiálandó! #endif

7.11 #pragma direktívák
#pragma újsor

A direktívák gép és operációs rendszerfüggk. Bennük a #pragma kulcsszót követ szimbólumok mindig objektumai a makrókifejtésnek, és tulajdonképpen speciális fordítói utasítások, s ezek paraméterei. Az elfeldolgozó a fel nem ismert #pragma direktívát figyelmen kívül hagyja.

C programnyelv

133

8 OBJEKTUMOK ÉS FÜGGVÉNYEK
Az azonosítók ,,értelmét" a deklarációk rögzítik. Tudjuk, hogy a deklaráció nem jelent szükségképpen memóriafoglalást. Csak a definíciós deklaráció ilyen.
deklaráció: deklaráció-specifikátorok init-deklarátorlista: init-deklarátor init-deklarátorlista, init-deklarátor init-deklarátor: deklarátor deklarátor=inicializátor

Az inicializátorokkal és inicializátorlistákkal a BEVEZETÉS ÉS ALAPISMERETEK szakasz Inicializálás fejezetében foglalkoztunk. A deklarátorok a deklarálandó neveket tartalmazzák. A deklaráció-specifikátorok típus és tárolási osztály specifikátorokból állnak:
deklaráció-specifikátorok: tárolási-osztály-specifikátor típusspecifikátor típusmódosító

A típusspecifikátorokat a TÍPUSOK ÉS KONSTANSOK szakaszban tárgyaltuk, s ezek közül kivettük a const és a volatile típusmódosítókat. 8.1 Objektumok attribútumai Az objektum egy azonosítható memória területet, mely konstans vagy változó érték(ek)et tartalmaz. Az objektum egyik attribútuma (tulajdonsága) az adattípusa. Tudjuk, hogy van ezen kívül azonosítója (neve) is. Az objektum adattípus attribútuma rögzíti az objektumnak · allokálandó (lefoglalandó) memória mennyiségét és · a benne tárolt információ belsábrázolási formáját. Az objektum neve nem attribútum, hisz különféle hatáskörben több különböz objektumnak is lehet ugyanaz az azonosítója. Az objektum további attribútumait (tárolási osztály, hatáskör, láthatóság, élettartam, stb.) a deklarációja és annak a forráskódban elfoglalt helye határozza meg. Emlékeztetül: a lokális, globális és a bels, küls változókat taglaltuk már a BEVEZETÉS ÉS ALAPISMERETEK szakaszban!

134

OBJEKTUMOK ÉS FÜGGVÉNYEK

8.1.1 Tárolási osztályok Az objektumokhoz rendelt azonosítóknak van legalább · tárolási osztály és · adattípus attribútuma. Szokás e kettt együtt is adattípusnak nevezni. A tárolási osztály specifikátor definíciója:
tárolási-osztály-specifikátor: auto register extern static typedef

A tárolási osztály meghatározza az objektum élettartamát, hatáskörét és kapcsolódását. Egy adott objektumnak csak egy tárolási osztály specifikátora lehet egy deklarációban. A tárolási osztályt a deklaráció forráskódbeli elhelyezése implicit módon rögzíti, de a megfelel tárolási osztály kulcsszó expliciten is beleírható a deklarációba. A kapcsolódással külön fejezetben foglalkozunk. Tárolási osztály kulcsszó nélküli deklarációk esetében a blokkon belül deklarált · objektum mindig auto definíció, és · a függvény pedig extern deklaráció. A függvénydefiníciók és az ezeken kívüli objektum és függvénydeklarációk mind extern, statikus tárolási osztályúak. Kétféle tárolási osztály van. 8.1.1.1 Automatikus (auto, register) tárolási osztály Az ilyen objektumok lokális élettartamúak, és lokálisak a blokk egy adott példányára. Az ilyen deklarációk definíciók is egyben, azaz megtörténik a memóriafoglalás is. Ismeretes, hogy a függvényparaméterek is automatikus tárolási osztályúaknak minsülnek. Rekurzív kód esetén az automatikus objektumok garantáltan különböz memória területen helyezkednek el mindenegyes blokkpéldányra. A C az automatikus objektumokat a program vermében tárolja, s így alapértelmezett kezdértékük "szemét". Expliciten inicializált, lokális automatikus objektum esetében a kezdérték adás viszont mindannyiszor

C programnyelv

135

megtörténik, valahányszor bekerül a vezérlés a blokkba. A blokkon belül definiált objektumok auto tárolási osztályúak, hacsak ki nem írták expliciten az extern vagy a static kulcsszót a deklarációjukban. Csak lokális hatáskör objektumok deklarációjában használható az auto tárolási osztály specifikátor. Az auto kulcsszót tilos küls deklarációban vagy definícióban alkalmazni! Az automatikus tárolási osztályú objektumok lokális élettartamúak és nincs kapcsolódásuk. Miután ez az alapértelmezés az összes lokális hatáskör objektum deklarációjára, nem szokás és szükségtelen expliciten kiírni. Az auto tárolási osztály specifikátor függvényre nem alkalmazható! Az automatikus tárolási osztály speciális válfaja a regiszteres. A register kulcsszó a deklarációban azt jelzi a fordítónak, hogy · a változót nagyon gyakran fogjuk használni, és · kérjük, hogy az illet objektumot regiszterben helyezze el, ha lehetséges. A regiszteres tárolás rövidebb gépi kódú programot eredményez, hisz elmarad a memóriából regiszterbe (és vissza) töltögetés. Emiatt, és mert a regiszter a memóriánál jóval kisebb elérési idej, a szoftver futása is gyorsul. A hardvertl függ ugyan, de valójában csak kevés objektum helyezkedhet el regiszterben, és csak meghatározott típusú változók kerülhetnek oda. A fordító elhagyja a register kulcsszót a felesleges és a nem megfelel típusú deklarációkból, azaz az ilyen változók csak normál, automatikus tárolási osztályúak lesznek. A regiszteres objektumok lokális élettartamúak, és ekvivalensek az automatikus változókkal. Csak lokális változók és függvényparaméterek deklarációjában alkalmazható a register kulcsszó. Küls deklarációban vagy definícióban a register kulcsszót tilos alkalmazni! Függetlenül attól, hogy a register változó igazán regiszterben helyezkedik el, vagy sem, tilos a címére hivatkozni! A globális optimalizálást bekapcsolva a fordító figyelmen kívül hagyja a programozó register igényeit, s saját maga választ regiszter ki-

136

OBJEKTUMOK ÉS FÜGGVÉNYEK

osztást, de minden más a register kulcsszóhoz kapcsolódó szemantikát tekintetbe vesz. Írjunk int prime(int x) függvény, mely eldönti pozitív egész paraméterérl, hogy prímszám­e! A prímszám csak 1­gyel és önmagával osztható maradék nélkül. Próbáljuk meg tehát egész számok szorzataként elállítani. Ha sikerül, akkor nem törzsszámról van szó. A szám prím viszont, ha ez nem megy. Indítunk tehát 2­rl mindig növelgetve egy osz változót, és megpróbáljuk, hogy osztható­e vele maradék nélkül az x. Meddig növekedhet az osz? x négyzetgyökéig, mert a két szorzótényezre bontásnál fordított arányosság van a két tényez között.
int prime(register x){ register osz = 2; if(x < 4) return 1; while(osz*osz <= x){ if(!(x%osz)) return 0; ++osz; if(!(osz&1)) ++osz; } return 1; }

A prime paramétere és az osz lokális változó register int típusú programgyorsítási céllal. A függvény utolsó eltti sorából látszik, hogy legalább a páros számokat nem próbáljuk ki osztóként, miután 2­vel nem volt maradék nélkül osztható az x. Az olvasóra bízzuk, hogy kísérje meg még gyorsítani az algoritmust! Készítsünk programot, mely megállapítja egy valós számsorozat átlagát és azt, hogy hány átlagnál kisebb és nagyobb eleme van a sorozatnak! A valós számokat a szabvány bemenetrl kell beolvasni! Egy sorban egyet! A sorozat megadásának végét jelentse üres sor érkezése a bemenetrl! Kezdjük a kódolást az int lebege(char s[]) függvénnyel, mely megállapítja karakterlánc paraméterérl, hogy formálisan helyes lebegpontos szám­e! A karaktertömb elején lev fehér karaktereket át kell lépni, és a numerikus rész ugyancsak fehér, vagy lánczáró zérus karakterrel zárul. A lebegpontos számnak ki kell egyébként elégítenie a lebegpontos konstans írásszabályát!
#include int lebege(char s[]){ int i=0, kezd; /* Fehér karakterek átlépése a lánc elején: */ while(isspace(s[i])) ++i; /* A mantissza eljele: */

C programnyelv

137

if(s[i]=='+'||s[i]=='-') ++i; kezd=i; /* A szájegyek itt kezddnek. */ /* A mantissza egész része: */ while(isdigit(s[i])) ++i; /* A mantissza tört része: */ if(s[i]=='.') ++i; while(isdigit(s[i])) ++i; /* Nincs számjegy, vagy csak egy . van: */ if(i==kezd||kezd+1==i&&s[kezd]=='.') return 0; /* Kitev rész: */ if(toupper(s[i])=='E'){ ++i; if(s[i]=='+'||s[i]=='-')++i; /* Egy számjegynek lennie kell a kitevben! */ if(!isdigit(s[i])) return 0; while(isdigit(s[i])) ++i;} /* Vége: */ if(isspace(s[i])||!s[i]) return 1; else return 0; }

8.1.1.2 Statikus (static, extern) tárolási osztály A statikus tárolási osztályú objektumok kétfélék: · blokkra lokálisak, vagy · blokkokon át külsk. Az ilyen tárolási osztályú objektumok statikus élettartamúak. Bárhogyan is: megrzik értéküket az egész program végrehajtása során, akárhányszor is hagyja el a vezérlés az ket tartalmazó blokkot, és tér oda vissza. Függvényen, blokkon belül úgy definiálható statikus tárolási osztályú változó, hogy a deklarációjába ki kell expliciten írni a static kulcsszót. A static kulcsszavas deklaráció definíció. Készítsünk double gyujto(double a) függvényt, mely gyjti aktuális paraméterei értékét! Mindig az eddig megállapított összeget adja vissza. Ne legyen ,,bamba", azaz ne lehessen vele ,,áttörni" az ábrázolási határokat! A lebegpontos túl, vagy alulcsordulás futásidej hiba!
#include double gyujto(double a){ static double ossz=0.0; if(a<0.0&&-(DBL_MAX+a)ossz) ossz+=a; return ossz; }

138

OBJEKTUMOK ÉS FÜGGVÉNYEK

Látszik, hogy nincs összegzés, ha az ossz ­DBL_MAX­hoz, vagy +DBL_MAX­hoz a távolságon belülre kerül. Rekurzív kódban a statikus objektum állapota garantáltan ugyanaz minden függvénypéldányra. Explicit inicializátorok nélkül a statikus változók minden bitje zérus kezdértéket kap. Az implicit és az explicit inicializálás még lokális statikus objektum esetén is csak egyszer történik meg, a program indulásakor. A gyujto­ben teljesen felesleges zérussal inicializálni a statikus ossz változót, hisz implicit módon is ez lenne a kezdértéke. A függvénydefiníciókon kívül elhelyezett, tárolási osztály kulcsszó nélküli deklarációk küls, statikus tárolási osztályú objektumokat definiálnak, melyek globálisak az egész programra nézve. A küls objektumok statikus élettartamúak. Az explicit módon extern tárolási osztályúnak deklaráltak olyan objektumokat deklarálnak, melyek definíciója nem ebben a fordítási egységben van, vagy befoglaló hatáskörben található.
extern int MasholDefinialt; /* Más ford. egységben */ void main(){ int IttDefiniált; { extern int IttDefiniált; /* A befoglaló hatáskörbeli IttDefiniált-ra való hivatkozás. */ } }

A küls kapcsolódást jelölend az extern függvény és objektum fájl és lokális hatáskör deklarációiban használható. Fájl hatáskör változók és függvények esetében ez az alapértelmezés, tehát expliciten nem szokás kiírni. Az extern kulcsszó explicit kiírása tilos a változó definiáló deklarációjában! A küls objektumok és a függvények is deklarálhatók static­nek, amikor is lokálissá válnak az ket tartalmazó fordítási egységre, és minden ilyen deklaráció definíció is egyben. Folytatva a példánkat: a szabvány bemenetrl érkez, valós számokat valahol tárolni kéne, mert az átlag csak az összes elem beolvasása után állapítható meg. Ez után újra végig kell járni a számokat, hogy kideríthessük, hány átlag alatti és feletti van köztük. A változatosság kedvéért, és mert a célnak tökéletesen megfelel, használjunk vermet a letároláshoz, melyet és kezel függvényeinek definícióit

C programnyelv

139

helyezzük el a DVEREM.C forrásfájlban, és a más fordítási egységbl is hívható függvények prototípusait tegyük be a DVEREM.H fejfájlba! Egy témakör adatait és kezel függvényeit egyébként is szokás a C­ ben külön forrásfájlban (úgy nevezett implementációs fájlban) elhelyezni, vagy a lefordított változatot külön könyvtárfájlba tenni. A dologhoz mindig tartozik egy fejfájl is, mely tartalmazza legalább a témakör más forrásmodulból is elérhet adatainak deklarációit, és kezel függvényeinek prototípusait. Implementációs fájl esetén a fejfájlban még típusdefiníciók, szimbolikus állandók, makrók, stb. szoktak lenni.
/* DVEREM.H: double verem push, pop és clear függvényenek prototípusai. */ int clear(void); double push(double x); double pop(void); /* DVEREM.C: double verem push, pop és clear függvényekkel. */ #define MERET 128 /* A verem mérete. */ static int vmut; /* A veremmutató. */ static double v[MERET]; /* A verem. */ int clear(void){ vmut=0; return MERET; } double push(double x){ if(vmut0) return v[--vmut]; else return 0.; }

A vmut veremmutató, és a v verem statikus ugyan, de lokális a DVEREM.C fordítási egységre. Látszik, hogy a push x paraméterével tölti a vermet, és sikeres esetben ezt is adja vissza. Ha a verem betelt, más érték jön vissza tle. A pop visszaszolgáltatja a legutóbb betett értéket, ill. az üres verembl mindig zérussal tér vissza. A clear törli a veremmutatót, s ez által a vermet, és visszaadja a verem maximális méretét. Kódoljuk le végre az eredetileg kitzött feladatot!
/* PELDA20.C: Valós számok átlaga, és az ez alatti, ill. feletti elemek száma. */ #include #include /* Az atof miatt! */ #define INP 60 /* Az input puffer mérete. */ int getline(char s[],int lim); #include int lebege(char s[]);

140

OBJEKTUMOK ÉS FÜGGVÉNYEK

#include double gyujto(double a); #include "DVEREM.H" void main(void){ int max=clear(), /* A verem max. mérete. */ i=0, alatt, felett; char s[INP+1]; /* Az input puffer. */ double a; printf( "Számsorozat átlaga alatti és feletti " "elemeinek száma.\nA megadást üres " "sorral kell befejezni!\n\n"); while( printf("%4d. elem: ", i+1), i0) if(lebege(s)){ push(a=atof(s)); printf("Az összeg:%30.6f\n\n", a=gyujto(a)); ++i;} printf( "\nAz átlag: %30.6f\n", a/=i); for(max=alatt=felett=0; maxa) ++felett; } printf("Az átlag alattiak száma: %8d.\n" "Az átlag felettiek száma:%8d.\n", alatt, felett); }

Vigyázat: a példa a PELDA20.C­bl és a DVEREM.C­bl képzett prodzsekt segítségével futtatható csak! 8.1.2 Élettartam (lifetime, duration) Az élettartam attribútum szorosan kötdik a tárolási osztályhoz, s az a periódus a program végrehajtása közben, míg a deklarált azonosítóhoz objektumot allokál a fordító a memóriában, azaz amíg a változó vagy a függvény létezik. Megkülönböztethetünk · fordítási idej és · futásidej objektumokat. A változók például a típusoktól és a típusdefinícióktól eltéren futás idben valós, allokált memóriával rendelkeznek. Három fajta élettartam van. 8.1.2.1 Statikus (static vagy extern) élettartam Az ilyen objektumokhoz a memória hozzárendelés a program futásának megkezddésekor történik meg, s az allokáció marad is a program befejezdéséig. Minden függvény statikus élettartamú objektum bárhol is defi-

C programnyelv

141

niálják ket. Az összes fájl hatáskör változó is ilyen élettartamú. Más változók a static vagy az extern tárolási osztály specifikátorok explicit megadásával tehetk ilyenné. A statikus élettartamú objektumok minden memória bitje (a függvényektl eltekintve) zérus kezdértéket kap explicit inicializálás hiányában. Ne keverjük össze a statikus élettartamot a fájl (globális) hatáskörrel, ui. egy objektum lokális hatáskörrel is lehet statikus élettartamú, csak deklarációjában meg kell adni expliciten a static tárolási osztály kulcsszót. 8.1.2.2 Lokális (auto vagy register) élettartam Ezek az objektumok akkor jönnek létre (allokáció) a veremben vagy regiszterben, amikor a vezérlés belép az ket magába foglaló blokkba vagy függvénybe, s meg is semmisülnek (deallokáció), mihelyt kikerül a vezérlés innét. A lokális élettartamú objektumok lokális hatáskörek, és mindig explicit inicializálásra szorulnak, hisz létrejövetelük helyén ,,szemét" van. Ne feledjük, hogy a függvényparaméterek is lokális élettartamúak! Az auto tárolási osztály specifikátor deklarációban való kiírásával expliciten lokális élettartamúvá tehetünk egy változót, de erre többnyire semmi szükség sincs, mert blokkon vagy függvényen belül deklarált változók esetében az alapértelmezett tárolási osztály amúgy is az auto. A lokális élettartamú objektum egyben lokális hatáskör is, hisz az t magába foglaló blokkon kívül nem létezik. A dolog megfordítása nem igaz, mert lokális hatáskör objektum is lehet statikus élettartamú. Ha egy regiszterben is elfér változót (például char, short, stb. típusút) expliciten register tárolási osztályúnak deklarálunk, akkor a fordító ehhez hozzáérti automatikusan az auto kulcsszót is, hisz a változókat csak addig tudja regiszterben elhelyezni, míg azok el nem fogynak, s ezután a veremben allokál nekik memóriát. 8.1.2.3 Dinamikus élettartam Az ilyen objektumokhoz a C­ben például a malloc függvénnyel rendelhetünk memóriát a heap­en, amit aztán a free­vel felszabadíthatunk. Miután a memóriaallokáláshoz könyvtári függvényeket használunk, és ezek nem részei a nyelvnek, így a C­ben nincs is dinamikus élettartam igazából. A C++­ban ugyanezen funkciókra megalkották a new és a delete operátorokat, melyek részei a nyelvnek.

142

OBJEKTUMOK ÉS FÜGGVÉNYEK

8.1.3 Hatáskör (scope) és láthatóság (visibility) A hatáskör ­ érvényességi tartománynak is nevezik ­ az azonosító azon tulajdonsága, hogy vele az objektumot a program mely részébl érhetjük el. Ez is a deklaráció helyétl és magától a deklarációtól függ attribútum. Felsoroljuk ket! 8.1.3.1 Blokk (lokális, bels) hatáskör A deklarációs ponttól indul és a deklarációt magába foglaló blokk végéig tart. Az ilyen hatáskör változókat szokás bels változóknak is nevezni. Lokális hatáskörek a függvények formális paraméterei is, s hatáskörük a függvénydefiníció teljes blokkja. A blokk hatáskör azonosító hatásköre minden a kérdéses blokkba beágyazott blokkra is kiterjed. Például:
int fv(float lo){ double szamar; /* A lokális hatáskör itt indul. */ /* . . . */ long double oszver; /* Az oszver hatásköre innét (deklarációs pont) indul és a függvénydefiníció végéig tart. Szóval nem lehetne a szamar változót az oszver-rel inicializálni. */ if (/* feltétel */){ char lodarazs = 'l'; /* A lodarazs hatásköre ez a bels blokk. */ /* . . . */ } /* A lodarazs hatáskörének vége. */ /* . . . */ } /* A lo, szamar és oszver hatáskörének vége. */

8.1.3.2 Függvény hatáskör Ilyen hatásköre csak az utasítás címkének van. Az utasítás címke ezen az alapon: függvényen belüli egyedi azonosító, melyet egy olyan végrehajtható utasítás elé kell írni kettspontot közbeszúrva, melyre el kívánunk ágazni. Például:
int fv(float k){ int i, j; /* . . . */ cimke: utasítás; /* . . . */ if (/* feltétel */) goto cimke; /* . . . */ }

8.1.3.3 Függvény prototípus hatáskör Ilyen hatásköre a prototípusban deklarált paraméterlista azonosítóinak van, melyek tehát a függvény prototípussal be is fejezdnek. Például a

C programnyelv

143

következ függvénydefinícióban az i, j és k azonosítóknak van függvény prototípus hatásköre:
void fv(int i, char j, float k);

Az i, j és k ilyen megadásának semmi értelme sincs. Az azonosítók teljesen feleslegesek. A
void fv(int, char, float);

ugyanennyit ,,mondott" volna. Függvény prototípusban neveket akkor célszer használni, ha azok leírnak valamit. Például a
double KamatOsszeg(double osszeg, double kamat, int evek);

az osszeg kamatos kamatát közli evek évre. 8.1.3.4 Fájl (globális, küls) hatáskör A minden függvény testén kívül deklarált azonosítók rendelkeznek ilyen hatáskörrel, mely a deklarációs pontban indul és a forrásfájl végéig tart. Ez persze azt is jelenti, hogy a fájl hatáskör objektumok a deklarációs pontjuktól kezdve minden függvénybl és blokkból elérhetk. A globális változókat szokás küls változóknak is nevezni. Például a g1, g2 és g3 változók ilyenek:
int g1 = 7; /* g1 fájl hatásköre innét indul. */ void main(void) { /* ... */ } float g2; /* g2 fájl hatásköre itt startol. */ void fv1(void) { /* ... */ } double g3 = .52E-40; /* Itt kezddik g3 hatásköre */ void fv2(void) { /* ... */ } /* Itt van vége a forrásfájlnak és a g1, g2 és g3 küls változók hatáskörének. */

8.1.3.5 Láthatóság A forráskód azon régiója egy azonosítóra vonatkozóan, melyben legális módon elérhet az azonosítóhoz kapcsolt objektum. A hatáskör és a láthatóság többnyire fedik egymást, de bizonyos körülmények között egy objektum ideiglenesen rejtetté válhat egy másik ugyanilyen nev azonosító feltnése miatt. A rejtett objektum továbbra is létezik, de egyszeren az azonosítójával hivatkozva nem érhet el, míg a másik ugyanilyen nev azonosító hatásköre le nem jár. Például:
{ int i; char c = 'z'; /* Az i és c hatásköre indul. */ i = 3; /* int i-t értük el. */ /* . . . */ { double i = 3.5e3; /* double i hatásköre itt kezddik, s elrejti int i-t, bár */

144
/* . . . */ c = 'A'; } } ++i;

OBJEKTUMOK ÉS FÜGGVÉNYEK
/* hatásköre nem sznik meg. */ /* char c látható és hatásköre itt is tart. */ /* A double i hatáskörének vége */ /* int i és c hatáskörben és láthatók. */ /* int i és char c hatáskörének vége. */

8.1.3.6 Névterület (name space) Az a ,,hatáskör", melyen belül az azonosítónak egyedinek kell lennie. Más névterületen konfliktus nélkül létezhet ugyanilyen azonosító, a fordító képes megkülönböztetni ket. A névterület fajtákat a STRUKTÚRÁK ÉS UNIÓK tárgyalása után tisztázzuk majd! 8.1.4 Kapcsolódás (linkage) A kapcsolódást csatolásnak is nevezik. A végrehajtható program úgy jön létre, hogy · több, különálló fordítási egységet fordítunk, · aztán a kapcsoló-szerkesztvel (linker) összekapcsoltatjuk az eredmény .OBJ fájlokat, más meglév tárgymodulokat és a könyvtárakból származó tárgykódokat. Probléma akkor van, ha ugyanaz az azonosító különböz hatáskörökkel deklarált - például más-más forrásfájlban - vagy ugyanolyan hatáskörrel egynél többször is deklarált. A kapcsoló-szerkesztés az a folyamat, mely az azonosító minden elfordulását korrekt módon egy bizonyos objektumhoz vagy függvényhez rendeli. E folyamat során minden azonosító kap egy kapcsolódási attribútumot a következ lehetségesek közül: · küls (external) kapcsolódás, · bels (internal) kapcsolódás vagy · nincs (no) kapcsolódás. Ezt az attribútumot a deklarációk elhelyezésével és formájával, ill. a tárolási osztály (static vagy extern) explicit vagy implicit megadásával határozzuk meg. Lássuk a különféle kapcsolódások részleteit!

C programnyelv

145

A küls kapcsolódású azonosító minden példánya ugyanazt az objektumot vagy függvényt reprezentálja a programot alkotó minden forrásfájlban és könyvtárban. A bels kapcsolódású azonosító ugyanazt az objektumot vagy függvényt jelenti egy és csak egy fordítási egységben (forrásfájlban). A bels kapcsolódású azonosítók a fordítási egységre, a küls kapcsolódásúak viszont az egész programra egyediek. A küls és bels kapcsolódási szabályok a következk: · Bármely objektum vagy függvényazonosító fájl hatáskörrel bels kapcsolódású, ha deklarációjában expliciten elírták a static tárolási osztályt. · Az explicit módon extern tárolási osztályú objektum vagy függvényazonosítónak ugyanaz a kapcsolódása, mint bármely látható fájl hatáskör deklarációjának. Ha nincs ilyen látható fájl hatáskör deklaráció, akkor az azonosító küls kapcsolódású lesz. · Ha függvényt explicit tárolási osztály specifikátor nélkül deklarálnak, akkor kapcsolódása olyan lesz, mintha kiírták volna az extern kulcsszót. · Ha fájl hatáskör objektumazonosítót deklarálnak tárolási osztály specifikátor nélkül, akkor az azonosító küls kapcsolódású lesz. A fordítási egység bels kapcsolódásúnak deklarált azonosítójához egy és csak egy küls definíció adható meg. A küls definíció olyan küls deklaráció, mely az objektumhoz vagy függvényhez memóriát is rendel. Ha küls kapcsolódású azonosítót használunk kifejezésben (a sizeof operandusától eltekintve), akkor az azonosítónak csak egyetlen küls definíciója létezhet az egész programban. A kapcsolódás nélküli azonosító egyedi entitás. Ha a blokkban az azonosító deklarációja nem vonja maga után az extern tárolási osztály specifikátort, akkor az azonosítónak nincs kapcsolódása, és egyedi a függvényre. A következ azonosítóknak nincs kapcsolódása: · Bármely nem objektum vagy függvénynévvel deklarált azonosítónak. Ilyen például a típusdefiníciós (typedef) azonosító. · A függvényparamétereknek. · Explicit extern tárolási osztály specifikátor nélkül deklarált, blokk hatáskör objektumazonosítóknak.

146

OBJEKTUMOK ÉS FÜGGVÉNYEK

8.2 Függvények A függvényekkel kapcsolatos alapfogalmakat tisztáztuk már a BEVEZETÉS ÉS ALAPISMERETEK szakaszban, de fussunk át rajtuk még egyszer! A függvénynek kell legyen definíciója, és lehetnek deklarációi. A függvény definíciója deklarációnak is minsül, ha megelzi a forrásszövegben a függvényhívást. A függvénydefinícióban van a függvény teste, azaz az a kód, amit a függvény meghívásakor végrehajt a processzor. A függvénydefiníció rögzíti a függvény nevét, visszatérési értékének típusát, tárolási osztályát és más attribútumait. Ha a függvénydefinícióban a formális paraméterek típusát, sorrendjét és számát is elírják, függvény prototípusnak nevezzük. A függvény deklarációjának meg kell elznie a függvényhívást, melyben aktuális paraméterek vannak. Ez az oka annak, hogy a forrásfájlban a szabvány függvények hívása eltt behozzuk a prototípusaikat tartalmazó fejfájlokat (#include). A függvényparamétereket argumentumoknak is szokták nevezni. A függvényeket a forrásfájlokban szokás definiálni, vagy elrefordított könyvtárakból lehet bekapcsoltatni (linkage). Egy függvény a programban többször is deklarálható, feltéve, hogy a deklarációk kompatibilisek. A függvény prototípusok használata a C-ben ajánlatos (a C++ meg úgy is kötelezen elírja), mert a fordítót így látjuk el elegend információval ahhoz, hogy ellenrizhesse · a függvény nevét (a függvények adott azonosító), · a paraméterek számát, típusát és sorrendjét (típuskonverzió lehetséges), valamint · a függvény által visszaadott érték típusát (típuskonverzió itt is lehet). A függvényhívás átruházza a vezérlést a hívó függvénybl a hívott függvénybe úgy, hogy az aktuális paramétereket is ­ ha vannak ­ átadja érték szerint. Ha a hívott függvényben return utasításra ér a végrehajtás, akkor visszakapja a vezérlést a hívó függvény egy visszaadott értékkel együtt (ha megadtak ilyet!). Egy függvényre a programban csak egyetlen definíció lehetséges. A deklarációk (prototípusok) kötelesek egyezni a definícióval.

C programnyelv

147

8.2.1 Függvénydefiníció A függvénydefiníció specifikálja a függvény nevét, a formális paraméterek típusát, sorrendjét és számát, valamint a visszatérési érték típusát, a függvény tárolási osztályát és más attribútumait. A függvénydefinícióban van a függvény teste is, azaz a használatos lokális változók deklarációja, és a függvény tevékenységét megszabó utasítások. A szintaktika:
fordítási-egység: küls-deklaráció fordítási-egység küls-deklaráció küls-deklaráció: függvénydefiníció deklaráció függvénydefiníció: deklarátor összetett-utasítás deklarátor: direkt-deklarátor direkt-deklarátor: direkt-deklarátor(paraméter-típus-lista) direkt-deklarátor() deklarációlista: deklaráció deklarációlista deklaráció

A küls-deklarációk hatásköre a fordítási egység végéig tart. A külsdeklaráció szintaktikája egyezik a többi deklarációéval, de függvényeket csak ezen a szinten szabad definiálni, azaz: tilos függvényben másik függvényt definiálni! A deklaráció és az azonosítólista definíciók a TÍPUSOK ÉS KONSTANSOK szakasz Deklaráció fejezetében megtalálhatók. A mutatókat a következ szakasz tartalmazza. A függvénydefinícióbeli összetett-utasítás a függvény teste, mely tartalmazza a használatos lokális változók deklarációit, a külsleg deklarált tételekre való hivatkozásokat, és a függvény tevékenységét megvalósító utasításokat. Az opcionális deklaráció-specifikátorok és a kötelezen megadandó deklarátor együtt rögzítik a függvény visszatérési érték típusát és nevét. A deklarátor természetesen függvénydeklarátor, azaz a függvénynév és az t követ zárójel pár. Az els direkt-deklarátor(paraméter-típus-lista) alak a függvény új (modern) stílusú definícióját teszi lehetvé. A deklarátor szintaktikában szerepl direkt-deklarátor a modern stílus szerint a definiálás alatt álló függvény nevét rögzíti, és a kerek zárójelben álló pa-

148

OBJEKTUMOK ÉS FÜGGVÉNYEK

raméter-típus-lista specifikálja az összes paraméter típusát. Ilyen deklarátor tulajdonképpen a függvény prototípus is. Például:
char fv(int i, double d){ /* . . . */ }

A második direkt-deklarátor() forma a régi stílusú definíció:
char fv(i, d) int i; double d; { /* . . . */ }

A továbbiakban csak az új stílusú függvénydefinícióval foglalkozunk, s nem emlegetjük tovább a régit!
deklaráció-specifikátorok: tárolási-osztály-specifikátor típusspecifikátor típusmódosító típusmódosító: (a következk egyike!) const volatile

A tárolási-osztály-specifikátorok és a típuspecifikátorok definíciói a TÍPUSOK ÉS KONSTANSOK szakasz Deklaráció fejezetében megtekinthetk! 8.2.1.1 Tárolási osztály Függvénydefinícióban két tárolási osztály kulcsszó használható: az extern vagy a static. A függvények alapértelmezés szerint extern tárolási osztályúak, azaz normálisan a program minden forrásfájljából elérhetk, de explicit módon is deklarálhatók extern­nek. Ha a függvény deklarációja tartalmazza az extern tárolási osztály specifikátort, akkor az azonosítónak ugyanaz a kapcsolódása, mint bármely látható, fájl hatáskör ugyanilyen küls deklarációnak, és ugyanazt a függvényt jelenti. Ha nincs ilyen fájl hatáskör, látható deklaráció, akkor az azonosító küls kapcsolódású. A fájl hatáskör, tárolási osztály specifikátor nélküli azonosító mindig küls kapcsolódású. A küls kapcsolódás azt jelenti, hogy az azonosító minden példánya ugyanarra a függvényre hivatkozik, azaz az explicit vagy implicit módon extern tárolási osztályú függvény a program minden forrásfájljában látható. Az extern­tl különböz tárolási osztályú, blokk hatáskör függvénydeklaráció hibát generál.

C programnyelv

149

A függvény explicit módon deklarálható azonban static-nek is, amikor is a rá való hivatkozást az t tartalmazó forrásfájlra korlátozzuk, azaz a függvény bels kapcsolódású, és csak a definícióját tartalmazó forrásmodulban látható. Az ilyen függvény legels bekövetkez deklarációjában (ha van ilyen!) és definíciójában is ki kell írni a static kulcsszót. Akármilyen esetrl is van szó azonban, a függvény mindig a definíciós vagy deklarációs pontjától a forrásfájl végéig látható magától. 8.2.1.2 A visszatérési érték típusa A visszatérési érték típusa meghatározza a függvény által szolgáltatott érték méretét és típusát. A függvénydefiníció metanyelvi meghatározása az elhagyható deklaráció-specifikátorokkal kezddik. Ezek közül tulajdonképpen a típusspecifikátor felel meg a visszatérési érték típusának. E meghatározásokat nézegetve látható, hogy a visszaadott érték típusa bármi lehet eltekintve a tömbtl és a függvénytl (az ezekre mutató mutató persze megengedett). Lehet valamilyen aritmetikai típusú, lehet void (nincs visszaadott érték), de el is hagyható, amikor is alapértelmezés az int. Lehet struktúra, unió vagy mutató is, melyekrl majd késbbi szakaszokban lesz szó. A függvénydefinícióban elírt visszaadott érték típusának egyeznie kell a programban bárhol elforduló, e függvényre vonatkozó deklarációkban megadott visszatérési érték típussal. A meghívott függvény akkor ad viszsza értéket a hívó függvénynek a hívás pontjára, ha a processzor kifejezéssel ellátott return utasítást hajt végre. A fordító természetesen elbb kiértékeli a kifejezést, és konvertálja ­ ha szükséges ­ az értéket a visszaadott érték típusára. A void visszatérésnek deklarált függvénybeli kifejezéssel ellátott return figyelmeztet üzenetet eredményez, és a fordító nem értékeli ki a kifejezést. Vigyázat! A függvény típusa nem azonos a visszatérési érték típusával. A függvény típusban ezen kívül benne van még a paraméterek · száma, · típusai és · sorrendje is! 8.2.1.3 Formális paraméterdeklarációk A függvénydefiníció metanyelvi meghatározásából következen a modern stílusú direkt-deklarátor(paraméter-típus-lista) alakban, a zárójelben

150

OBJEKTUMOK ÉS FÜGGVÉNYEK

álló paraméter-típus-lista vesszvel elválasztott paraméterdeklarációk sorozata.
paraméter-típus-lista: paraméterlista paraméterlista, ... paraméterlista: paraméterdeklaráció paraméterlista, paraméterdeklaráció paraméterdeklaráció: deklaráció-specifikátor deklarátor deklaráció-specifikátor absztrakt-deklarátor: mutató direkt-absztrakt-deklarátor: (absztrakt-deklarátor) [] ()

A paraméterdeklaráció nem tartalmazhat más tárolási-osztály-specifikátort, mint a register­t. A deklaráció-specifikátor szintaktikabeli típusspecifikátor elhagyható, ha a típus int, és egyébként megadják a register tárolási osztály specifikátort. Összesítve a formális paraméterlista egy elemének formája a következ:
típusspecifikátor

Az auto­nak deklarált függvényparaméter fordítási hiba! A C szabályai szerint a paraméter lehet bármilyen aritmetikai típusú. Lehet akár tömb is, de függvény nem (az erre mutató mutató persze megengedett). A paraméter lehet természetesen struktúra, unió vagy mutató is, melyekrl majd késbbi szakaszokban lesz szó. A paraméterlista lehet void is, ami nincs paraméter jelentés. A formális paraméterazonosítók nem definiálhatók át a függvény testének küls blokkjában, csak egy ebbe beágyazott bels blokkban, azaz a formális paraméterek hatásköre és élettartama a függvénytest teljes legküls blokkja. Az egyetlen rájuk is legálisan alkalmazható tárolási osztály specifikátor a register. Például:
int f1(register int i){/* ... */}/* Igény regiszteres paraméter átadásra. */

A const és a volatile módosítók használhatók a formális paraméter deklarátorokkal. Például a
void f0(double p1, const char s[]){

C programnyelv
/* . . . */ s[0]='A';

151
/* Szintaktikai hiba. */}

const­nak deklarált formális paramétere nem lehet balérték a függvény testében, mert hibaüzenetet okoz. Ha nincs átadandó paraméter, akkor a paraméterlista helyére a definícióban és a prototípusban a void kulcsszó írandó:
int f2(void){/* ... */} /* Nincs paraméter. */

Ha van legalább egy formális paraméter a listában, akkor az ,...-ra is végzdhet:
int f3(char str[], ...){/* ... */}/* Változó számú vagy típusú paraméter. */

Az ilyen függvény hívásában legalább annyi aktuális paramétert meg kell adni, mint amennyi formális paraméter a ,... eltt van, de természetesen ezeken túl további aktuális paraméterek is elírhatók. A ,... eltti paraméterek típusának és sorrendjének ugyanannak kell lennie a függvény deklarációiban (ha egyáltalán vannak), mint a definíciójában. A függvény aktuális paraméterei típusának az esetleges szokásos konverzió után hozzárendelés kompatibilisnek kell lennie a megfelel formális paraméter típusokra. A ,... helyén álló aktuális paramétereket nem ellenrzi a fordító. Az STGARG.H fejfájlban vannak olyan makrók, melyek segítik a felhasználói, változó számú paraméteres függvények megalkotását! A témára visszatérünk még a MUTATÓK kapcsán! 8.2.1.4 A függvény teste A függvény teste elhagyható deklarációs és végrehajtható utasításokból álló összetett utasítás, azaz az a kód, amit a függvény meghívásakor végrehajt a processzor.
összetett-utasítás: { }

A függvénytestben deklarált változók lokálisak, auto tárolási osztályúak, ha másként nem specifikálták ket. Ezek a lokális változók akkor jönnek létre, mikor a függvényt meghívják, és lokális inicializálást hajt rajtuk végre a fordító. A függvény meghívásakor a vezérlést a függvénytest els végrehajtható utasítása kapja meg. void-ot visszaadó függvény blokkjában aztán a végrehajtás addig folytatódik, míg return utasítás nem következik vagy a függvény blokkját záró }-re nem kerül a vezérlés. Ezután a hívási ponttól folytatódik a program végrehajtása.

152

OBJEKTUMOK ÉS FÜGGVÉNYEK

A ,,valamit" szolgáltató függvényben viszont lennie kell legalább egy return kifejezés utasításnak, és visszatérés eltt rá is kell, hogy kerüljön a vezérlés. A visszaadott érték meghatározatlan, ha a processzor nem hajt végre return utasítást, vagy a return utasításhoz nem tartozott kifejezés. A kifejezés értékét szükséges esetben hozzárendelési konverziónak veti alá a fordító, ha a visszaadandó érték típusa eltér a kifejezésétl. 8.2.2 Függvény prototípusok A függvénydeklaráció megelzi a definíciót, és specifikálja a függvény nevét, a visszatérési érték típusát, tárolási osztályát és a függvény más attribútumait. A függvénydeklaráció akkor válik prototípussá, ha benne megadják az elvárt paraméterek típusát, sorrendjét és számát is. Összegezve: a függvény prototípus csak abban különbözik a definíciótól, hogy a függvény teste helyén egy ; van. C­ben ugyan nem kötelez, de tegyük magunknak kötelezvé a függvény prototípus használatát, mert ez a következket rögzíti: · A függvény int­tl különböz visszatérési érték típusát. · Ezt az információt a fordító a függvényhívások paramétertípus és szám megfeleltetés ellenrzésén túl konverziók elvégzésére is felhasználja. A paraméterek konvertált típusa határozza meg azokat az aktuális paraméter értékeket, melyek másolatait a függvényhívás teszi ki a verembe. Gondoljuk csak meg, hogyha az int­ként kirakott aktuális paraméter értéket a függvény double­nek tekintené, akkor nem csak e paraméter félreértelmezésérl van szó, hanem az összes többi ezt követ is "elcsúszik"! A prototípussal a fordító nem csak a visszatérési érték és a paraméterek típusegyezését tudja ellenrizni, hanem az attribútumokat is. Például a static tárolási osztályú prototípus hatására a függvénydefiníciónak is ilyennek kell lennie. A függvénydefiníció módosítóinak egyeznie kell a függvénydeklarációk módosítóival! A prototípusbeli azonosító hatásköre a prototípus. Prototípus adható változó számú paraméterre, ill. akkor is, ha paraméter egyáltalán nincs. A komplett paraméterdeklarációk (int a) vegyíthetk az absztraktdeklarátorokkal (int) ugyanabban a deklarációban. Például:
int add(int a, int);

C programnyelv

153

A paraméter típusának deklarálásakor szükség lehet az adattípus nevének feltüntetésére, mely a típusnév segítségével érhet el. A típusnév az objektum olyan deklarációja, melybl hiányzik az azonosító. A metanyelvi leírás:
típusnév: típusspecifikátor-lista absztrakt-deklarátor: mutató direkt-absztrakt-deklarátor: (absztrakt-deklarátor) [] ()

Az absztrakt-deklarátorban mindig megállapítható az a hely, ahol az azonosítónak lennie kellene, ha a konstrukció deklaráción belüli deklarátor lenne. A következ típusnevek jelentése: int, 10 elem int tömb és nem meghatározott elemszámú int tömb:
int, int [10], int []

Lássuk be, hogy a függvény prototípus a kód dokumentálására is jó! Szinte rögtön tudunk mindent a függvényrl:
void strcopy(char cel[], char forras[]);

A
fv(void);

olyan függvény prototípusa, melynek nincsenek paraméterei. Normál esetben a függvény prototípus olyan függvényt deklarál, mely fix számú paramétert fogad. Lehetség van azonban változó számú vagy típusú paraméter átadására is. Az ilyen függvény prototípus paraméterlistája ...-tal végzdik:
fv(int, long, ...);

A fixen megadott paramétereket fordítási idben ellenrzi a fordító, s a változó számú vagy típusú paramétert viszont a függvény hívásakor típusellenrzés nélkül adja át a veremben. Az STGARG.H fejfájlban vannak olyan makrók, melyek segítik a felhasználói, változó számú paraméteres függvények megalkotását! A témára visszatérünk még a MUTATÓK kapcsán! Nézzünk néhány példa prototípust!
int f(); /* int-et visszaadó függvény, melynek paramétereirl nincs információnk. */

154
int f1(void);

OBJEKTUMOK ÉS FÜGGVÉNYEK

/* Olyan int-et szolgáltató függvény, melynek nincsenek paraméterei. */ int f2(int, long); /* int-et visszaadó függvény, mely elsnek egy int, s aztán egy long paramétert fogad. */ int pascal f3(void);/*Paraméter nélküli, int-et szolgáltató pascal függvény. */ int printf(const char [], ...);/* int-tel visszatér függvény egy fix, és nem meghatározott számú vagy típusú paraméterrel. */

Készítsünk programot, mely megállapítja az ÉÉÉÉ.HH.NN alakú karakterláncról, hogy érvényes dátum­e!
/* PELDA21.C: Dátumellenrzés. */ #include #include /* Az atoi miatt! */ #define INP 11 /* Az input puffer mérete. */ #include /* Az isdigit miatt! */ int getline(char [], int); int datume(const char []); void main(void){ char s[INP+1]; /* Az input puffer. */ printf( "Dátumellenrzés.\nBefejezés üres sorral!\n"); while( printf("\nDátum (ÉÉÉÉ.HH.NN)? "), getline(s,INP)>0) if(datume(s)) printf("Érvényes!\n"); else printf("Érvénytelen!\n"); }

Az ÉÉÉÉ.HH.NN alakú dátum érvényességét a kérdésre logikai érték választ szolgáltató, int datume(const char s[]) függvény segítségével érdemes eldöntetni. Követelmények: · A karakterláncnak 10 hosszúságúnak kell lennie. · A hónapot az évtl elválasztó karakter nem lehet numerikus, és azonosnak kell lennie a hónapot a naptól elválasztóval. · A láncbeli összes többi karakter csak numerikus lehet. · Csak 0001 és 9999 közötti évet fogadunk el. · Az évszám alapján megállapítjuk a február hónap napszámát. · A hónapszám csak 01 és 12 közötti lehet. · A napszám megengedett értéke 01 és a hónapszám maximális napszáma között van.
int datume(const char s[]){

C programnyelv

155

static int honap[ ] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 }; int i, ho; if(!s[10] && !isdigit(s[4]) && s[4]==s[7]){ for(i=0; i<10; ++i){ if(i==4||i==7) ++i; if(!isdigit(s[i])) return 0; } if((i=atoi(s))>=1){ honap[2]=28+(!(i%4)&&i%100 || !(i%400)); if((ho=10*(s[5]-'0')+s[6]-'0')>=1&&ho<=12&& (i=10*(s[8]-'0')+s[9]-'0')>=1&& i<=honap[ho]) return 1; } } return 0; }

Megoldandó feladatok: Változtasson úgy az int datume(const char s[]) függvényen, hogy akár egyjegy is lehessen: · az évszám, majd · a hónap és a napszám is! int indexe(char s[], char t[]) és int indexu(char s[], char t[]) függvények készítendk, melyek meghatározzák és visszaadják a t paraméter karakterlánc s karaktertömbbeli els, illetve utolsó elfordulásának indexét! Próbálja is ki egy rövid tesztprogrammal a függvényeket! Írjon olyan szoftvert, mely megkeresi egy próba karakterlánc összes elfordulását a szabvány bemenetrl érkez sorokban! 8.2.3 Függvények hívása és paraméterkonverziók A függvényt aktuális paraméterekkel hívjuk meg. Ezek sorrendjét és típusát a formális paraméterek határozzák meg. A függvényhívás operátor alakja
utótag-kifejezés() kifejezéslista: hozzárendelés-kifejezés kifejezéslista, hozzárendelés-kifejezés

, ahol az utótag-kifejezés egy függvény neve, vagy függvénycímmé értékeli ki a fordító, s ezt hívja meg. A zárójelben álló, elhagyható kifejezéslista tagjait egymástól vessz választja el, és tudjuk, hogy ezek azok az aktuális paraméter kifejezések, melyek értékmásolatait a hívott függvény kapja meg.

156

OBJEKTUMOK ÉS FÜGGVÉNYEK

Ha az utótag-kifejezés nem deklarált azonosító az aktuális hatáskörben, akkor a fordító implicit módon a függvényhívás blokkjában
extern int azonosító();

módon tekinti deklaráltnak. A függvényhívás kifejezés értéke és típusa a függvény visszatérési értéke és típusa. Az értéket vissza nem adó függvényt void­nak kell deklarálni, ill. void írandó a kifejezéslista helyére, ha a függvénynek nincs paramétere. Ha a prototípus paraméterlistája void, akkor a fordító zérus paramétert vár mind a függvényhívásban, mind a definícióban. E szabály megsértése hibaüzenethez vezet. Az aktuális paraméter kifejezéslista kiértékelési sorrendje nem meghatározott, pontosabban a konkrét fordítótól függ. A más paraméter mellékhatásától függ paraméter értéke így ugyancsak definiálatlan. A függvényhívás operátor egyedül azt garantálja, hogy a fordító a paraméterlista minden mellékhatását realizálja, mieltt a vezérlést a függvényre adná. Függvénynek tömb és függvény nem adható át paraméterként, de ezekre mutató mutató persze igen. A paraméter lehet aritmetikai típusú. Lehet struktúra, unió vagy mutató is, de ezekkel késbbi szakaszokban foglalkozunk. A paraméter átadása érték szerinti, azaz a függvény az értékmásolatot kapja meg, melyet természetesen el is ronthat a hívás helyén lev eredeti értékre gyakorolt bármiféle hatás nélkül. Szóval a függvény módosíthatja a formális paraméterek értékét. A fordító kiértékeli a függvényhívás kifejezéslistáját, és szokásos konverziót (egész­elléptetést) hajt végre minden aktuális paraméteren. Ez azt jelenti, hogy a float értékbl double lesz, a char és a short értékbl int, valamint az unsigned char és az unsigned short értékbl unsigned int válik. Ha van vonatkozó deklaráció a függvényhívás eltt, de nincs benne információ a paraméterekre, akkor a fordító kész az aktuális paraméterek értékével. Ha deklaráltak elzetesen függvény prototípust, akkor az eredmény aktuális paraméter típusát hasonlítja a fordító a prototípusbeli megfelel paraméter típusával. Ha nem egyeznek, akkor a deklarált formális paraméter típusára alakítja az aktuális paraméter értékét hozzárendelési kon-

C programnyelv

157

verzióval, és újra a szokásos konverzió következik. A nem egyezés másik lehetséges végkifejlete diagnosztikai üzenet. A hívásnál a kifejezéslistabeli paraméterek számának egyeznie kell a függvény prototípus vagy definíció paramétereinek számával. Kivétel az, ha a prototípus ,...-tal végzdik, amikor is a fordító a fix paramétereket az elz pontban ismertetett módon kezeli, s a ,... helyén lev aktuális paramétereket úgy manipulálja, mintha nem deklaráltak volna függvény prototípust. 8.2.4 Nem szabványos módosítók, hívási konvenció A deklaráció deklarátorlistájában a megismert szabványos alaptípusokon, típusmódosítókon kívül minden fordítóprogram rendelkezik még speciális célokat szolgáló, a deklarált objektum tulajdonságait változtató, nem szabványos módosítókkal is. Az olvasónak javasoljuk, hogy nézzen utána ezeknek a programfejleszt rendszere segítségében! Teljességre való törekvés nélkül felsorolunk itt néhány ilyen módosítót, melyek közül egyik­másik ki is zárja egymást!
módosító: cdecl pascal interrupt fastcall stdcall export near far huge

Az els néhány módosító a függvény hívási konvencióját határozza meg. Az alapértelmezett hívási konvenció C programokra cdecl. Tekintsünk csak bele bármelyik szabványos fejfájlba, mindegyik függvény prototípusa elején ott találjuk a cdecl módosítót! Ha egy azonosító esetében biztosítani kívánjuk a kis-nagybet érzékenységet, az aláhúzás karakter (_) név elé kerülését, ill. függvénynévnél a paraméterek jobbról balra való verembe rakását, akkor az azonosító deklarációjában írjuk ki expliciten a cdecl módosítót! Ez a hívási konvenció biztosítja az igazi változó paraméteres függvények írását, hisz a vermet a hívó függvénynek kell rendbe tennie.

158

OBJEKTUMOK ÉS FÜGGVÉNYEK

Változó paraméteres függvények írásával majd a MUTATÓK kapcsán foglalkozunk, most vegyünk egy példát az elmondottakra!
void fv(short s, int i, double d){} void main(void){ static short s=5; static int i=7; static float f=3.14f; /* . . . */ fv(s, i, f); /* . . . */ }

Hívás A paraméter értékeket jobbról balra haladva rakja ki a verembe a kód a konverziók elvégzése után, majd meghívja az fv­t. A veremmutató SP!

A hívott függvény

Verem helyreállítás

SP+12: 3.14 (double) SP+8: 3.14 (double) SP+8: 7 (int) SP+4: 7 (int) SP+4: 5 (int) SP: visszatérési cím SP: 5 (int)

Hozzáfér az értékmá- A hívó függvényben solatokhoz a verem- 16­tal csökkenti a kód ben, de közben az SP az SP értékét. nem változik meg. SP: Elvégzi a dolgát a Változó számú parafüggvény, és visszatér méter átadása könnyen a hívó függvénybe. lehetséges, hiszen ha még a hívás helyén sem ismerjük az aktuális paraméterek számát, akkor nem fogjuk megtudni sohasem.

Az stdcall kis­nagybet érzékeny. Standard hívási konvencióval kell hívni a WIN32 API függvények többségét. A fastcall módosító azt jelenti, hogy a fordító regiszterekben igyekszik átadni a paramétereket a hívott függvénynek a verembe rakás sorrendjében. Ha nincs elég regiszter, akkor a maradékot vermen át juttatja el a hívóhoz. A visszatérési érték átadása ugyancsak regiszteren át történik, ha lehetséges stb. A cdecl nagyobb végrehajtható kódot generál, mint a fastcall, vagy az stdcall, hisz a hívásit, vermet rendbe tev kódnak kell követnie a hívó függvényben. Már mondottuk, hogy cdecl az alapértelmezett hívási konvenció C programokra. A main­nek mindenképp cdecl-nek kell lennie, mert az indító kód C hívási konvenciók szerint hívja meg. A legtöbb programfejleszt rendszerben beállítható az általánosan használandó hívási konvenció, mely mindig felülbírálható a kívánt módosító explicit kiírásával.

C programnyelv

159

Ha például nem cdecl hívási sorrend programban szeretnénk használni a printf függvényt, akkor:
extern cdecl printf(const char format[], ...); void egeszekki(int i, int j, int k); void cdecl main(void){ egeszekki( 1, 4, 9); } void egeszekki(int i, int j, int k){ printf("%d %d %d\n", i, j, k); }

módon kell dolgozni. Elismerjük, hogy #include -t alkalmazva semmi szükség sincs az
extern cdecl printf(const char format[], ...);

kiírására, hisz ez a prototípus amúgy is benne van az STDIO.H fejfájlban. 8.2.5 Rekurzív függvényhívás Bármely függvény meghívhatja önmagát közvetlenül vagy közvetve. A rekurzív függvényhívásoknak egyedül a verem mérete szab határt. A verem méretét befolyásoló, például kapcsoló-szerkeszt opció, beállítást megtudhatjuk programfejleszt rendszerünk segítségébl. Valahányszor meghívják a függvényt, új tároló területet allokál a rendszer az aktuális paramétereknek, az auto és a nem regiszterben tárolt register változóknak. A paraméterek és a lokális változók tehát a veremben jönnek létre a függvénybe való belépéskor, és megsznnek, mihelyt a vezérlés távozik a függvénybl, azaz: · valahányszor meghívjuk a függvényt, saját lokális változó és aktuális paraméter másolatokkal rendelkezik, ami · biztosítja, hogy a függvény ,,baj nélkül" meghívhatja önmagát közvetlenül vagy közvetetten (más függvényeken át). A rekurzívan hívott függvények tulajdonképpen dolgozhatnak dinamikusan kezelt, globális vagy static tárolási osztályú lokális változókkal is. Azt azonban ilyenkor ne felejtsük el, hogy a függvény összes híváspéldánya ugyanazt a változót éri el. Ha az lenne a feladatunk, hogy írjunk egy olyan függvényt, mely meghatározza n faktoriálisát, akkor valószínleg így járnánk el:
long double faktor(int n){ long double N = n<1? 1.L: n; while(--n) N*=n; return N; }

160

OBJEKTUMOK ÉS FÜGGVÉNYEK

Látható, hogy a long double ábrázolási formát választottuk, hogy a lehet legnagyobb szám faktoriálisát legyünk képesek meghatározni a C számábrázolási lehetségeivel. Az algoritmus az egynél kisebb egészek faktoriálisát egynek tekinti, és az ismételt szorzást fordított sorrendben hajtja végre, vagyis: N=n*(n­1)*(n­2)*...*3*2*1. Írjunk egy rövid keretprogramot, mely bekéri azt az 1 és MAXN (fordítási idben változtatható) közti egész számot, melynek megállapíttatjuk a faktoriálisát!
/* PELDA22.C: Rekurziós példaprogram: faktoriális. */ #include #include #include #define MAX 40 /* Az input puffer mérete. */ #define MAXN 40 /* A max. szám. */ long double faktor(int n); int getline(char s[],int n); /* A decimális számjegyek száma maximálisan: */ #define HSZ sizeof(int)/sizeof(short)*5 int egesze(char s[]); void main(void){ int n=0; /* A szám. */ char sor[MAX+1]; /* A bemeneti puffer. */ printf("\n\t\tEgész szám faktoriálisa.\n"); while(n<1||n>MAXN){ printf("\nMelyik egész faktoriálisát számítjuk" "(1-%d)? ",MAXN); getline(sor,MAX); if(egesze(sor)) n=atoi(sor); } printf("%d! = %-20.0Lf \n",n, faktor(n)); }

Ismeretes, hogyha a formátumspecifikációban elírjuk a mezszélességet, akkor a kijelzés alapértelmezés szerint jobbra igazított. A balra igazítás a szélesség elé írt ­ jellel érhet el. A faktoriális­számítás rekurzív megoldása a következ lehetne:
long double faktor(int n){ if(n <= 1) return 1.L; else return (n*faktor(n-1)); }

Látható, hogy a faktor egynél nem nagyobb n paraméter esetén azonnal long double 1­gyel tér vissza. Más esetben viszont meghívja önmagát n­nél eggyel kisebb paraméterrel. Az itt visszakapott értéket megszorozza n­nel, és ez lesz a majdani visszaadott értéke. Nézzük meg asztali teszttel, hogyan történnek a hívások, feltéve, hogy a main a 0­s hívási szint faktor(5)­tel indult!

C programnyelv n: Hívási szint: Visszatérési szint: Visszatérési érték: 5 1 0 120 4 2 1 24 3 3 2 6 2 4 3 2

161 1 5 4 1

Hívás: faktor(4) faktor(3) faktor(2) faktor(1)

Cseréljük ki a keretprogramban a faktor függvényt a rekurzív változatra, és próbáljuk ki! Megoldandó feladatok: Írja meg a következ függvények rekurzív változatai: · void strrv(char s[]), mely megfordítja a saját helyén a paraméter karakterláncot. · Az itoa függvény, mely az int paraméterét karakterlánccá konvertálja, és elhelyezi az ugyancsak paraméter karaktertömbben.

162

MUTATÓK

9 MUTATÓK
A nyelvben a mutatóknak két fajtája van: · (adat) objektumra mutató mutatók és · függvényre (kód) mutató mutatók. Bármilyenek is legyenek, a mutatók memória címek tárolására szolgálnak. Elbb az adatmutatókkal fogunk foglalkozni. A mutató mindaddig, míg errl külön nem szólunk, jelentsen adatmutatót! A típus típusú mutató típus típusú objektum címét tartalmazhatja. A mutatók unsigned egészek, de saját szabályaik és korlátozásaik vannak a hozzárendelésre, a konverzióra és az aritmetikára. Miután a mutató is skalár objektum, létezik a mutatóra mutató mutató is. A mutatók azonban többnyire más skalár vagy void objektumokra (változókra), ill. aggregátumokra (tömbökre, struktúrákra és uniókra) mutatnak. 9.1 Mutatódeklarációk A mutatódeklaráció elnevezi a mutató változót, és rögzíti annak az objektumnak a típusát, melyre ez a változó mutathat. Tehát csak egy bizonyos típusra (elredefiniáltra - beleértve a void-ot is - vagy felhasználó definiáltára) mutató mutató deklarálható
típus *azonosító;

módon, amikor is az azonosító nev mutató típus típusú objektum címét veheti fel. Vegyük észre, hogy típus * az azonosító típusa! Például az
int *imut; int *fv(char *);

deklarációk alapján: imut int típusú objektumra mutató mutató, fv int típusú címet visszaadó, és egy char objektumra mutató paramétert fogadó függvény. Ha a definiált mutató statikus élettartamú, akkor tiszta zérus kezdértéket kap implicit módon. A fordító tiszta zérus címen nem helyez el sem objektumot, sem függvényt, s így a zérus cím (az ún. NULL mutató) speciális felhasználású. Magát a NULL mutatót a szabványos fejfájlokban (például az STDIO.H­ban) definiálja a nyelv.

C programnyelv

163

Bármilyen típusú mutató NULL-hoz hasonlítása mindenkor helyes eredményre vezet, ill. bármilyen típusú mutatóhoz hozzárendelhetjük a NULL mutatót. Ha lokális a definiált mutató, akkor a definíció hatására a fordító akkora memóriaterületet foglal, hogy abban egy cím elférjen, de a lefoglalt bájtok ,,szemetet" tartalmaznak. Az ilyen mutató tehát nem használható mindaddig ,,értelmesen" semmire, míg valamilyen módon érvényes címet nem teszünk bele. Hogy lehet elérni valamilyen objektum címét? 9.1.1 Cím operátor (&) Egyoperandusos, magas prioritású mvelet, mely definíció szerint:
& eltag-kifejezés

alakú. Az eltag-kifejezés operandusnak vagy függvényt kell kijelölnie, vagy olyan objektumot elér balértéknek kell lennie, mely nem register tárolási osztályú, és nem bitmez. Lehet tehát például változó, vagy tömbelem. A bitmezkkel a struktúrák kapcsán késbb foglalkozunk! Ha az operandus típusa típus, akkor az eredmény mutató a típus típusra. Cím csak mutatónak adható át. A lehetséges módszerek szerint vagy hozzárendeljük, vagy inicializátort alkalmazunk a deklarációban. Például:
típus val1, val2, *ptr = &val1; /* A ptr mutatónak van kezdértéke, de a vele elért objektumnak nincs. */ ptr = &val2; /* Hozzárendeljük a másik változó címét a mutatóhoz. */

Ha
T1 *ptr1; T2 *ptr2;

különböz típusú objektumokra mutató mutatók, akkor a
ptr1 = ptr2;

vagy a
ptr2 = ptr1;

hozzárendelés figyelmeztet vagy hibaüzenetet okoz. Explicit típusmódosító szerkezetet alkalmazva viszont ,,gond nélkül" mehet a dolog:
ptr1 = (T1 *) ptr2; ptr2 = (T2 *) ptr1;

164

MUTATÓK

Teljesen illegális dolog azonban a függvény és az adatmutatók öszszerendelése! Ha a mutató már érvényes címet tartalmaz, az 9.1.2 Indirekció operátor (*) segítségével elérhetjük a mutatott értéket. Az indirekció operátor ugyancsak egyoperandusos, magas prioritású mvelet, melynek definíciója:
* eltag-kifejezés

Az eltag-kifejezés operandusnak típus típusra mutató mutatónak kell lennie, ahol a típus bármilyen lehet. Az indirekció eredménye az eltagkifejezés mutatta címen lev, típus típusú érték. Az indirekció eredménye egyben balérték is. Például:
típus t1, t2; /* . . . */ típus *ptr = &t1; /* A mutatót inicializáltuk is. */ *ptr = t2; /* Ekvivalens a t1 = t2-vel. */

Ne kíséreljük meg azonban a cím operátorral kifejezés vagy konstans címét elállítani, vagy indirekció operátort nem címmé kiértékelhet operandus elé odaírni,
ptr = &(t1 + 6); ptr = &8; t2 = *t1; /* HIBÁS */ /* HIBÁS */ /* HIBÁS */

mert hibaüzenethez jutunk! Meghatározatlan az indirekció eredménye akkor is, ha a mutató: · NULL mutató, · vagy olyan lokális objektum címét tartalmazza, mely a program adott pontján nem látható, · vagy a végrehajtható program által nem használható címet tartalmaz. A mutatókkal végzett munka során ,,ököl-szabályunk" szerint: ahol objektum lehet egy kifejezésben, ott kerek zárójelbe téve az indirekció mveletét követen ilyen típusú objektumra mutató mutató is állhat. Ha:
típus val, *ptr = &val;

, akkor ahol val szerepelhet egy kifejezésben, ott állhat (*ptr) is.

C programnyelv Például:

165

int y, val=0, *ptr = &val; /* . . . */ printf("val címe (%p) van ptr-ben (%p).\n", &val, ptr); y = *ptr + 3; /* y = val +3 tulajdonképpen. */ y = sqrt((double) ptr); /* y = sqrt(val). */ *ptr += 6; /* val += 6. */ (*ptr)++; /* Zárójel nélkül a ptr-t inkrementáltatnánk. */ printf("val értéke %d.\n", val); printf("A ptr címen lev érték %d.\n", *ptr); }

Fedezzük fel, hogy a cím, vagy mutatótartalom kijelzéséhez használatos típuskarakter a p a formátumspecifikációban, és hogy a printf paraméterlistájában cím érték kifejezés is állhat, persze akár indirekcióval is! Vigyázzunk nagyon! Ha egy változónak nem adunk kezdértéket, és mondjuk, hozzáadogatjuk egy tömb elemeit, akkor a végeredmény ,,zöldség", s a dolog szarvashiba. Ha mutatónak nem adunk kezdértéket, és a benne lev ,,szemétre", mint címre, írunk ki értéket indirekcióval, akkor az duplán szarvashiba. A ,,szeméttel", mint címmel valahol ,,pancsolunk" a memóriában, és felülírjuk valami egészen más objektum értékét, s a hiba is egészen más helyen jelentkezik, mint ahol elkövettük. 9.1.3 void mutató Külön kell említenünk a
void *vptr;

mutató típust, ami nem semmire, hanem meghatározatlan típusra mutató mutató. Explicit típusmódosító szerkezet alkalmazása nélkül bármilyen típusú mutató vagy cím hozzárendelhet a void mutatóhoz. A dolog megfordítva is igaz, azaz bármilyen típusú mutatóhoz hozzárendelhetünk void mutatót. Például:
típus ertek, *ptr=&ertek; vptr=ptr; /* OK */ vptr=&ertek; /* OK */ ptr=vptr; /* OK */

void mutatóval egyetlen mvelet nem végezhet csak: az indirekció, hisz meghatározatlan a típus, és a fordító nem tudja, hogy hány bájtot és milyen értelmezésben kell elérnie.
ertek=*vptr; /* HIBÁS */

166

MUTATÓK

9.1.4 Statikus és lokális címek Miután a statikus élettartamú objektum minden bitjét zérusra inicializálja alapértelmezés szerint a fordító, de címe nem változik, kezdértéke lehet akár statikus mutatónak is. Az auto változók címe viszont nem lehet statikus inicializátor, hisz a cím más­más lehet a blokk különböz végrehajtásakor.
int GLOBALIS; int fv(void){ int LOKALIS; static int *slp = &LOKALIS; /* HIBÁS. */ static int *sgp = &GLOBALIS; /* OK. */ register int *rlp = &LOKALIS;/* OK. */ long a = 1000000l, *lp = &a;/* a kezdértéke 1000000 és lp kezdetben rá mutat. */ register int *pi = 0; /* pi regiszteres mutató értéke NULL. */ const int i = 26; /* Ez az egyetlen hely, ahol i értéket kaphatott. */ /* . . . */ }

9.1.5

Mutatódeklarátorok

deklarátor: direkt-deklarátor direkt-deklarátor: azonosító (deklarátor) direkt-deklarátor [] direkt-deklarátor (paraméter-típus-lista) direkt-deklarátor () mutató: * *mutató típusmódosító-lista: típusmódosító típusmódosító-lista típusmódosító típusmódosító: (a következk egyike!) const volatile

A deklarátorok szerkezetileg az indirekcióhoz, a függvényhez és a tömbkifejezéshez hasonlítanak, s csoportosításuk is azonos. A deklarátort követheti egyenlségjel után inicializátor, de mindig deklarációspecifikátorok elzik meg. A deklaráció-specifikátorok tárolási-osztályspecifikátor és típusspecifikátor sorozat tulajdonképp, és igazából nem csak egyetlen deklarátorra vonatkoznak, hanem ezek listájára.

C programnyelv

167

Megkérjük az olvasót, hogy lapozzon vissza egy pillanatra a TÍPUSOK ÉS KONSTANSOK Deklaráció fejezetéhez! A direkt-deklarátor definíció els lehetsége szerint a deklarátor egy egyedi azonosítót deklarál, melyre a tárolási osztály egy az egyben vonatkozik, de a típus értelmezése kicsit függhet a deklarátor alakjától is. A deklarátor tehát egyedi azonosítót határoz meg, s mikor az azonosító feltnik egy tle típusban nem eltér kifejezésben, akkor a vele elnevezett objektum értékét eredményezi. Összesítve, és csak a lényeget tekintve a deklaráció
típus deklarátor

alakú. Ezt nem változtatja meg az sem, ha a direkt-deklarátor második alternatíváját tekintjük, mert a zárójelezés nem módosítja a típust, csak összetettebb deklarátorok kötésére lehet hatással. A függvénydeklarátorokat már tárgyaltuk, s a tömbdeklarátorokra még ebben a szakaszban visszatérünk! A mutatódeklaráció így módosul:
típus * deklarátor

, ahol a * ­val változtatott típus a deklarátor típusa. A * operátor után álló típusmódosító magára a mutatóra, és nem a vele megcímezhet objektumra vonatkozik. Foglalkozzunk például a const módosítóval! 9.1.6 Konstans mutató Mind a mutató, mind a mutatott objektum deklarálható const-nak. Bármely const-nak deklarált ,,valami" ugyebár nem változtathatja meg az értékét. Az sem mehet persze, hogy olyan mutatót kreáljunk, mellyel megsérthetnénk a const objektum érték megváltoztathatatlanságát.
int i; int *pi; int * const const const /* pi int objektumra mutató inicializálatlan mutató. */ const cp = &i; /* cp konstans mutató int-re, de amire mutat, az nem konstans. */ int ci = 7; /* ci 7 érték konstans int. */ int *pci; /* pci konstans int-re mutat. A mutató tehát nem konstans. */ int * const cpc = &ci; /* cpc konstans int-re mutató konstans mutató. */

A következ hozzárendelések legálisak:

168

MUTATÓK

i = ci; /* const int hozzárendelése int-hez. */ *cp = ci;/* const int hozzárendelése konstans mutató mutatta nem konstans int-hez. */ ++pci; /* const int-re mutató mutató inkrementálása.*/ pci = cpc;/*Konstans int-re mutató, konstans mutató hozzárendelése const int-re mutató mutatóhoz.*/

A következ hozzárendelések illegálisak:
ci = 0; /* Értékhozzárendelési kísérlet const int-hez.*/ ci--; /* const int dekrementálási kísérlete. */ *pci = 3;/* Értékadási kísérlet a const int-re mutató mutatóval megcímzett objektumnak. */ cp = &ci;/* Értékadási kísérlet konstans mutatónak. */ cpc++; /* Konstans mutató inkrementálási kísérlete. */ pi = pci;/* Ha ez a hozzárendelés legális lenne, akkor *pi = ... módon módosíthatnánk azt a const int-et, amire pci mutat. */

Ahhoz, hogy a ,,kígyó megharapja a farkát" kell, hogy const objektumra mutató mutatót ne lehessen hozzárendelni nem const-ra mutató mutatóhoz. Ha ez menne, akkor ugyan ,,kerül úton", de a mutatott const érték megváltoztatható lenne. 9.2 Mutatók és függvényparaméterek Eddig kiemelten csak az ún. érték szerinti paraméter átadással foglalkoztunk a függvényhívás kapcsán. Ezt úgy interpretálhatjuk, hogy a fordító függvényhíváskor az aktuális paraméterek (esetleg típuskonverzión átesett) értékét helyezi el, például a veremben, s a meghívott függvény nem az aktuális paraméterek értékéhez, hanem annak csak egy másolatához fér hozzá. Rövidsége és találósága miatt átvesszük [4] vonatkozó mintapéldáját! Tegyük fel, hogy a programozó azt a feladatot kapta, hogy írjon olyan függvényt, mely megcseréli két, int paramétere értékét!
csere(paraméter1, paraméter2);

módon hívható els kísérlete a következ volt:
void csere(int x, int y){ int seged = x; x = y; y = seged; }

Barátunk próbálkozása ,,professzionális" olyan értelemben, hogy gondolt arra, hogy a csere végrehajtásához szüksége van segédváltozóra, de a

C programnyelv

169

függvényt hívó programjában ,,meglepetten" tapasztalta, hogy semmiféle értékcsere nem történt. A csere tulajdonképpen lezajlott az aktuális paraméterek másolatain a veremben, de ennek semmilyen hatása sincs az aktuális paraméterek hívó programbeli értékeire. A függvény visszatérése miatt a veremmutató is visszaállt a hívás eltti értékére, s így a verembeli, felcserélt értékmásolatok is elérhetetlenné váltak. A megoldás a cím szerinti paraméter átadásban rejlik, azaz a csere függvényt
csere(¶méter1, ¶méter2);

módon kell meghívni, s a függvénydefiníció pedig így módosul:
void csere(int *x, int *y){ int seged = *x; *x = *y; *y = seged; }

A rutin most az aktuális paraméterek címmásolatait kapja meg a veremben, s az indirekció mveletét alkalmazva így az aktuális paraméterek értékén dolgozik. Megoldandó feladatok: Írja át a következ, formai érvényességet ellenrz függvényeket úgy, hogy igaz (1) visszatérési érték mellett a vizsgált karakterlánc konvertált eredményét is szolgáltassák! Ha az érvényességellenrzés viszont hibával zárul, akkor a visszaadott hamis (0) értéken kívül semmiféle értékváltozást nem okozhat a függvény. · A PELDA18.C­ben definiált egesze legyen int egesze(const char s[], int *ertek) prototípusú! · A PELDA20.C­beli lebege átalakítandó int lebege(const char s[], double *ertek)­ké! · A PELDA21.C datume függvényébl váljék int datume(const char s[], int *ev, int *ho, int *nap)! Tömbök és mutatók A BEVEZETÉS ÉS ALAPISMERETEK szakaszban külön fejezet foglalkozik a tömbökkel, és az Inicializálás rész tárgyalja az egydimenziós tömbök kezdérték adását is. Tömb létesíthet aritmetikai típusokból, de definiálható 9.3

170 · mutatóból (külön késbbi fejezet),

MUTATÓK

· struktúrából és unióból (a következ szakasz), valamint · tömbbl (önálló fejezete van a többdimenziós tömböknek). Bármilyen típusból is hozzuk azonban létre a(z egydimenziós) tömböt, a típusnak teljesnek kell lennie. Nem lehet félig kész, nem teljesen definiált, felhasználói típusból tömböt kreálni. A tömbök és a mutatók között nagyon szoros kapcsolat van. Ha kifejezés, vagy annak része típus tömbje, akkor a (rész)kifejezés értékét a tömb els elemét megcímz, konstans mutatóvá alakítja a fordító, és a (rész)kifejezés típusa típus * const lesz. Nem hajtja végre a fordító ezt a konverziót, ha a (rész)kifejezés cím operátor (&), ++, ­­, vagy a pont (.) szelekciós operátor, vagy a sizeof operandusa, vagy hozzárendelési mvelet bal oldalán áll. A . operátorral a következ szakasz foglalkozik. Elemezzük az elz bekezdésben mondottakat egy példa tükrében! Legyen a következ tömb és mutató!
#define MERET 20 /* Tömbméret. */ /* . . . */ float tomb[MERET], *pt; /* . . . */

A tömböt a fordító, mondjuk, a 100­as címtl kezdve helyezte el a memóriában. Egy tömbelem helyfoglalása sizeof(tomb[0]) sizeof(float) 4, általánosságban sizeof(típus). Az elemek
tomb[0], tomb[1], ..., tomb[MERET - 1]

sorrendben, növekv címeken helyezkednek el a tárban. Tehát a tomb[0] a 100­as, a tomb[1] a 104­es, a tomb[2] a 108­as és így tovább címen van. A 180­as memóriacím már nem tartozik a tömbhöz. Ha valamilyen kifejezésben meglátja a fordító a tomb azonosítót, akkor rögtön helyettesíteni fogja a float * const 100­as címmel. Tehát, ha a pt­t fel kívánjuk tölteni tomb kezdcímével, akkor nem kell ilyen
pt = &tomb[0];

hosszadalmasan kódolni, tökéletesen elég a
pt = tomb;

C programnyelv

171

9.3.1 Index operátor A MVELETEK ÉS KIFEJEZÉSEK szakasz elején definiált utótagkifejezés második alternatívája az
utótag-kifejezés[kifejezés]

az indexel operátor. Nevezik ezt indexes változónak is, vagyis mindenképpen hivatkozás ez a tömb egy meghatározott elemére. A szabályok szerint az utótag-kifejezés és a kifejezés közül az egyiknek mutatónak, a másiknak egész típusúnak kell lennie, és hatásukra a fordító a
*((utótag-kifejezés) + (kifejezés))

mveletet valósítja meg. A fordító alkalmazza a következ fejezetben tárgyalt konverziós szabályokat a + mveletre és a tömbre. Ha az utótagkifejezés a tömb és a kifejezés az egész típusú, akkor a konstrukció a tömb kifejezésedik elemére hivatkozik. Mi van a típussal? A küls zárójel párban még mutató típus (típus *) van, s ezen hajtja végre a fordító az indirekciót. Tehát az eredmény típusát a mutató dönti el. Vegyük a tomb[6] indexes változót! A mondottak szerint ebbl
*((tomb) + (6))

lesz, ami nem 6, hanem 6­nak, mint indexnek, a hozzáadását jelenti a tomb kezdcíméhez, azaz:
*((float *)100 + 6*sizeof(float))

Az összeadás elvégzése után
*((float *)124)

, ami az indirekció végrehajtása után a tomb 6­os index elemét eredményezi. A + kommutatív mvelet, így az indexelés is az. Egydimenziós tömbökre a következ négy kifejezés teljesen ekvivalens feltéve, hogy p mutató és i egész:
p[i] i[p] *(p + i) *(i + p)

Folytassuk az index operátor ismertetésének megkezdése eltt elkezdett gondolatmenetet, azaz a pt mutató legyen tomb érték!
pt = tomb;

Ilyenkor:

172
*pt tomb[0], *(pt + 1) tomb[1]

MUTATÓK

és általában
*(pt + i) tomb[i].

Vegyük észre, hogy a pt-re szükség sincs a tömbelemek és címeik elállításához, hisz:
tomb[i] *(tomb + i)

és
&tomb[i] tomb + i.

Fontos különbség van azonban a tomb és a pt között. A pt mutató változó. A tomb pedig mutató konstans. Ebbl következleg nem megengedettek a következk:
tomb = pt; tomb++; pt = &tomb; /* Mintha 3=i-t írtunk volna fel. */ /* Mint 3++. */ /* Mintha &3-at akarnánk elállítani. */

A mutató változóra természetesen megengedettek ezek a mveletek:
pt = tomb; pt++;

Ha pt mutató, akkor azt kifejezés indexelheti a tanultak szerint, azaz
pt[i] *(pt + i)

Meg kell említeni még, hogy amikor egy függvényt tömbazonosító aktuális paraméterrel hívtunk meg, akkor is cím szerinti paraméter átadás történt, azaz a függvény a tömb kezdcím konstans másolatát kapta meg, például, a veremben. Ez a címmásolat aztán persze a függvényben már nem konstans, el is lehet rontani stb. Vegyük észre, hogy a cím szerinti paraméter átadást szinte minden példánkban, már a kezdetek óta használjuk! Eddig a függvényparaméter tömböt mindig
típus azonosító[]

alakban adtuk meg, de legújabb ismereteink szerint a
típus * azonosító

forma használandó, hisz ez egyértelmen mutatja, hogy a paraméter mutató. A függvény testén belül ettl függetlenül szabadon dönthet a programozó, hogy · tömbként,

C programnyelv · mutatóként, vagy · vegyesen kezeli az ilyen paramétert.

173

A gépi kódú utasítások operandusai többnyire memória címek. Ebbl következleg minél inkább mutatókat használva írjuk meg programjainkat, annál közelebb kerülünk a gépi kódhoz, s ez által annál gyorsabb lesz a szoftverünk. Írjuk át ennek szellemében a PELDA21.C datume függvényét!
int datume(const char *s){ static int honap[ ] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 }; int i, ho; if(!*(s+10) && !isdigit(*(s+4)) && *(s+4)==*(s+7)){ for(i=0; i<10; ++i){ if(i==4||i==7) ++i; if(!isdigit(*(s+i))) return 0; } if((i=atoi(s))>=1){ *(honap+2)=28+(!(i%4)&&i%100 || !(i%400)); if((ho=atoi(&s[5])>=1&&ho<=12&&(i=atoi(&s[8])>=1&& i<=*(honap+ho)) return 1; } } return 0; }

Lássuk be, hogy az ilyen fajta függvény átírásnak, amikor s[i]­bl *(s+i)­t csinálgatunk, semmi értelme sincs, hisz ezt a fordító magától is megteszi. Az átalakítás egyetlen elnye, hogy a formális paraméter jobban szemlélteti, hogy const karakterláncra mutat, ill. az észre vehet, hogy az atoi függvényt nem csak a karaktertömb kezdetével szabad meghívni. Foglalkozzunk még egy kicsit a tömbdeklarátorokkal! 9.3.2 Tömbdeklarátor és nem teljes típusú tömb A Mutatódeklarátorok fejezetben a direkt-deklarátor definíció harmadik változata
direkt-deklarátor []

a tömbdeklarátor. A tömbdeklaráció tehát
típus deklarátor []<={inicializátorlista<,>}>

, ahol az elhagyható konstans-kifejezésnek egész típusúnak és zérusnál nagyobb értéknek kell lennie, s ez a tömb mérete. Ez a TÍPUSOK ÉS KONSTANSOK szakasz Deklaráció fejezetében írottakon túl további korlátozásokat ró a konstans kifejezésre, hogy egész típusúnak kell lennie.

174

MUTATÓK

Operandusai ebben az esetben csak egész, felsorolás, karakteres és lebegpontos állandók lehetnek, de a lebegpontos konstanst explicit típuskonverzióval egésszé kell alakítani. Operandus lehet még a sizeof operátor is, aminek operandusára természetesen nincsenek ilyen korlátozások. Tudjuk, hogyha elmarad a tömbméret, akkor a fordító az inicializátorlista elemszámának megállapításával rögzíti azt, s a típus így válik teljessé. Megadott méret tömb esetében a lista inicializátorainak száma nem haladhatja meg a tömb elemeinek számát. Ha az inicializátorok kevesebben vannak a tömbméretnél, akkor a magasabb index, fennmaradó tömbelemek zérus kezdértéket kapnak. Tudjuk azt is, hogy tömb inicializátorai csak állandó kifejezések lehetnek. Ne felejtkezzünk meg róla, hogy ugyan a karaktertömb inicializátorlistája karakterlánc konstans, de az elbb felsorolt korlátozások ugyanúgy vonatkoznak rá is! Vagyis rögzített méret tömb esetén a karakterlánc hossza sem haladhatja meg a méretet. Ha a lánchossz és a tömbméret egyezik, nem lesz lánczáró zérus a karaktertömb végén. Ha a tömbdeklarációból hiányzik a tömbméret és inicializátorlista sincs, akkor a deklaráció nem teljes típusú tömböt határoz meg. Lássuk a lehetséges eseteket! · Ez csak elzetes deklaráció és a tömb méretét majd egy késbbi definíció rögzíti. Például:
float t[]; /* . . . */ float t[20];

· Ez csak elzetes deklaráció és a tömbméretet egy késbbi inicializátorlistás deklaráció tisztázza. Például:
char lanc[]; /* . . . */ char lanc[]="Ne tudd ki!";

Természetesen az együtt tömbméretes és inicializátorlistás definiáló deklaráció is megengedett! · A globális tömböt nem ebben a forrásmodulban definiálták, itt csak egyszeren a rá való hivatkozás eltt deklarálták. Például:
extern double dtomb[];

Persze a dtomb méretét valamilyen módon ebben a forrásmodulban is ismernünk kell! · A tömb függvény paramétere. Például:

C programnyelv
void fv(float t[], int n) { /* . . . */ }

175

A tömb méretét valahonnan a függvényben is tudni kéne! Például úgy, hogy további paraméterként átadják neki. 9.4 Mutatóaritmetika és konverzió A mutató vagy címaritmetika az inkrementálásra, a dekrementálásra, az összeadásra, a kivonásra és az összehasonlításra szorítkozik. A típus típusú objektumra mutató mutatón végrehajtott aritmetikai mveletek automatikusan figyelembe veszik a típus méretét, azaz az objektum tárolására elhasznált bájtok számát. A mutatóaritmetika ezen kívül feltételezi, hogy a mutató a típus típusú objektumok tömbjére mutat, azaz például:= 6; int i
float ftomb[50], *fptr = ftomb;

esetén
fptr += i;

hatására az fptr-beli cím
sizeof(float)*i

-vel (általánosságban sizeof(típus)*egész-szel) n, azaz a példa szerint ftomb[6]-ra mutat. Ha ptr1 a típus típusú tömb második és ptr2 a tizedik elemére mutat, akkor a két mutató különbsége
ptr2 - ptr1 8.

Figyeljük meg, hogy a mutatókhoz indexértéket adunk hozzá vagy vonunk ki belle és a mutatók különbsége is indexérték! Igazából a két mutató különbsége ptrdiff_t típusú egész indexkülönbség. A ptrdiff_t az STDDEF.H fejfájlban definiált, s többnyire signed int. 9.4.1 Összeadás, kivonás, inkrementálás és dekrementálás · Összeadás egyik operandusa lehet mutató, ha a másik operandus egész típusú. Az eredmény ilyenkor a címaritmetika szabályait követ mutató, azaz olyan cím, mely egész*sizeof(típus)­sal nagyobb, mint az eredeti. A típus a mutató által mutatott nem void típus. · Kivonásnál az els operandus lehet valamilyen objektumra mutató mutató, ha ilyenkor a második egész típusú. Az eredmény most is a mutatóaritmetika szabályait követ mutató, azaz olyan cím, mely egész*sizeof(típus)­sal kisebb, mint az eredeti. A típus a mutató által mutatott nem void típus.

176

MUTATÓK

· Kivonásnál mindkét operandus lehet ugyanazon objektum részeire mutató mutató. Az eredmény az elz két pont szellemében a mutatóaritmetika szabályainak megfelel egész (indexkülönbség). Az egész típusa a szabvány STDDEF.H fejfájlban definiált ptrdiff_t (signed int). Ha az operandus mutató, akkor az eggyel növelésben (++) vagy csökkentésben (­­) a címaritmetika szabályai érvényesek, azaz az eredmény mutató a következ, vagy a megelz elemre fog mutatni. Ha p mutató a tömb utolsó elemére mutat, akkor a ++p még legális érték: a tömb utolsó utáni elemének címe, de minden ezután következ mutatónövelés definiálatlan eredményre vezet. Hasonló probléma van akkor is, ha p a tömb kezdetére mutat. Ilyenkor a mutatócsökkentés - már a --p is - definiálatlan eredményt okoz. Vegyük észre, hogy az egész mutatóaritmetikának csak akkor van értelme, ha a mutatók egyazon tömb elemeire mutatnak! Minden nem tömbre alkalmazott címaritmetikai mvelet eredménye definiálatlan. Ugyanez mondható el akkor is, ha a tömbre alkalmazott mutatóaritmetikai mvelet eredménye a tömb legels eleme elé, vagy a legutolsó utánin túlra mutat. 9.4.2 Relációk A következ operandus típuskombinációk használhatók relációkban: · Mindkét operandus lehet ugyanazon típusú objektumra mutató mutató, amikor is a két objektum memória címének összehasonlításáról lesz szó. · A mutató összehasonlítás csak egyazon objektum részeire definiált, és a mutatók nem tartalmazhatnak ezen objektumon kívülre irányuló címet sem egyetlen kivétellel, tömb esetén megengedett az utolsó létez elem utáni címére való hivatkozás. Ha a mutatók tömb elemekre hivatkoznak, akkor az összehasonlítás az indexek összehasonlításával ekvivalens. A nagyobb index elem címe magasabb. · Az egyenlségi relációkat használva a mutató hasonlítható a konstans 0 értékhez, azaz a NULL mutatóhoz is. · Csak az egyenlségi relációk esetében a mutató hasonlítható a konstans, 0 érték, egész kifejezéshez, vagy void­ra mutató mutatóhoz. Ha mindkét mutató NULL mutató, akkor egyenlk.

C programnyelv

177

9.4.3 Feltételes kifejezés Ha a K1 ? K2 : K3­ban K2, K3 egyike­másika mutató, akkor a K2 vagy K3 operandusok típusától függ konstrukció eredményének típusa a következ: · Ha az egyik operandus valamilyen típusú objektumra mutató és a másik void mutató, akkor az eredmény (esetleges konverzió után) ugyancsak void mutató. · Ha az egyik operandus mutató, s a másik operandus 0 érték konstans kifejezés, akkor az eredmény típusa a mutató típusa lesz. Mutatók típusának összehasonlításakor a const vagy volatile típusmódosítók nem szignifikánsak, de az eredmény típusa megörökli mindkét oldal módosítóit. Írjuk át a címaritmetika alkalmazásával a PELDA20.C­ben használt DVEREM.C­t!
/* DVEREMUT.C: double verem push, pop és clear függvényekkel. */ #define MERET 128 /* A verem mérete. */ static double v[MERET]; /* A verem. */ static double *vmut=v; /* A veremmutató. */ int clear(void){ vmut=v; return MERET; } double push(double x){ if(vmutv) return *--vmut; else return 0.; }

A vmut most valóban veremmutató a double veremben (és nem a processzoréban). v kezdértékkel indul, és mindig a következ szabad helyre irányul. Ha kisebb, mint a veremhez már nem tartozó cím (v+MERET), akkor a push kiteszi rá a paraméterét, és mellékhatásként elbbre is lépteti eggyel a veremmutatót a következ szabad helyre. A pop csak akkor olvas a verembl, ha van benne valami (vmut>v). Kiszedéskor elbb vissza kell állítani a veremmutatót (az eltag ­­), s csak aztán érhet el indirekcióval a legutoljára kitett érték, s most ez lesz a következ szabad hely is egyben a következ push számára.

178

MUTATÓK

9.4.4 Konverzió A fordító által automatikusan elvégzett, implicit konverziókat már megismertük a címaritmetika mveleteinél. Az explicit típuskonverziós
(típusnév) eltag-kifejezés

szerkezetben a (típusnév) többnyire (típus *) alakú lesz, és ilyen típusú objektumra mutató mutatóvá konvertálja az eltag-kifejezés értékét. Például:
char *lanc; int *ip; /* . . . */ lanc = (char *) ip;

Nullaérték konstans, egész kifejezés, vagy ilyen (void *)­gal típusmódosítva konvertálható explicit típusmódosítással, hozzárendeléssel vagy összehasonlítással akármilyen típusú mutatóvá. Ez NULL mutatót eredményez, mely megegyezik az ugyanilyen típusú NULL mutatóval, de eltér bármely más objektumra vagy függvényre mutató mutatótól. Egy bizonyos típusú mutató konvertálható más típusú mutatóvá. Az eredmény azonban címzés hibás lehet, ha nem megfelel tárillesztés objektumot érne el. Csak azonos, vagy kisebb szigorúságú tárillesztési feltételekkel bíró adattípus mutatójává konvertálható az adott mutató, és onnét vissza. A tárillesztés hardver sajátosság, s azt jelenti, hogy a processzor bizonyos típusú adatokat csak bizonyos határon lev címeken helyezhet el. A legkevésbé megszorító a char típus szokott lenni. A short csak szóhatáron (2­vel maradék nélkül osztható címen) kezddhet, a long viszont dupla szóhatáron (4­gyel maradék nélkül osztható címen) helyezkedhet el, s így tovább. Felkérjük az olvasót, hogy programfejleszt rendszere segítségében feltétlenül nézzen utána a konkrétumoknak! void mutató készíthet akármilyen típusú mutatóból, és megfordítva korlátozás és információvesztés nélkül. Ha az eredményt visszakonvertáljuk az eredeti típusra, akkor az eredeti mutatót állítjuk újra el. Ha ugyanolyan, de más, const vagy volatile módosítójú típusra konvertálunk, akkor az eredmény ugyanaz a mutató a módosító által elidézett megszorításokkal. Ha a módosítót aztán elhagyjuk, akkor a további mveletek során az eredetileg az objektum deklarációjában szerepl const vagy volatile módosítók maradnak érvényben.

C programnyelv

179

A mutató mindig konvertálható a tárolásához elegenden nagy, egész típussá. A mutató mérete, és az átalakító függvény persze nem gépfüggetlen. Leírunk egy, több fejleszt rendszerben is használatos mutató­egész és egész­mutató konverziót. A mutató­egész konverzió módszere függ a mutató és az egész típus méretétl, valamint a következ szabályoktól: · Ha a mutató mérete nem kisebb az egész típusénál, akkor a mutató érték unsigned­ként viselkedik azzal a megkötéssel, hogy nem konvertálható lebegpontossá. · Ha a mutató mérete kisebb az egész típusénál, akkor a mutatót elbb az egésszel megegyez méretvé alakítja a fordító, s aztán konvertálja egésszé. Az egész­mutató átalakítás sem portábilis, de a következ szabályok szerint mehet például: · Ha az egész típus ugyanolyan méret, mint a mutató, akkor az egész értéket unsigned­ként mutatónak tekinti a fordító. · Ha az egész típus mérete különbözik a mutatóétól, akkor az egész típust az elz pontokban ismertetett módon konvertálja elbb mutató méret, egész típusúvá, majd unsigned­ként mutatónak tekinti. 9.5 Karaktermutatók Tudjuk, hogy a karakterlánc konstans karaktertömb típusú, s ebbl következleg mögötte ugyancsak egy cím konstans van, hisz például a
printf("Ez egy karakterlánc konstans.");

kitnen mködik, holott a printf függvény els paramétere const char * típusú. Ez a konstans mutató azonban a tömbtl eltéren nem rendelkezik azonosítóval sem, tehát késbb nincs módunk hivatkozni rá. Ennek elkerülésére, azaz a cím konstans értékének megrzésére, a következ módszerek ajánlhatók:
char *uzenet; uzenet = "Kész a kávé!\n";

vagy
const char *uzenet = "Kész a kávé!\n";

9.5.1 Karakterlánc kezel függvények A rutinok prototípusai a szabványos STRING.H fejfájlban helyezkednek el. Egyik részüknek str­rel, másik csoportjuknak mem­mel kezddik

180

MUTATÓK

a neve. Az str kezdetek karakterláncokkal (char *), míg a mem nevek memóriaterületekkel bájtonként haladva (void * és nincs feltétlenül lánczáró zérus a bájtsorozat végén) foglalkoznak. A char *, vagy void * viszszaadott érték függvények mindig az eredmény lánc kezdcímét szolgáltatják. A memmove­tól eltekintve, a többi rutin viselkedése definiálatlan, ha egymást a memóriában átfed karaktertömbökre használják ket. Néhányat ­ teljességre való törekvés nélkül ­ felsorolunk közülük! char *strcat(char *cel, const char *forras); char *strncat(char *cel, const char *forras, size_t n); A függvények a cel karakterlánchoz fzik a forras­t (strcat), vagy a forras legfeljebb els, n karakterét (strncat), és visszatérnek az egyesített cel karakterlánc címével. Nincs hibát jelz visszaadott érték! Nincs túlcsordulás vizsgálat a karakterláncok másolásakor és hozzáfzésekor. A size_t többnyire az unsigned int típusneve. Írjuk csak meg a saját strncat függvényünket!
char *strncat(char *cel, const char *forras, size_t n){ char *seged = cel; /* Pozícionálás a cel lezáró '\0' karakterére: */ while(*cel ) ++cel; /* forras cel végére másolása a záró '\0'-ig, vagy legfeljebb n karakterig: */ while(n-- && (*cel++ = *forras++)); /* Vissza az egyesített karakterlánc kezdcíme: */ return seged; }

char *strchr(const char *string, int c); void *memchr(const void *string, int c, size_t n); A rutinok a c karaktert keresik string­ben, ill. string els n bájtjában, és az els elfordulás címével térnek vissza, ill. NULL mutatóval, ha nincs is c a string­ben, vagy az els n bájtjában. A lánczáró zérus is lehet c paraméter. Az char *strrchr(const char *string, int c); ugyanazt teszi, mint az strchr, csak c string­beli utolsó elfordulásának címével tér vissza, ill. NULL mutatóval, ha nincs is c a string­ben. int strcmp(const char *string1, const char *string2); int strncmp(const char *string1, const char *string2, size_t n);

C programnyelv int memcmp(const void *string1, const void *string2, size_t n);

181

A függvények unsigned char típusú tömbökként összehasonlítják string1 és string2 karakterláncokat, és negatív értéket szolgáltatnak, ha string1 < string2. Pozitív érték jön, ha string1 > string2. Az egyenlséget viszont a visszaadott zérus jelzi. Az strncmp és a memcmp a hasonlítást legföljebb az els n karakterig, ill. bájtig végzik. A legtöbb fejleszt rendszerben nem szabványos stricmp és strnicmp is szokott lenni, melyek nem kis­nagybet érzékenyen hasonlítják össze a karakterláncokat. A saját strcmp:
int strcmp(const char *s1, const char *s2 ){ for( ; *s1 == *s2; ++s1, ++s2) if(!(*s1)) return 0; /* s1 == s2 */ return(*s1 - *s2); } /* s1 < s2 vagy s1 > s2 */

char *strcpy(char *cel, const char *forras); char *strncpy(char *cel, const char *forras, size_t n); void *memcpy(void *cel, const void *forras, size_t n); void *memmove(void *cel, const void *forras, size_t n); Az strcpy a forras karakterláncot másolja lánczáró karakterével együtt a cel karaktertömbbe, és visszatér a cel címmel. Nincs hibát jelz visszatérési érték. Nincs túlcsordulás ellenrzés a karakterláncok másolásánál. Az strncpy a forras legfeljebb els n karakterét másolja. Ha a forras n karakternél rövidebb, akkor cel végét '\0'­ázza n hosszig a rutin. Ha az n nem kisebb, mint a forras mérete, akkor nincs zérus a másolt karakterlánc végén. A memcpy és a memmove mindenképpen n bájtot másolnak. Egyetlenként a karakterlánc kezel függvények közül, a memmove akkor is biztosítja az átlapoló memóriaterületeken lev, eredeti forras bájtok felülírás eltti átmásolását, ha a forras és a cel átfedné egymást. A saját strcpy:
char *strcpy(char *cel, const char *forras ){ char *seged = cel; /* forras cel-ba másolása lezáró '\0' karakterével: */ while(*cel++ = *forras++); /* Vissza a másolat karakterlánc kezdcíme: */ return seged; }

182 size_t strlen(const char *string);

MUTATÓK

A rutin a string karakterlánc karaktereinek számával tér vissza a lánczárót nem beszámítva. Nincs hibát jelz visszaadott érték! char *strpbrk(const char *string, const char *strCharSet); A függvények az strCharSet­beli karakterek valamelyikét keresik a string­ben, és visszaadják az els elfordulás címét, ill. NULL mutatót, ha a két paraméternek közös karaktere sincs. A keresésbe nem értendk bele a lánczáró zérusok. A példában számokat keresünk az s karakterláncban.
#include #include void main(void){ char s[100] = "3 férfi és 2 fiu 5 disznót ettek.\n"; char *er = s; int i=1; while(er = strpbrk(er, "0123456789")) printf("%d: %s\n", i++, er++); }

A kimenet a következ:
1: 3 férfi és 2 fiu 5 disznót ettek. 2: 2 fiu 5 disznót ettek. 3: 5 disznót ettek.

char *strset(char *string, int c); char *strnset(char *string, int c, size_t n); void *memset(void *string, int c, size_t n); A memset feltölti string els n bájtját c karakterrel, és visszaadja string kezdcímét. A legtöbb fejleszt rendszerben létez, nem ANSI szabványos strset és strnset a string minden karakterét ­ a lánczáró zérus kivételével ­ c karakterre állítják át, és visszaadják a string kezdcímét. Nincs hibát jelz visszatérési érték. Az strnset azonban a string legfeljebb els n karakterét inicializálja c­re. size_t strspn(const char *string, const char *strCharSet); size_t strcspn(const char *string, const char *strCharSet); Az strspn annak a string elején lev, maximális alkarakterláncnak a méretét szolgáltatja, mely teljes egészében csak az strCharSet­beli karakterekbl áll. Ha a string nem strCharSet­beli karakterrel kezddik, akkor

C programnyelv

183

zérust kapunk. Nincs hibát jelz visszatérési érték. A keresésbe nem értendk bele a lánczáró zérus karakterek. A függvény visszaadja tulajdonképpen az els olyan karakter indexét a string­ben, mely nincs benn az strCharSet karakterlánccal definiált karakterkészletben. Az strcspn viszont annak a string elején lev, maximális alkarakterláncnak a méretét szolgáltatja, mely egyetlen karaktert sem tartalmaz az strCharSet­ben megadott karakterekbl. Ha a string strCharSet­beli karakterrel kezddik, akkor zérust kapunk. Például a:
#include #include void main(void){ char string[] = "xyzabc"; printf("%s láncban az els a, b vagy c indexe %d\n", string, strcspn(string, "abc")); }

eredménye:
xyzabc láncban az els a, b vagy c indexe 3

char *strstr(const char *string1, const char *string2); A függvény string2 karakterlánc els elfordulásának címét szolgáltatják string1­ben, ill. NULL mutatót kapunk, ha string2 nincs meg string1­ben. Ha string2 üres karakterlánc, akkor a rutin string1­gyel tér vissza. A keresésbe nem értendk bele a lánczáró zérus karakterek. char *strtok(char *strToken, const char *strDelimit); A függvény a következleg megtalált, strToken­beli szimbólum címével tér vissza, ill. NULL mutatóval, ha nincs már további szimbólum az strToken karakterláncban. Mindenegyes hívás módosítja az strToken karakterláncot, úgy hogy '\0' karaktert tesz a bekövetkezett elválasztójel helyére. Az strDelimit karakterlánc az strToken­beli szimbólumok lehetséges elválasztó karaktereit tartalmazza. Az els strtok hívás átlépi a vezet elválasztójeleket, visszatér az strToken­beli els szimbólum címével, és ezeltt a szimbólumot '\0' karakterrel zárja. Az strToken maradék része további szimbólumokra bontható újabb strtok hívásokkal. Mindenegyes strtok hívás módosítja az strToken karakterláncot, úgy hogy '\0' karaktert tesz az aktuálisan visszaadott szimbólum végére. Az strToken következ szimbólumát az strToken paraméter helyén NULL mutatós strtok hívással lehet elérni. A NULL mutató els paraméter hatására az strtok megkeresi a következ szimbólumot a módosított strToken­ben. A lehetséges elválasztójeleket tartalma-

184

MUTATÓK

zó strDelimit paraméter, s így maguk az elválasztó karakterek is, változhatnak hívásról­hívásra. A példaprogramban ciklusban hívjuk az strtok függvényt, hogy megjelentethessük az s karakterlánc összes szóközzel, vesszvel, stb. elválasztott szimbólumát:
#include #include void main( void ){ char s[] = "Szimbólumok\tkarakterlánca\n ,, és néhány" " további szimbólum"; char selv[] = " ,\t\n", *token; printf("%s\nSzimbólumok:\n", s); /* Míg vannak szimbólumok a karakterláncban, */ token = strtok(string, selv); while(token != NULL){ /* addig megjelentetjük ket, és */ printf("\t%s\n", token); /* vesszük a következket: */ token=strtok(NULL, selv); } }

A kimenet a következ:
Szimbólumok karakterlánca ,, és néhány további szimbólum Szimbólumok: Szimbólumok karakterlánca és néhány további szimbólum

Megoldandó feladatok: Készítse el az alább felsorolt, ismert C függvények mutatókat használó változatát! A char * visszatérés rutinok az eredmény címével térnek vissza. · int getline(char *, int): sor beolvasása a szabvány bemenetrl. · char *squeeze(char *s, int c): c karakter törlése az s karakterláncból a saját helyén. · int *binker(int x, int *t, int n): a növekvleg rendezett, n elem, t tömbben megkeresend x csak mutatókat használó, bináris keresési algoritmussal! Ha megvan, vissza kell adni a t­beli elfordulás címét. Ha nincs, NULL mutatót kell szolgáltatni. · int atoi(char *): karakterlánc egésszé konvertálása.

C programnyelv

185

· char *strupr(char *): karakterlánc nagybetssé alakítása a saját helyén. · char *strrev(char *): karakterlánc megfordítása a saját helyén. · char *strset(char *s, int c): s karakterlánc feltöltése c karakterrel. · char *strstr(const char *, const char *): a második karakterlánc keresése az elsben. A pontos ismertetés kicsit elbbre megtalálható! Készítsen char *strstrnext(const char *, const char *) függvényt is, mely ugyanazt teszi, mint az strstr, de static mutató segítségével az els hívás után a második karakterlánc következ elsbeli elfordulásának címével tér vissza, és ezt mindaddig teszi, míg NULL mutatót nem kell szolgáltatnia. 9.5.2 Változó paraméterlista Az OBJEKTUMOK ÉS FÜGGVÉNYEK szakaszból tudjuk, hogy a függvény paraméterlistája ...­tal is végzdhet, amikor is a fordító a ,... eltti fix paramétereket a szokásos módon kezeli, de a ,.. helyén álló aktuális paramétereket úgy manipulálja, mintha nem adtak volna meg függvény prototípust. A szabványos STDARG.H fejfájlban definiált típus és függvények (makrók) segítséget nyújtanak az ismeretlen paraméterszámú és típusú paraméterlista feldolgozásában. A típus és az ANSI kompatibilis makródefiníciók a következk lehetnek például:
typedef char *va_list; #define _INTSIZEOF(x) ((sizeof(x)+sizeof(int)-1)&\ ~(sizeof(int) -1)) #define va_start(ap, utsofix) (ap=(va_list)&utsofix+\ _INTSIZEOF(utsofix)) #define va_arg(ap, tipus) (*(tipus *)((ap+=_INTSIZEOF (tipus)) - _INTSIZEOF(tipus))) #define va_end(ap) ap = (va_list)0

A va_... szerkezettel csak olyan változó paraméteres függvények írhatók, ahol a változó paraméterek a paraméterlista végén helyezkednek el. A va_... makrókat a következképp kell használni: 1. A szerkezetet használó függvényben deklarálni kell egy va_list típusú, mondjuk, param nev változót:
va_list param;

186

MUTATÓK , ami a va_arg és a va_end által igényelt információt hordozó mutató.

2. Meg kell hívni a va_start függvényt:
va_start(param, utsofix);

a va_arg és a va_end függvények használata eltt. A va_start a param mutatót a változó paraméterlistával meghívott függvény els változó paraméterére állítja. Az utsofix paraméterben el kell írni a változó paraméterlistával meghívott függvény azon utolsó formális paraméterének nevét, amely még rögzített. A va_start­nak nincs visszaadott értéke. A va_start els paraméterének va_list típusúnak kell lennie. Ha a második paraméter register tárolási osztályú, akkor a makró viselkedése nem meghatározott. 3. Eztán va_arg hívások következnek:
(tipus) va_arg(param, tipus);

, melyek rendre szolgáltatják a változó paraméterlista következ aktuális paraméterének értékét. A makróból látszik, hogy a tipus megadása fontos, hisz eszerint lépteti a param a mutatót elre, ill. ilyen típusú értéket szolgáltat a változó paraméterlistában soron következ paraméterbl. Azt aztán, hogy a változó paraméterlistának mikor van vége, a programozónak kell tudnia! 4. Ha a va_arg kiolvasta a változó paraméterlista minden elemét, vagy más miatt kell abbahagynunk a további feldolgozást, akkor meg kell hívni a
va_end(param);

makrót, hogy a változó paraméterlistával meghívott függvénybl biztosítsuk a normális visszatérést. A va_end­nek nincs visszaadott értéke, és a makródefinícióból látszik, hogy NULLázza a param mutatót. Tekintsünk meg a következ egyszer példát, melyben zérus jelzi a paraméterlista végét!
#include #include void sum(char *uzen, ...) { int osszeg = 0, tag; va_list param; va_start(param, uzen); while(tag = va_arg(param, int)) osszeg += tag;

C programnyelv
printf(uzen, osszeg); } void main(void) { sum("1+2+3+4 = %d\n", 1, 2, 3, 4, 0); }

187

A param char * típusú. Tételezzük fel a 32 bites int és cím esetét! Akkor a va_start(param, uzen) hatására az
_INTSIZEOF(uzen)= (sizeof(uzen)+sizeof(int)-1)&~(sizeof(int) -1)= (4+4-1)&~(4-1)=7&~3=4

és így a
param=(char *)&uzen+4

, ami azt jelenti ugye, hogy param az els változó paraméter címét fogja tartalmazni. A tag=va_arg(param, int) következtében
tag=(*(int *)((param+=_INTSIZEOF(int))-_INTSIZEOF(int)))

, azaz:
tag=*(int *)((param+=4)-4)

Tehát a tag változó felveszi a következ, int, aktuális paraméter értékét, és eközben param már a következ aktuális paraméterre mutat. A következ függvények a parlist változó paraméterlistától eltekintve ugyanúgy dolgoznak, mint nevükben v nélküli párjaik. A felsorolt printf függvények a 3. pont va_arg hívásait maguk intézik, és a format karakterlánc alapján a változó paraméterlista végét is látják. A parlist az aktuális változó paraméterlista els elemére mutató mutató. int vfprintf(FILE *stream, const char *format, va_list parlist); int vprintf(const char *format, va_list parlist); int vsprintf(char * puffer, const char * format, va_list parlist); 9.6 Mutatótömbök Mutatótömb a
típus *azonosító[] <={inicializátorlista}>;

deklarációval hozható létre. Az azonosító a mutatótömb neve: konstans mutató, melynek típusa (típus **), vagyis típus típusú objektumra mutató mutatóra mutató mutató. Tehát olyan konstans mutató, mely (típus *) mutatóra mutat. Vegyük észre a * mutatóképz operátor rekurzív használatát!

188

MUTATÓK

A tömb egy eleme viszont (típus *) típusú változó mutató. Kezdjünk el egy példát magyar kártyával! A színeket kódoljuk a következképp: makk (0), piros (1), tök (2) és zöld (3). A kártyákat egy színen belül jelöljük így: hetes (0), nyolcas (1), kilences (2), tízes (3), alsó (4), fels (5), király (6) és ász (7). A konkrét kártya kódja úgy keletkezik, hogy a színkód után leírjuk a kártya kódját. Ilyen alapon a 17 például piros ász. Készítsünk char *KartyaNev(int kod) függvényt, mely visszaadja a paraméter kodú kártya megnevezését!
/* KARTYA.H fejfájl. */ /* Szimbolikus állandók: */ #define SZINDB 4 /* Színek száma. */ #define KTYDB 8 /* Kártyák száma egy színen belül. */ #define OSZTO 10 /* Szín és kártyakód szétválasztásához*/ #define PAKLI SZINDB*KTYDB /* Kártyaszám a pakliban. */ #define MAXKOD (SZINDB-1)*OSZTO+KTYDB /* Max. kártykód.*/ /* Prototípusok: */ char * KartyaNev(int); void UjPakli(void); int Mennyi(void); int UjLap(void); /* KARTYA.C: Adat és függvénydefiníciók. */ #include #include #include #include "KARTYA.H" static char *szin[SZINDB]={"makk ", "piros ", "tök ", "zöld "}; static char *kartya[KTYDB]={"VII", "VIII", "IX", "X", "alsó", "fels", "király", "ász"}; char * KartyaNev(int kod){ static char szoveg[20]; if(kod>=0 && kod/OSZTO
Vigyázzunk nagyon a típusokkal! A szin és a kartya statikus élettartamú, bels kapcsolódású, fájl hatáskör, karakterláncokra mutatók tömbjeinek kezdeteire mutató mutató (char **) konstansok nevei. A szin[1] a piros karakterlánc kezdetének címét tartalmazó, karakteres mutatótömb elem (char *). Igazak például a következk:

C programnyelv
*szin szin[0] **szin *szin[0] 'm'

189

A szoveg statikus élettartamú karaktertömb, így nem sznik meg a memóriafoglalása a függvénybl való visszatéréskor, csak lokális hatáskör azonosítójával nem érhet el! Írjunk rövid, kipróbáló programot!
/* PELDA23.C: Kártya megnevezések kiíratása. */ #include #include "KARTYA.H" void main(void){ int i; printf("Magyar kártya megnevezések rendre:\n"); for(i=-1; i<=MAXKOD; ++i) printf("%-40s", KartyaNev(i)); }

9.7 Többdimenziós tömbök A többdimenziós tömböket a tömb típus tömbjeiként konstruálja meg a fordító. A deklaráció
típus azonosító[][méret2]. . .[méretN] <={inicializátorlista}>;

alakú. Az elhelyezésrl egyelre annyit, hogy sorfolytonos, azaz a jobbra álló index változik gyorsabban. Például a
double matrix[2][3];

sorfolytonosan a következ sorrendben helyezkedik el a tárban:
matrix[0][0], matrix[0][1], matrix[0][2], matrix[1][0], matrix[1][1], matrix[1][2]

Egy elem elérése például
azonosító[index1][index2]. . .[indexN]

módon megy, ahol az indexekre igazak a következk:
0 <= index1 < méret1 0 <= index2 < méret2 ... 0 <= indexN < méretN

Vigyázzunk az indexek külön­külön []­be írására is! Véve mondjuk a matrix[i][j]­t, a matrix[i,j] kifejezés is ,,értelmes" a C­ben. Persze nem matrix[i][j]­t jelenti, hanem a vessz operátor végrehajtása után a matrix[j] tömböt. A BEVEZETÉS ÉS ALAPISMERETEK szakasz Inicializálás fejezetében a tömbök inicializálásáról mondottak most is érvényben vannak, de

190

MUTATÓK

a többdimenziós tömb további tömbökbl áll, s így az inicializálási szabályok is rekurzívan alkalmazandók. Az inicializátorlista egymásba ágyazott, egymástól vesszvel elválasztott inicializátorok és inicializátorlisták sorozata a sorfolytonos elhelyezési rend betartásával. Például a 4x3­as t tömb els sorának minden eleme 1 érték, második sorának minden eleme 2 érték, s így tovább.
int t[4][3]={{1, 1, 1}, {2, 2, 2}, {3, 3, 3}, {4, 4, 4}};

Ha az inicializátorlistában nincs beágyazott inicializátorlista, akkor az ott felsorolt értékek az alaggregátumok, s azon belül az elemek sorrendjében veszik fel az aggregátum elemei. Az elz példával azonos eredményre vezetne a következ inicializálás is:
int t[4][3] = {1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4};

Kapcsos zárójelek akár az egyes inicializátorok köré is tehetk, de ha a fordítót nem kívánjuk ,,becsapni", akkor célszer ket az aggregátum szerkezetét pontosan követve használni! Az
int t[4][3]={{1, 1, 1}, {2, 2, 2}, {3, 3, 3}};

hatására a t mátrix utolsó sorára (a t[3] tömbre) nem jut inicializátor, így a t[3][0], t[3][1] és t[3][2] elemek mind zérus kezdértéket kapnak. Az
int t[4][3]={{1}, {2}, {3}};

eredményeként a t[0][0] 1, a t[1][0] 2, a t[2][0] 3 lesz, és a tömb összes többi eleme zérus. Tudjuk, hogyha nem adjuk meg, akkor az inicializátorlista elemszámából állapítja meg a tömbméretet a fordító. Többdimenziós tömb deklarációjában ez azonban csak az els méretre vonatkozik, a további méreteket mind kötelez elírni.
int a[][]={{1, 1}, {2, 2}, {3, 3}}; int t[][3]={{1, 1, 1}, {2, 2, 2}, {3, 3, 3}}; /* HIBÁS */ /* OK */

Folytassuk tovább a PELDA23.C­ben megkezdett magyar kártyás példánkat! Újabb programunknak legyen az a feladata, hogy megkeveri a paklit, s kioszt öt lapot! Aztán újra kioszt öt lapot, s így tovább mindaddig, míg a kártya el nem fogy. Újabb keverés következik és újabb osztások. A szoftvernek akkor van vége, ha már nem kérnek több lapot. Azt, hogy milyen kártyákat osztott már ki a pakliból, statikus élettartamú, csak a KARTYA.C forrásfájlban elérhet, kétdimenziós, SZINDB* KTYDB­s, kty tömbben tartja nyilván a program. A megfelel tömbelem zérus, ha még pakliban van a lap, és 1­gyé válik, ha kiosztják. A statikus,

C programnyelv

191

a KARTYA.C forrásmodulra ugyancsak lokális, ktydb változóban a pakli aktuális kártyaszámát rzi a szoftver. A következ sorokkal mindig a KARTYA.C bvítend!
static int kty[SZINDB][KTYDB]; static int ktydb=SZINDB*KTYDB;

Az UjPakli függvény ugyanezt az állapotot állítja el.
void UjPakli(void){ int i, j; for(i=0; i
A Mennyi rutinnal lekérdezhet, hogy még hány kártya van a pakliban.
int Mennyi(void){ return ktydb; }

Az int UjLap(void) függvény egy lapot ad a pakliból, azonban ezt véletlenszeren teszi a következ technikával: · Ha nincs kártya a pakliban, ­1­et szolgáltat. · Ha egyetlen lapból áll a pakli, akkor azt adja. · Ha ktydb=KTYDB, akkor véletlen színt és véletlen kártyát választ. Kiadja a lapot, ha még eddig nem osztotta ki. Ha a kártya már nincs a pakliban, újat választ, és így tovább. Látszik, hogy szükségünk lesz véletlenszám generátorra! 9.7.1 Véletlenszám generátor Használatához az STDLIB.H fejfájlt kell bekapcsolni. Egész számok pszeudóvéletlen sorozatát generálja 0 és RAND_MAX között az int rand(void); függvény. A rutinnak nincs hibás visszatérése. A véletlenszám generálás kezdértékét lehet beállítani a void srand(unsigned int kezd); függvénnyel. Az alapértelmezett induló érték 1, így ilyen érték kezd paraméterrel mindig újrainicializáltathatjuk a véletlenszám generátort, azaz

192

MUTATÓK

a rand hívások ugyanazt a véletlenszám sorozatot produkálják srand(1) után, mintha mindenféle srand megidézés nélkül használtuk volna a rand­ot. Az srand­ot a véletlenszer indulást is biztosítandó rendszerint a TIME.H­ban helyet foglaló time_t time(time_t *timer); függvény kezd paraméterrel szokták meghívni. A time rutin az aktuális rendszer idt 1970. január elseje éjfél óta eltelt másodpercek számában, time_t (long) típusban szolgáltatja. Nincs hibás visszatérés. A visszatérési értéket a timer címen is elhelyezi, ha a paraméter nem NULL mutató. NULL mutató aktuális paraméter esetén viszont nem tesz ilyet. Az srand függvény szokásos hívása tehát:
srand(time(NULL))

Véletlenszám generátorral kockadobás értéket a következképp produkálhatunk:
rand()%6 + 1

Ha a fejleszt rendszerben nincs lebegpontos véletlenszámot generáló függvény, akkor 0 és 1 közötti véletlenszámokat a következ kifejezéssel állíthatunk el:
(double)rand()/RAND_MAX

Folytassuk az UjLap függvényt!
int UjLap(void){ int i, j, db; if(ktydb==SZINDB*KTYDB) srand(time(NULL)); if(ktydb){ if(ktydb>=KTYDB) while(kty[i=rand()%SZINDB][j=rand()%KTYDB]); else{ db=ktydb>1?rand()%ktydb+1:1; for(i=0; i
Elkészültünk a kibvített KARTYA.C­vel, s most írjuk meg a mködtet PELDA24.C programot!

C programnyelv
/* PELDA24.C: Öt lap osztása. */ #include #include #include "KARTYA.H" #define LAPDB 5 void main(void){ int i, c; printf("Zsugázás: %d lap leosztása:\n", LAPDB); while(printf("Ossza már (I/N)? "), (c=toupper(getchar()))!='N'){ if(c=='I'){ putchar('\n'); if(Mennyi()
193

Megoldandó feladatok: Javítson a közölt programon a következ módon: · A leosztott öt lap megjelentetése történjék színek szerint, és azon belül kártyák szerint rendezetten! · A kapott öt lapból legyen legfeljebb 3 cserélhet! · A játék mködjék francia kártyával! Ahhoz, hogy a többdimenziós tömbök bels szerkezetét megértsük, hozzunk létre dinamikusan egy mátrixot! 9.7.2 Dinamikus memóriakezelés A C a memóriát három részre osztja. Az elsdleges adatterületen helyezi el a fordító a konstansokat és a statikus objektumokat. A lokális objektumokat és a függvényparamétereket a verembe (stack) teszi. A harmadik memóriaterületet ­ nevezzük heap­nek, bár más névvel is szokták illetni ­ futás közben éri el a C program, és változó méret memória blokkok dinamikus allokálására való. Például fák, listák, tömbök vagy bármi más helyezhet el rajta. Az ismertetett, ANSI szabványos függvények prototípusai az STDLIB.H fejfájlban találhatók. void *calloc(size_t tetelek, size_t meret); A calloc tetelek*meret méret memória blokkot foglal, feltölti 0X00-val és visszaadja kezdcímét. Tulajdonképpen tetelek elemszámú tömbnek foglal helyet, ahol egy elem mérete meret bájt.

194

MUTATÓK

Ha nincs elég hely, vagy a tetelek*meret szorzat értéke zérus, NULL mutatót kapunk vissza. void *malloc(size_t meret); A malloc legalább meret bájtos memória blokkot foglal, nem tölti fel semmivel és visszaadja kezdcímét. A blokk nagyobb lehet meret bájtnál a tárillesztéshez igényelt, plusz terület és a karbantartási információ elhelyezése miatt. Ha nincs elég hely a heap-en, NULL mutatót kapunk vissza a függvénytl. Ha a meret 0, a malloc zérusméret blokkot allokál, és érvényes mutatót ad vissza erre a területre. Jó néhány szabványos függvény is hívja a malloc­ot. Például a calloc, a getchar stb. A calloc­kal, a malloc­kal lefoglalt, vagy a rögtön ismertetend realloc­kal újrafoglalt terület tárillesztése olyan, hogy bármilyen típusú objektum elhelyezésére alkalmas. A függvényektl visszakapott cím explicit típusmódosító szerkezettel bármilyen típusúvá átalakítható. Tegyük fel, hogy a program futása közben derül ki egy double tömb mérete! Ezt az értéket az int típusú, N változó tartalmazza. Hogyan lehet létrehozni, kezelni, s végül felszabadítani egy ilyen tömböt?
/* . . . */ int N; double *dtomb; /* . . . */ /* Itt kiderül, hogy mennyi N. */ /* . . . */ /* Ettl kezdve szükség van a tömbre. */ if((dtomb=(double *)malloc(N*sizeof(double)))!=NULL){ /* Sikeres a memóriafoglalás, azaz használható a tömb. Például a 6. eleme dtomb[5] módon is elérhet. */ /* . . . */ /* Nincs szükség a továbbiakban a tömbre. */ free(dtomb); /* . . . */ } else /* Hibakezelés. */ /* . . . */

void *realloc(void *blokk, size_t meret); A realloc meret méretre szkíti vagy bvíti a korábban malloc, calloc vagy realloc hívással allokált memória blokkot, melynek kezdcímét megkapja a blokk paraméterben. Sikeres esetben visszaadja az átméretezett memória blokk kezdcímét. Ez a cím nem feltétlenül egyezik meg a

C programnyelv

195

blokk paraméterben átadottal. Címeltérés esetén a függvény a korábbi memória blokk tartalmát átmozgatja az újba. Az esetleges rövidüléstl eltekintve elmondható, hogy az új blokk megrzi a régi tartalmát. Ha az újraallokálás sikertelen memória hiány miatt, ugyancsak NULL mutatót kapunk, de az eredeti blokk változatlan marad. void free(void *blokk); A free deallokálja vagy felszabadítja a korábban malloc, calloc vagy realloc hívással allokált memóriaterületet, melynek kezdcímét megkapja a blokk paraméterben. A felszabadított bájtok száma egyezik az allokációkor (vagy a realloc esetén) igényelttel. Ha a blokk NULL, a mutatót elhagyja a free, és rögtön visszatér. Az érvénytelen mutatós (nem calloc, malloc, vagy realloc függvénnyel foglalt memória terület címének átadása) felszabadítási kísérlet befolyásolhatja a rákövetkez allokációs kéréseket, és fatális hibát is okozhat. Folytassuk a mátrixos feladatot!
/* PELDA25.C: Kétdimenziós tömb létrehozása dinamikusan.*/ #include #include typedef long double TIPUS; typedef TIPUS **OBJEKTUM; int m=3, n=5; /* Sorok és oszlopok száma. */ int main(void) { OBJEKTUM matrix; int i, j; /* A sorok létrehozása: */ printf("%d*%d-s, kétdimenziós tömb létrehozása " "dinamikusan.\n", m, n); if(!(matrix=(OBJEKTUM)calloc(m, sizeof(TIPUS *)))){ printf("Létrehozhatatlanok a mátrix sorai!\n"); return 1; } /* Az oszlopok létrehozása: */ for(i = 0; i < m; ++i) if(!(matrix[i]=(TIPUS *)malloc(n*sizeof(TIPUS)))){ printf("Létrehozhatatlan a mátrix %d. sora!\n", i); while(--i>=0) free(matrix[i]); free(matrix); return 1; } /* Mesterséges inicializálás: */ for(i = 0; i < m; ++i) for(j = 0; j < n; ++j) matrix[i][j] = rand(); /* Kiírás: */

196
printf("A mátrix tartalma:\n"); for(i = 0; i < m; ++i) { for(j = 0; j < n; ++j) printf("%10.0Lf", matrix[i][j]); printf("\n"); } /* Az oszlopok felszabadítása: */ for(i = 0; i < m; ++i) free(matrix[i]); /* Sorok felszabadítása: */ free(matrix); return 0; }

MUTATÓK

Vegyük észre, hogy a main­nek van visszaadott értéke! Zérust szolgáltat, ha minden rendben megy, és 1­et, ha memóriafoglalási probléma lép fel. Figyeljük meg azt is, hogy a memória felszabadítása foglalásával éppen ellenkez sorrendben történik, hogy a heap­en ne maradjanak foglalt ,,szigetek"! A heap C konstrukció, s ha a program befejezdik, akkor maga is felszabadul, megsznik létezni. Összesítve: A matrix azonosító a tömb kezdetére mutató változó mutató. A tömb kezdete viszont m változó mutatóból álló mutatótömb. A mutatótömb egy-egy eleme n elem, long double típusú tömb kezdetére mutat. A példa a részek memóriabeli elhelyezkedését is szemlélteti, azaz: · elbb az m elem mutatótömböt allokáljuk, aztán · a mátrix els sora (0-s index) n elemének foglalunk helyet. · A mátrix második sora (1-s index) n elemének helyfoglalása következik. · ... · Legvégül a mátrix utolsó (m-1 index) sorának n elemét helyezzük el a memóriában. A fordító ugyanezzel a módszerrel dolgozik, de az általa létrehozott mátrixban a mutatótömb konstans mutatókat tartalmaz, s a mátrix azonosítója is konstans mutató. A többdimenziós tömbök többféleképpen is szemlélhetk. A fordító által létrehozott
long double matrix[m][n];

mátrix példájánál maradva:

C programnyelv

197

· A matrix egy m elem vektor (tömb) azonosítója. E vektor minden eleme egy n elem, long double típusú vektor. · A matrix[i] (i = 0, 1, ..., m-1) az i­edik, n long double elem vektor azonosítója. · A matrix[i][j] (i = 0, 1, ..., m-1 és j = 0, 1, ..., n-1) a mátrix egy long double típusú eleme. A dolgokat más oldalról tekintve! · A matrix konstans mutató, mely az m elem, matrix[], konstans mutatótömb kezdcímét tartalmazza (tehát matrix[0]-ét). · A matrix+i e tömb i. elemének címe. E tömb elemeinek tartalmát megszemlélve láthatjuk, hogy n long double típusú változó méretével térnek el egymástól. · A matrix+i cím tartalma ugye *(matrix+i) vagy matrix[i]. · A matrix[i] tehát az i-edik, n darab long double elembl álló tömb azonosítója: konstans mutató, mely az i-edik, n long double elem tömb kezdetére mutat. A matrix konstans mutató e konstans mutatótömb kezdcímét tartalmazza. · A matrix[i]+j vagy *(matrix+i)+j az i­edik, n long double elem tömb j-edik elemének címe. E cím tartalma elérhet a következ hivatkozásokkal:
matrix[i][j], *(matrix[i]+j) vagy *(*(matrix+i)+j).

· A &matrix[0][0], a matrix[0], a &matrix[0] és a matrix ugyanaz az érték, azaz a mátrix kezdetének címe, de a matrix[0][0] egy long double azonosítója, a matrix[0] egy n long double elem tömb azonosítója, és a matrix a tömbökbl álló tömb azonosítója. Így:
&matrix[0][0] + 1 &matrix[0][1], matrix[0] + 1 *matrix + 1 &matrix[0][1], &matrix[0] + 1 matrix + 1 és matrix + 1 &matrix[1] &matrix[1][0].

32 bites címeket feltételezve, 1000­rl indulva, m=3 és n=5 esetén a matrix a következképpen helyezkedhet el a memóriában:

198 matrix[0] matrix: 1012 1000 1012 1062 1112 matrix[1] 1062 1004 1022 1072 1122 matrix[2] 1112 1008 1032 1082 1132

MUTATÓK

matrix[0]:matrix[0][0] matrix[0][1] matrix[0][2] matrix[0][3] matrix[0][4] 1042 1092 1142 1052 1102 1152 matrix[1]:matrix[1][0] matrix[1][1] matrix[1][2] matrix[1][3] matrix[1][4] matrix[2]:matrix[2][0] matrix[2][1] matrix[2][2] matrix[2][3] matrix[2][4] A háromdimenziós tömböt úgy valósítja meg a fordító, hogy létrehoz elbb egy mutatótömböt, melynek mindenegyes eleme egy, az elzekben ismertetett szerkezet mátrixra mutat. Néhány szó még a többszörösen alkalmazott index operátorról! A kifejezés1[kifejezés2][kifejezés3]...­ban az index operátorok balról jobbra kötnek, így a fordító elször a legbaloldalibb kifejezés1[kifejezés2] kifejezést értékeli ki. A született mutató értékhez aztán hozzáadva kifejezés3 értékét új mutató kifejezést képez, s ezt a folyamatot a legjobboldalibb index kifejezés összegzéséig végzi. Ha a végs mutató érték nem tömb típust címez, akkor az indirekció mvelete következik. Tehát például:
matrix[2][3] (*(matrix+2))[3] *(*(matrix+2)+3)

9.8 Tömbök, mint függvényparaméterek Ha van egy
float vektor[100];

tömbünk, és kezdcímével meghívjuk az
fv(vektor)

függvényt, akkor a függvény definíciójának
void Fv(float *v) { /* . . . */ }

vagy
void Fv(float v[]){ /* . . . */ }

módon kell kinéznie. Az utóbbi alakról tudjuk, hogy a fordító rögtön és automatikusan átkonvertálja az elz (a mutatós) formára.

C programnyelv

199

Ne feledjük, hogy ugyan a vektor konstans mutató a hívó függvényben, de v (címmásolat) már változó mutató a meghívott függvényben. Vele tehát elvégezhet például a v++ mvelet. A *v, a *(v+i) vagy a v[i] balérték alkalmazásával a vektor tömb bármelyik eleme módosítható a meghívott függvényben. Emlékezzünk arra is, hogy a meghívott függvényt is tájékoztatni kell valahogyan a tömb méretérl. Például úgy, hogy a méretet is átadjuk paraméterként. Az itt elmondottak többdimenziós tömbök vonatkozásában is igazak, de ott már nem ismételjük meg! Ha van egy
float matrix[10][20];

definíciónk, és az azonosítóval meghívjuk
Fvm(matrix)

módon az Fvm függvényt, akkor hogyan kell az Fvm definíciójának kinéznie? A
void Fvm(float **m) { /* . . . */ }

próbálkozás rossz, mert a formális paraméter float mutatóra mutató mutató. A
void Fvm(float *m[20]) { /* . . . */ }

változat sem jó, mert így a formális paraméter 20 elem, float típusú objektumokra mutató mutatótömb. Ennél a kísérletnél az az igazi probléma, hogy a [] operátor prioritása nagyobb, mint *-é. Nekünk formális paraméterként 20 float elem tömbre mutató mutatót kéne átadni. Tehát a helyes megoldás:
void Fvm(float (*m)[20]) { /* . . . */ }

vagy a ,,tradicionális" módszer szerint:
void Fvm(float m[][20]) { /* . . . */ }

, amibl rögtön és automatikusan elállítja a fordító az elz (a mutatós) alakot. Meg kell még említenünk, hogyha a többdimenziós tömböt dinamikusan hozzuk létre, akkor az elzleg ajánlott megoldás nyilvánvalóan helytelen. A mátrix ,,horgonypontját" ebben az esetben
float **matrix;

módon definiáljuk, ami ugye float mutatóra mutató mutató. Tehát ilyenkor a
Fvmd(matrix)

200

MUTATÓK

módon hívott Fvmd függvény helyes formális paramétere:
void Fvmd(float **m) { /* . . . */ }

Megoldandó feladatok: Készítsen programot két mátrix összeadására! A mátrixoknak ne dinamikusan foglaljon helyet a memóriában! A mátrixok mérete azonban csak futás idben válik konkréttá. Írjon szoftvert két mátrix összeadására és szorzására! A mátrixok mérete itt is futás közben dl el! A programban használjon függvényeket · a mátrix méretének · és elemeinek bekéréséhez, valamint · a két mátrix összeszorzásához! A két utóbbi függvény paraméterként kapja meg a mátrixokat! 9.9 Parancssori paraméterek Minden C programban kell lennie egy a programot elindító függvénynek, mely konzol bázisú alkalmazások esetében a main függvény. Most és itt csak a main paramétereivel és visszatérési értékével szeretnénk foglalkozni! A paraméterekrl állíthatjuk, hogy: · elhagyhatóak és · nem ANSI szabványosak. A main legáltalánosabb alakja:
int main(int argc, char *argv[]);

A paraméterek azonosítói bizonyos, C nyelvet támogató környezetekben ettl el is térhetnek, de funkciójuk akkor is változatlan marad. Az argc a main-nek átadott parancssori paraméterek száma, melyben az indított végrehajtandó program azonosítója is benne van, és értéke így legalább 1. Az argv a paraméter karakterláncokra mutató mutatótömb, ahol az egyes elemek rendre: · argv[0]: A futó program (meghajtónévvel és) úttal ellátott azonosítójára mutató mutató. · argv[1]: Az els parancssori paraméter karakterláncára mutató mutató.

C programnyelv · argv[2]: Az második paraméter karakterlánc kezdcíme. · ...

201

· argv[argc - 1]: Az utolsó parancssori paraméter karakterláncára mutató mutató. · argv[argc]: NULL mutató. Megjegyezzük, hogy az argc és az argv main paraméterek elérhetk az
extern int _argc; extern char **_argv;

globális változókon át is (STDLIB.H)! A main lehetséges alakjai a következk:
void main(void); int main(void); int main(int argc); int main(int argc, char *argv[]);

Vegyünk egy példát!
/* PELDA26.C: Parancssori paraméterek. */ #include #include int main(int argc,char *argv[]){ int i=0; printf("Parancssori paraméterek:\n"); printf("Argc értéke %d.\n", argc); printf("Az átadott parancssori paraméterek:\n"); for(i=0; i
Tételezzük fel, hogy a programot a következ parancssorral indítottuk:
PELDA26 elso_par "sodik par" 3 4 stop!

Ekkor a megjelen kimenet a következ lehet:
Parancssori paraméterek: Argc értéke 6. Az átadott parancssori paraméterek: argv[0]: C:\C\PELDA26.EXE argv[1]: elso_par argv[2]: sodik par argv[3]: 3 argv[4]: 4 argv[5]: stop!

202

MUTATÓK

Beszéljünk kicsit a printf utolsó, *argv++ kifejezésérl! Az argv­t a main paraméterként kapja, tehát csak címmásolat, vagyis a main­ben akár el is rontható. Az argv típusa char **, és funkcionálisan a parancssori paraméter karakterláncok kezdcímeit tartalmazó mutatótömb kezdetének címe. A rajta végrehajtott indirekcióval a típus char * lesz, s épp a mutatótömb els elemét (argv[0]) érjük el. Az utótag ++ operátor miatt eközben az argv már a második mutatótömb elemre (argv[1]) mutat. Elérjük ezt is, és mellékhatásként az argv megint elbbre mutat egy tömbelemmel. Tehát a ciklusban rendre végigjárjuk az összes parancssori paramétert. Jusson eszünkbe, hogy a main-nek átadott parancssor maximális hosszát korlátozhatja az operációs rendszer! A legtöbb operációs rendszerben léteznek
változó=érték

alakú, ún. környezeti változók, melyek definiálják a környezetet (információt szolgáltatnak) az operációs rendszer és a benne futó programok számára. Például a PATH környezeti változó szokta tartalmazni az alapértelmezett keresési utakat a végrehajtható programokhoz, a parancsinterpreter helyét írja el a COMSPEC, és így tovább. Az operációs rendszer természetesen lehetséget biztosít ilyen környezeti változók törlésére, megadására, és értékük módosítására. C­bl a környezeti változók általában az STDLIB.H­ban deklarált, nem ANSI szabványos extern char **_environ; globális változóval érhetk el. Ez karakterláncokra mutató mutatótömb, és a mutatott karakterláncok a változó=érték alakú környezeti változókat írják le. Az utolsó utáni környezeti változó karakterláncára mutató tömbelem itt is NULL mutató, mint az argv­nél. A globális változó nevét azért nem árt pontosítani a programfejleszt rendszer segítségébl! A másik lehetség az STDLIB.H­ban deklarált, szabványos, nem kis­ nagybet érzékeny char *getenv(const char *valtozo); függvény, mely visszaad az aktuális környezet alapján a valtozo nev környezeti változó értékére mutató mutatót. Ha nincs ilyen változó az aktuális környezeti táblában, NULL mutatót kapunk. A visszakapott nem NULL mutatóval azonban nem célszer és nem biztonságos dolog a kör-

C programnyelv

203

nyezeti változó értékét módosítani. Ehhez a nem szabványos putenv rutin használata ajánlott. A környezeti változó nevének a végére nem kell kitenni az = jelet, azaz például a PATH környezeti változót a getenv("PATH") hívással kérdezhetjük le! Megoldandó feladatok: Készítsen programot, mely a JANI, fordítási idben változtatható azonosítójú, környezeti változóról megállapítja, hogy létezik­e! Ha létezik, akkor eldönti, hogy értéke ,,kicsi", ,,nagy", vagy más. A feladat fokozható egyrészt úgy, hogy a változó lehetséges értékei is legyenek fordítási idben módosíthatók, másrészt úgy, hogy ne rögzítsük kettben a lehetséges értékek darabszámát! Írjon szoftvert, mely a környezeti változó azonosítóját és lehetséges értékeit parancssori paraméterekként kapja meg, és megállapításai az elz példában megfogalmazottakkal azonosak! Ha a programot paraméter nélkül indítják, akkor tájékoztasson használatáról! Ha expliciten nem deklaráljuk void-nak, akkor a main-nek int típusú státuszkóddal kell visszatérnie az t indító programhoz (process), rendszerint az operációs rendszerhez. Konvenció szerint a zérus visszaadott érték (EXIT_SUCCESS) hibátlan futást, s a nem zérus státuszkód (EXIT_FAILURE 1) valamilyen hibát jelez. Magát a main-bl való visszatérést (mondjuk 1­es státuszkóddal) megoldhatjuk a következ módok egyikével:
return 1; exit(1);

Foglalkozzunk kicsit a programbefejezéssel is! 9.9.1 Programbefejezés A return 1 csak a main­ben kiadva fejezi be a program futását. Az STDLIB.H bekapcsolásakor rendelkezésre álló, mindegyik operációs rendszerben használható void exit(int statusz); void abort(void); függvények mind befejezik annak a programnak a futását, amiben meghívják ket akármilyen mély rutin szintrl is. A statusz paraméter értékét visszakapja a befejezettet indító (várakozó szül) program, mint kilépési állapotot (exit status). A statusz értéket átveszi persze az operációs rend-

204

MUTATÓK

szer is, ha volt a befejezett program indítója. Zérus (EXIT_SUCCESS) állapottal szokás jelezni a normál befejezést. A nem zérus állapot valamilyen hibát közöl (EXIT_FAILURE 1). Az exit függvény a program befejezése eltt meghív minden regisztrált (lásd atexit!) kilépési függvényt, kiüríti a kimeneti puffereket, és lezárja a nyitott fájlokat. Az abort alapértelmezés szerint befejezi az aktuális programot. Megjelenteti például az
Abnormal program termination

üzenetet az stderr­en, és aztán SIGABRT (abnormális programbefejezés) jelet generál. Ha nem írtak kezelt (signal) a SIGABRT számára, akkor az alapértelmezett tevékenység szerint az abort 3­as státuszkóddal visszaadja a vezérlést a szül programnak. Szóval nem üríti a puffereket, és nem hív meg semmilyen kilépési függvényt (atexit) sem. Az stderr a szabvány hibakimenet. Az atexit és a signal függvényekrl rögtön szó lesz a következ fejezetben! Megoldandó feladatok: Készítsen programot, mely neveket olvas a szabvány bemenetrl! Egy sorban egy név érkezik, s az üres sor a bemenet végét jelzi. A név nagybetvel kezddik, és a többi karaktere kisbet. A feltételeket ki nem elégít név helyett azonnal másikat kell kérni a probléma kijelzése után! A neveket rendezze névsorba, s listázza ki ket lapokra bontva! A feladat a következképp fokozható: · Ha a névben az angol ábécébelieken kívül az ékezetes kis és nagybetk is megengedettek. · Ha a neveket közl listán elre­hátra lehet lapozni. · Ha egy nevet csak egyszer lehet megadni, azaz a második bevitelt elutasítja hibaként a szoftver. · Ha a programot ,,­v" parancssori paraméterrel indítják, akkor a rendezés visszafelé halad a névsoron. · Ha a neveknek dinamikusan foglal helyet, kezdcímeiket mutatótömbben helyezi el, és a rendezésnél a mutatótömb elemeket cserélgeti, s nem a név karakterláncokat a szoftver.

C programnyelv

205

9.10 Függvény (kód) mutatók A mutatók függvények ún. belépési pontjának címét is tartalmazhatják, s ilyenkor függvény vagy kódmutatókról beszélünk. Ha van egy
int fv(double, int);

prototípusú függvényünk, akkor erre mutató mutatót
int (*pfv)(double, int);

módon deklarálhatunk. A pfv azonosító ezek után olyan változót deklarál, melyben egy double, s egy int paramétert fogadó és int-tel visszatér függvények címeit tarthatjuk. A pfv tehát változó kódmutató. Kódmutató konstans is létezik azonban, s ez a függvénynév (a példában az fv). Vigyázzunk a deklarációban a függvénymutató körüli kerek zárójel pár el nem hagyhatóságára, mert az
int *pfv(double, int);

olyan pfv azonosítójú függvényt deklarál, mely egy double, s egy int paramétert fogad, és int típusú objektumra mutató mutatóval tér vissza. A probléma az, hogy a mutatóképz operátor (*) prioritása alacsonyabb a függvényképzénél (()). Hogyan lehet értéket adni a függvénymutatónak? Természetesen a szokásos módokon, azaz hozzárendeléssel:
pfv = fv;

, ill. a deklarációban inicializátor alkalmazásával:
int fv(double, int); int (*pfv)(double, int) = fv;

Vigyázzunk nagyon a típussal, mert az most ,,bonyolódott"! Csak olyan függvény címe tehet be a pfv-be, mely a függvénymutatóval egyez típusú, azaz int-et ad vissza, egy double és egy int paramétert fogad ebben a sorrendben. A
void (*mfv)();

szerint az mfv meg nem határozott számú és típusú paramétert fogadó olyan függvényre mutató mutató, melynek nincs visszatérési értéke. Vegyük észre, hogy a kérdéses függvények definíciója eltt függvénymutatók inicializálására is használható a megadott függvény prototípus!

206

MUTATÓK

Hogyan hívhatjuk meg azt a függvényt, melynek címét a kódmutató tartalmazza? Alkalmaznunk kell a mutatókra vonatkozó ökölszabályunkat, ami azt mondja ki, hogy ahol állhat azonosító a kifejezésben, ott állhat (*mutatóazonosító) is. Vegyük el újra az elz példát! Ha
int a = fv(0.65, 8);

az fv függvény hívása, és valamilyen módon lezajlott a pfv = fv hozzárendelés is, akkor az
a = (*pfv)(0.65, 8);

ugyanaz a függvényhívás. Itt a pfv-re alkalmaztuk az indirekció operátort (*), de mivel ennek prioritása alacsonyabb a függvényhívás operátorénál (()), ezért a *pfv-t külön zárójelbe kellett tenni! A kódmutatóval kapcsolatos alapismeretek letárgyalása után feltétlenül ismertetni kell a C fordító függvényekkel kapcsolatos fontos viselkedését: implicit konverzióját! Ha a kifejezés típussal visszatér függvény típusú, akkor hacsak nem cím operátor (&) mögött áll, típussal visszatér függvénymutató típusúvá konvertálja automatikusan és azonnal a fordító. Ez az implicit konverzió mindenek eltt megvalósul a
utótag-kifejezés()

függvényhívásban, ahol az utótag-kifejezésnek kell típussal visszatér függvénycímmé kiértékelhetnek lennie. A típus a függvényhívás értékének típusa. A dolog praktikusan azt jelenti, hogy a függvény bármilyen függvényre mutató kifejezéssel meghívható. Milyen mveletek végezhetk a kódmutatókkal? · Képezhet a címük. · sizeof operátor operandusai lehetnek. · Végrehajtható rajtuk az indirekció mvelete is, mint láttuk. · Értéket kaphatnak, ahogyan azt az elzekben ismertettük. · Meghívhatók velük függvények. Ezt is áttekintettük. · Átadhatók paraméterként függvényeknek. · Kódmutatótömbök is létrehozhatók.

C programnyelv · Függvény visszaadott értéke is lehet.

207

· Explicit típuskonverzióval más típusú függvénymutatókká alakíthatók. Kódmutatókra azonban nem alkalmazható a mutatóaritmetika az egyenlségi reláció operátoroktól (== és !=) eltekintve. Foglalkozzunk a kódmutató paraméterrel! A függvényekre érvényes implicit típuskonverzió a függvényparaméterekre is vonatkozik. Ha a paraméter típussal visszatér függvény, akkor a fordító automatikusan és rögtön típus típusú értéket szolgáltató függvényre mutató mutatóvá alakítja át. A következ, kissé elvonatkoztatott példában kitnen megszemlélhet a kódmutató paraméter függvény prototípusban, ill. függvény aktuális és formális paramétereként.
/* . . . */ long Emel(int); long Lep(int); long Letesz(int); void Munka(int n, long (* fv)(int)); /* . . . */ void main(void){ int valaszt=1, n; /* . . . */ switch(valaszt){ case 1: Munka(n, Emel); break; case 2: Munka(n, Lep); break; case 3: Munka(n, Letesz); } /* . . . */ } void Munka(int n, long (* fv)(int)){ int i; long j; for(i=j=0; i
A kódmutató típusa szerint az ilyen függvény egy int paramétert fogad, és long értéket szolgáltat. Kódmutatók paraméterként való átadását a Programbefejezés fejezetben már megemlített, de ott nem tárgyalt 9.10.1 atexit függvény leírásával is szemléltetjük! #include

208 int atexit(void (cdecl * fv)(void));

MUTATÓK

Az atexit regisztrálja a paraméter függvénycímet, s normál programbefejezéskor az exit meghívja az fv-t a szül programhoz való visszatérés eltt. Az fv függvénymutató paraméterbl látszik, hogy a kilépési függvényeknek nincs paraméterük, és nem adnak vissza értéket. Az atexit mindenegyes hívásával más-más kilépési függvényt regisztráltathatunk. A regisztrálás veremszer, azaz a legutoljára regisztrált függvényt hajtja végre elször a rendszer, s aztán így tovább visszafelé. Az atexit a heap­et használja a függvények regisztrálásához, s így a regisztrálható kilépési függvények számát csak a heap mérete korlátozza. Az atexit sikeres híváskor zérust ad vissza, és nem zérust csak akkor kapunk tle, ha már nem tud több függvényt feljegyezni. Például:
#include #include void cdecl exitfv1(void){ printf("Exitfv1 végrehajtva!\n"); } void cdecl exitfv2(void){ printf("Exitfv2 végrehajtva!\n"); } int main(void){ atexit(exitfv1); atexit(exitfv2); printf("A main befejezdött.\n"); return 0; }

A szabvány kimenet a következ:
A main befejezdött. Exitfv2 végrehajtva! Exitfv1 végrehajtva!

Folytassuk tovább a kódmutatók tárgyalását! Azt mondottuk, hogy kódmutatók tömbökben is elhelyezhetk. Visszatérve az els pfv-s példánkhoz! Az
int (*pfvt[])(double, int) = {fv1, fv2, fv3, fv4, fv5};

deklarációval létrehoztunk egy pfvt azonosítójú, olyan ötelem tömböt, mely int-et visszaadó, egy double, és egy int paramétert fogadó függvények címeit tartalmazhatja. Feltéve, hogy fv1, fv2, fv3, fv4 és fv5 ilyen prototípusú függvények, a pfvt tömb elemeit kezdértékkel is elláttuk ebben a deklarációban. Hívjuk még meg, mondjuk, a tömb 3. elemét!

C programnyelv
a = (*pfvt[2])(0.65, 8);

209

Alakítsuk át függvénymutató tömböt használóvá a kódmutató paraméternél ismertetett példát! Az új megoldásunk main­en kívüli része változatlan, a main viszont:
void main(void){ int valaszt=1, n; long (*fvmt[])(int) = {Emel, Lep, Letesz}; /* . . . */ Munka(n, fvmt[valaszt]); /* . . . */ }

A kódmutató visszatérési értékhez elemezzük ki a következ függvény prototípust! void (*signal(int jel, void (* kezelo)(int)))(int); A signal els paramétere int, a második (void (* kezelo)(int)) viszont értéket vissza nem adó, egy int paramétert fogadó függvénymutató típusú. Kitnen látszik ezek után, hogy a visszatérési érték void (*)(int), azaz értéket nem szolgáltató, egy int paramétert fogadó függvénymutató. A visszaadott érték típusa tehát a signal második paraméterének típusával egyezik. A SIGNAL.H fejfájlban elhelyezett prototípusú signal függvénnyel különben a program végrehajtása során bekövetkez, váratlan eseményeket (megszakítás, kivétel, hiba stb.), ún. jeleket lehet lekezeltetni. Többféle típusú jel létezik. A void (*)(int) típusú kezelfüggvényt a manipulálni kívánt jelre külön meg kell írni. A signal rutinnal hozzárendeltetjük a kezelt (2. paraméter) az els paraméter típusú jelhez, és a signal az eddigi kezel címével tér vissza. Egy bizonyos típusú függvénymutató explicit típuskonverzióval
(típusnév) eltag-kifejezés

átalakítható más típusú kódmutatóvá. Ha az e módon átkonvertált mutatóval függvényt hívunk, akkor a hatás a programfejleszt rendszertl, a hardvertl függ. Viszont, ha visszakonvertáljuk az átalakított mutatót az eredeti típusra, akkor az eredmény azonos lesz a kiindulási függvénymutatóval. Szedjük csak megint el az
int fv(double, int), a; int (*pfv)(double, int) = fv;

példánkat, és legyen a

210
void (*vpfv)(double); vpfv=(void (*)(double))pfv;

MUTATÓK

A
(*vpfv)(0.65);

eredményessége eléggé megkérdjelezhet, de a
pfv=(int (*)(double, int))vpfv;

után teljesen rendben lesz a dolog:
a=(*pfv)(0.65, 8);

Emlékezzünk csak! Explicit típusmódosított kifejezés nem lehet balérték. Foglalkozzunk csak újra egy kicsit a típusnevekkel! 9.10.2 Típusnév Explicit típusmódosításban, függvénydeklarátorban a paramétertípus rögzítésekor, sizeof operandusaként stb. szükség lehet a típus nevének megadására. Ehhez kell a típusnév, mely szintaktikailag a kérdéses típusú objektum olyan deklarációja, melybl elhagyták az objektum azonosítóját.
típusnév: típusspecifikátor-lista absztrakt-deklarátor: mutató direkt-absztrakt-deklarátor: (absztrakt-deklarátor) [] ()

Az absztrak-deklarátorban mindig lokalizálható az a hely, ahol az azonosítónak kéne lennie, ha a konstrukció deklaráción belüli deklarátor lenne. Lássunk néhány konkrét példát!
int *

int típusú objektumra mutató mutató.
int **

int típusú objektumra mutató mutatóra mutató mutató.

C programnyelv
int *[]

211

Nem meghatározott elemszámú, int típusú mutatótömb.
int *()

Ismeretlen paraméterlistájú, int­re mutató mutatóval visszatér függvény.
int (*[])(int)

int típussal visszatér, egy int paraméteres, meghatározatlan elemszámú függvénymutató tömb.
int (*(*())[])(void)

Ismeretlen paraméterezés, int típussal visszatér függvénymutatókból képzett, meghatározatlan méret tömbre mutató mutatót szolgáltató, paraméterrel nem rendelkez függvény. A problémán: a sok zárójelen, a nehezen érthetségen typedef alkalmazásával lehet segíteni. 9.11 Típusdefiníció (typedef) Az elemi típusdefinícióról szó volt már a TÍPUSOK ÉS KONSTANSOK szakasz végén. Az ott elmondottakat nem ismételjük meg, viszont annyit újra szeretnénk tisztázni, hogy: A típusdefiníció nem vezet be új típust, csak más módon megadott típusok szinonimáit állítja el. A typedef név, ami egy azonosító, szintaktikailag egyenérték a típust leíró kulcsszavakkal, vagy típusnévvel. A típusdefiníció ,,bonyolításához" elször azt említjük meg, hogy a
typedef típus azonosító;

szerkezetben az azonosító a prioritás sorrendjében lehet: · azonosító(): függvény típust képz, utótag operátor. Például:
typedef double dfvdi(double, int); dfvdi hatvany;

, ahol a hatvany egy double, s egy int paramétert fogadó és double-t visszaadó függvény azonosítója. · azonosító[]: tömb típust képz, utótag operátor. Például:
typedef double dtomb[20]; dtomb t;

, ahol a t 20 double elembl álló tömb azonosítója.

212

MUTATÓK

· *azonosító: mutató típust képz, eltag operátor. Például:
typedef short *shptr; shptr sptr;

, ahol az sptr short típusú objektumra mutató mutató azonosítója. · Ezek az operátorok egyszerre is elfordulhatnak az azonosító-val. Például:
typedef int *itb[10]; itb tomb;

, ahol a tomb 10 elem int objektumokra mutató mutatótömb azonosítója. Megemlítend még, hogy az elzek alkalmazásával ,,csínján" kell bánni, hisz az így típusdefiniált azonosítóknak éppen a jellege (függvény, tömb, mutató) veszik el. A típusdefiníció további komplexitása abból fakad, hogy a
typedef típus azonosító;

szerkezetbeli típus korábbi typedef típus azonosító;-ban definiált azonosító is lehet. Tehát a típusdefinícióval létrehozott típuspecifikátor típus lehet egy másik típusdefinícióban. Nézzünk néhány példát!
typedef int *iptr; typedef char nev[30]; typedef enum {no, ferfi, egyeb} sex; typedef long double *ldptr; ldptr ptr2; /* long double objektumra mutató mutató.*/ ldptr fv2(nev);/*30 elem karaktertömb paramétert fogadó, long double objektumra mutató mutatóval visszatér függvény. */ typedef iptr (*ipfvi)(sex); ipfvi fvp1; /* int-re mutató mutatót visszaadó, egy sex típusú enum paramétert fogadó függvényre mutató mutató. */ typedef ipfvi ptomb[5]; ptomb tomb; /* 5 elem, int-re mutató mutatót szolgáltató, egy sex típusú paramétert fogadó függvényre mutató mutatótömb. */ iptr fugg(ptomb);/*int-re mutató mutatót visszaadó, 5 elem, int-re mutató mutatóval visszatér, egy sex enum paraméteres függvényre mutató mutatótömböt paraméterként fogadó függvény. */

C programnyelv

213

A típusdefinícióval a program típusai parametrizálhatók, azaz a program portábilisabb lesz, hisz az egyetlen typedef módosításával a típus megváltoztatható. A komplex típusokra megadott typedef nevek ezen kívül javítják a program olvashatóságát is. Lokális szinten megadott típusdefiníció lokális hatáskör is. Az általánosan használt típusdefiníciókat globálisan, a feladathoz tartozó fejfájlban szokták elírni. Egy utolsó kérdés: Mikor ekvivalens két típus? · Ha a két típusspecifikátor-lista egyezik, beleértve azt is, hogy ugyanaz a típusspecifikátor többféleképpen is megadható. Például: a long, a long int és a signed long int azonosak. · Ha az absztrakt-deklarátoraik a typedef típusok kifejtése, és bármely függvényparaméter azonosító törlése után ekvivalens típusspecifikátor-listákat eredményeznek. A típusekvivalencia meghatározásánál a tömbméretek és a függvényparaméter típusok is lényegesek. 9.12 Ellenrzött bemenet Jegyzetünkben minden feladat megoldásában azt sugalltuk, hogy: A programnak ellenriznie kell a bemenetét. Ez a vizsgálat természetesen csak a konkrét adatok ismerete nélkül a lehetetlenségek, és a problémát okozó értékék kiszrésére szorítkozhat. Például: Ne etessünk két tonnás kutyát! Ne folyósítsunk nyugdíjat 300 éves embernek! Nem tekinthet, csak legfeljebb mkedvel, programnak az, ami egy véletlenül elgépelt információ miatt ,,feldobja a talpát"! Írjon int getint(int *) függvényt, mely ellenrzötten beolvas egy egész számot a billentyzetrl úgy, hogy a nem megengedett karaktereket nem is echózza a karakteres képernyn (ablakban). A szám eltti fehér karakterek közül az Enter­t soremeléssel, és minden más fehér karakter szóközzel echózandó! Ezután egy opcionális eljelet követen már csak számjegy karakterek következhetnek. Ha az els számjegy zérus, akkor további szám karakterek sem jöhetnek. A függvény legyen portábilis, azaz mködjön 16 és 32 bites int­re egyaránt! Ez azt jelenti 16 bites esetre, hogy legfeljebb öt számjegyet echózhat, de legyen tekintettel az ábrázolási korlátokra is! Ha az els 4 számjegy 3276­nál nagyobb, akkor több számjegyet már nem fogadhat. Ha az els 4 számjegy pontosan 3276, akkor ötödik számjegyként nem fogadhatja a függvény a 9­et, s a 8­at is

214

MUTATÓK

csak negatív egész szám esetén. A szám megadását fehér karakterrel, Ctrl+Z­vel vagy F6­tal kell lezárni. A fehér karaktert a már leírt módosított echó után vissza kell adni a hívónak. Ctrl+Z vagy F6 esetén viszont EOF szolgáltatandó. A beolvasott egész érték konvertálandó és elhelyezend a paraméter címen! A címen lev érték azonban nem változhat meg, ha nem adtak meg egyetlen számjegy karaktert sem. A rutin persze rövid programmal ki is próbálandó! A feladat megoldásához szükségünk van két konzolkezel függvényre. Az int putch(int c); rutin a c karaktert írja ki közvetlenül (pufferezés nélkül) a konzol képernyre (ablakba) az aktuális pozíciótól, aktuális színben és megjelenési attribútumokkal, s a kurzort eggyel elbbre állítja. Sikeres esetben a viszszakapott érték maga a c karakter. A sikertelenséget viszont EOF-fal jelzi a függvény. Kivitelkor nincs transzláció, azaz a függvény az LF ('\n') karakterbl nem állít el CR-LF ("\r\n") karakter párt. Megoldásunkban nem foglalkozunk majd a putch hibakezelésével, mert feltételezzük, hogyha az operációs rendszer mködik, akkor a konzol megy. int getch(void); Echó nélkül beolvas egyetlen karaktert a konzolról (billentyzet), s ezt szolgáltatja a hívónak. A bejöv karakter rögtön rendelkezésre áll, s nincs pufferezés soremelés karakterig. Funkció vagy nyíl billenty leütésekor a függvényt kétszer kell hívni, mert az els hívás zérussal tér vissza, s a második szolgáltatja az aktuális gomb kódját. A rutinnak nincs hibás viszszatérése. E függvények nem szabványosak, de szinte minden operációs rendszerben rendelkezésre állnak kisebb­nagyobb eltérésekkel a CONIO.H fejfájl bekapcsolása után.
/* PELDA27.C: Egészek beolvasása és visszaírása úgy, hogy az érvénytelen karakterek echója meg sem történik.*/ #include #include #include #if SHRT_MAX == INT_MAX #define HOSSZ 4 #else

C programnyelv
#define HOSSZ 9 #endif #define F6 64 #define CTRLZ 26 #define MAX INT_MAX/10 #define HATAR INT_MAX%10+'0'+1

215

A HOSSZ makró azt a számjegy mennyiséget rögzíti 16, és 32 bites int­re, ameddig még nem kell foglalkozni a megadott szám karakter értékével. A MAX maga az a HOSSZ számjegy érték, amihez még egy jegyet téve elérhet, de túl nem léphet a fels, vagy az alsó ábrázolási korlát. 16 bites int­nél ez az érték 3276, amihez pozitív irányban legfeljebb 7, s negatív irányban maximum 8 jöhet. A HATAR az a számjegy karakter, ami még negatív egész esetében elfordulhat MAX­ot követen megadható karakterként a HOSSZ+1. pozíción. 16 bites int számára ez az érték '8'.
int getint(int *pn){/* Egész beolvasása a bemenetrl. */ int c, /* A beolvasott karakter. */ sign=1, /* Eljel: pozitív +1, negatív -1. Alapértelmezés a pozitív, a kezdérték miatt. */ elojel=0, /* Volt-e már eljel? */ hossz=0, /* A beolvasott számjegy karakterek száma. */ null=0; /* A beolvasott szám zérus-e? */ while(!hossz) switch(c=getch()){ case ' ': case '\t': case '\r': if(!elojel) if(c!='\r')putch(' '); else {putch('\n'); putch('\r'); } break; case '+': case '-': if(!elojel){ putch(c); sign=(c=='+')?1:-1; ++elojel; } break; case '0': if(!elojel){ putch(c); *pn=0; ++hossz; null=1;} break; case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': putch(c); *pn=c-'0'; ++hossz; break; default: if(!c)c=getch(); if(c==CTRLZ||c==F6) return EOF; }

216

MUTATÓK

A getint lényegében két while ciklusra bontható, melyek mindegyike egy­egy switch. Az els addig tart, míg · egy számjegy karaktert meg nem adnak, vagy · Ctrl+Z­vel, ill. F6­tal le nem zárják a bemenetet. Az els switch: · Végrehajtja a fehér karakterekre elírt echót, de csak akkor, ha eljel karakter még nem volt. Magyarán eljel után nincs már echó a fehér karakterekre. · Az eljelet echózza a rutin, ha korábban még nem érkezett, és értékét megjegyzi a sign változóban. Bejelöli azt is, hogy volt már eljel, hogy még egyet ne tudjanak megadni. · Az els szám karaktert echózza a függvény, konvertálva kiteszi a paraméter címre, és a hossz változóban számlálja is. Eljel után nem enged már meg zérust gépelni, ill. ha megadható volt a nulla, akkor bejelzi bejövetelét a null változóba. · A default ágon újabb olvasás követi az elz zérus beérkezését. Az F6 másként vizsgálható sem lenne.
while(1) switch(c=getch()){ case ' ': case '\t': case '\r':if(c!='\r')putch(' '); else {putch('\n'); putch('\r'); } *pn*=sign; return c; case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': if(!null&&(hossz
C programnyelv A második while · fehér karakterrel, vagy · Ctrl+Z­vel, ill. F6­tal

217

zárul. A paraméter címen lev konvertált értéket eljel­helyessé teszi a rutin, majd visszatér a ki is echózott, fehér karakterrel, vagy az EOF­fal. A számjegy karakter echója, konverziója és számlálása csak akkor történik meg, ha az els szám nem zérus volt és: · HOSSZ­nál kevesebb karaktert adtak meg eddig, vagy · épp annyit, de a paraméter címre konvertált érték kisebb MAX­nál, ill. pont MAX és az utolsó számjegy karakter megfelel az elírt, szigorú feltételeknek. Megoldandó feladat: Írja úgy át a getint függvényt, hogy a visszatörlés (backspace) gombot funkciója szerinti módon kezelni tudja! Remélhetleg mindenki el tudja képzelni, hogy tovább bonyolódna a dolog, ha további szerkeszt billentyk (Delete, Insert, balra nyíl és jobbra nyíl) használatát is megengednénk, vagy ­ Uram, bocsá' ­ lebegpontos érték bekérését végeznénk és nem egészét. Magyarán: kitnen látszik, hogy ,,többe kerülne a leves, mint a hús". Összefoglalva: A bemenet ellenrzését sem szabad túlzásba vinni, de az ökölszabálynál írott elveket a jó programnak be kell tartania.

218

STRUKTÚRÁK ÉS UNIÓK

10 STRUKTÚRÁK ÉS UNIÓK
A struktúra és az unió aggregátum. Egy vagy több, esetleg különböz típusú változó (elnevezett tag) együttese, melynek önálló azonosítója van. A struktúrát más nyelvben rekordnak nevezik. Azt teszi lehetvé, hogy a valamilyen módon összetartozó változók egész csoportjára egyetlen névvel hivatkozhassunk, azaz hogy a változócsoport kezelése egyszerbb legyen. Tulajdonképpen minden struktúrával és unióval új, összetett, felhasználói adattípust hozunk létre. Az ANSI szabvány megengedi, hogy a struktúrákat át lehessen másolni egymásba, hozzá lehessen rendelni, és átadhatók legyenek függvényeknek, ill. rutinok visszatérési értéke is lehessen struktúra. Képezhet természetesen a struktúra címe (&), mérete (sizeof), és benne lehet explicit típusmódosító szerkezetben is, de a struktúrák nem hasonlíthatók össze. A struktúrában felsorolt változókat struktúratagoknak (member) nevezik. Struktúratag kis megszorításokkal - melyre késbb kitérünk - akármilyen típusú lehet. Lehet alap és származtatott típusú bármilyen sorrendben. A deklarációbeli típusspecifikátor egyik alternatívája a
struktúra-vagy-unió-specifikátor: struktúra-vagy-unió{struktúratag-deklarációlista} struktúra-vagy-unió azonosító struktúra-vagy-unió: struct union

A struktúratag-deklarációlista struktúra, ill. uniótag deklarációk sorozata:
struktúratag-deklarációlista: struktúratag-deklaráció struktúratag-deklarációlista struktúratag-deklaráció struktúratag-deklaráció: típusspecifikátor-lista struktúra-deklarátorlista típusspecifikátor-lista: típusspecifikátor típusspecifikátor-lista típusspecifikátor struktúra-deklarátorlista: struktúra-deklarátor struktúra-deklarátorlista, struktúra-deklarátor

C programnyelv

219

A struktúra-deklarátor többnyire a struktúra, ill. az unió egy tagjának deklarátora. A struktúratag azonban meghatározott számú bitbl is állhat, azaz lehet ún. bitmez (bit field) is, mely a nyelvben struktúrákon kívül nem is használható másutt. A mez bitszélességét a kettspontot követ, egész érték konstans-kifejezés határozza meg.
struktúra-deklarátor: deklarátor : konstans-kifejezés

A bitmezvel és az unióval még ebben a szakaszban foglalkozunk! 10.1 Struktúradeklaráció Alakja tehát a következ:
struct <{ struktúratag-deklarációlista }> ;

Például:
struct datum{ int ev, ho, nap, evnap; long datumssz; char datumlanc[11]; } d, dptr, dt[10]; /* Dátumsorszám. */ /* Azonosítólista. */

, ahol: · A tárolási-osztály-specifikátor elhagyásával, megadásával és ennek értelmezésével nem foglalkozunk újra! · A datum azonosító ennek a struktúrának a címkéje (struktúracímke), mely azt biztosítja, hogy késbb struct datum módon hivatkozni tudjunk a felhasználói típusra. Például: · Az ev, a ho, a nap és az evnap a struct datum típusú struktúra int típusú tagjainak, a datumssz a long típusú tagjának és a datumlanc a struktúra char* típusú tagjának az azonosítói. A tagneveknek csak a struktúrán belül kell egyedieknek lenniük, azaz a tagazonosítók nyugodtan egyezhetnek például más közönséges változók neveivel, vagy a struktúracímkékkel. · A d struct datum típusú változó, a dptr és az sptr struct datum típusú objektumra mutató mutatók, és a dt tíz, struct datum típusú elembl álló tömb azonosítója, azaz a dt struktúratömb.
struct datum *sptr = (struct datum *)malloc(sizeof(struct datum));

220

STRUKTÚRÁK ÉS UNIÓK

· A fordító a struktúrának éppen annyi helyet foglal a memóriában, hogy benne a struktúra minden tagja elférjen. A struktúratagok a deklaráció sorrendjében, folyamatosan növekv címeken helyezkednek el, s így a struktúradefinícióban késbb deklarált tag címe mindig nagyobb. Az azonosítólista nélküli struktúradeklarációt, ahol van struktúratag-deklarációlista, azaz megadottá válik a struktúra szerkezete, szokás struktúradefiníciónak is nevezni, ugyan nincs memóriafoglalása. · Minden struktúradeklaráció egyedi struktúra típust hoz létre, s így
struct A int i, double struct B int i, double { j; d; } a, a1; { j; d; } b;

az a és az a1 objektumok struct A típusú struktúrák, de az a és a b objektumok különböz struktúra típusúak annak ellenére is, hogy a két struktúra szerkezete azonos. · Az azonosítólista nélküli, de struktúracímkével és tag-deklarációlistával ellátott struktúradefiníció nem foglal ugyan helyet a memóriában, de biztosítja azt a lehetséget, hogy a struktúradeklaráció hatáskörében késbb ilyen típusú struktúrával azonosítólistát is megadva helyet foglalhassunk változóinknak, mutatóinknak és tömbjeinknek. Például:
struct datum{ int ev, ho, nap, evnap; long datumssz; /* Dátumsorszám. */ char datumlanc[11]; }; /* . . . */ struct datum d, *dptr=&d, dt[10];

Fedezzük fel, hogy a felhasználó definiálta típusnév struct datum. Hasonlításul:
double d, *dptr=&d, dt[10];

Struktúra, unió, enum deklarációt, definíciót záró kapcsos zárójel után kötelez pontosvesszt tenni, mert ez zárja az azonosítólistát!
struct struki{ int a, b; float matrix[20][10]; char nev[26]; }; /* Itt nem elhagyható a ; a } után! */

C programnyelv

221

· A struktúracímkének egyedinek kell lennie a struktúra, unió és enum címke névterületen! · Mint a tömböknél, struktúráknál is megadható nem teljes típusdeklaráció. Például a
struct datum;

még akkor is létrehozza az aktuális hatáskörben a struct datum nem teljes típust, ha ilyen befoglaló, vagy küls hatáskörben is létezne. Könnyen belátható, hogy ez struct datum típusú struktúra objektum definíciójára nem használható a struktúra szerkezetének közbens, ugyanezen hatáskörbeli definiálása nélkül, hisz ismeretlen a memóriaigény. Arra azonban ez is alkalmas, hogy deklarációban, typedef­ben használjuk a típusnevet, vagy hogy struct datum típusú objektumokra mutató mutatókat hozzunk létre:
struct datum *dptr1, *dptr2, *dptrt[20];

Az ilyen mutatók akár más struktúra tagjai is lehetnek:
struct A; /* struct B{ /* struct A *pa;}; /* struct A{ struct B *pb;}; /* Nem teljes típusdeklaráció. */ Itt tag a nem tejes típusra */ mutató mutató. */ Ez most már teljes típus lesz.*/

Vigyázat! Struktúradefinícióban a struktúra típusa a struktúra-tagdeklarációlistában csak akkor válik teljessé, ha elérjük a specifikátor bezáró kapcsos zárójelét (}). · A struktúradeklaráció általános alakjából látható volt, hogy belle a struktúracímke is elhagyható. Ha ezt megtesszük, ún. név nélküli, vagy címkézetlen struktúrához jutunk. Világos, hogy ebben az esetben a nem teljes típusdeklarációnak
struct;

semmi értelme (szintaktikai hiba is) sincs, de az olyan deklarációnak sincs, amiben csak a struktúra szerkezetét adjuk meg:
struct {int tag1, tag2; /* ... */};

hiszen késbb nem tudunk a típusra hivatkozni, s ebbl következleg ilyen típusú objektumokat deklarálni. Név nélküli struktúradeklarációban nem hagyható el tehát az azonosítólista, azaz:
struct {int tag1, tag2; /* ... */} az1, az2[14];

222

STRUKTÚRÁK ÉS UNIÓK

10.1.1 Típusdefiníció · Az elbb vázolt probléma típusdefiníció alkalmazásával áthidalható:
typedef struct{ int tag1, tag2; /* ... */} CIMKETLEN; /* Most sincs címke. */ CIMKETLEN y, *y, ytomb[12];

· A típusdefiníció a címkézett struktúrával is használható lett volna:
typedef struct datum{ int ev, ho, nap, evnap; long datumssz; /* Dátumsorszám. */ char datumlanc[11]; } DATUM; /* . . . */ DATUM d, *dptr, dt[10];

Összesítve: A typedef címke nélküli struktúrák, uniók és enum-ok típusdefiníciójára is alkalmas. Struktúrák esetében használjunk azonban struktúracímkét, vagy typedef-es szerkezetet, de a kettt együtt nem javasoljuk! · Egy kicsit összetettebb példát véve:
typedef char nev[30]; typedef enum{no, ferfi, egyeb} sex; typedef struct{ nev csalad, kereszt; /* Két 30 elem karaktertömb.*/ sex fino; /* no vagy ferfi érték enum.*/ /* . . . */ double osztondij; } hallgato; typedef hallgato evf[100];/* 100 elem, fenti szerkezet struktúratömb. */ evf evf1, evf2, evf3; /* Három darab,100 elem, fenti szerkezet struktúratömb. */

10.2 Struktúratag deklarációk A { }-ben álló struktúratag-deklarációlista a deklarátor szintaktikát követve meghatározza a struktúratagok neveit és típusait. · A struktúratag bármilyen típusú lehet a void, a nem teljes, vagy a függvény típustól eltekintve. A struktúratag deklaráció nem tartalmazhat azonban tárolási osztály specifikátort vagy inicializátort. Struktúratag nem lehet az éppen definíció alatt álló struktúra sem:
struct szoszlo{ static char *szo; int szlo=0; /* HIBÁS */ /* HIBÁS */

C programnyelv
struct szoszlo elozo, kovetkezo; }; /* HIBÁS */

223

· Struktúratag lehet azonban nem teljes típusú struktúrára, így akár az éppen deklaráció alatt állóra mutató mutató:
struct szoszlo{ char *szo; int szlo; struct szoszlo *elozo, *kovetkezo; };

/* OK */

· Struktúratag lehet tömb, st már definiált szerkezet struktúra is:
struct sor_lanc{ int sorszam; char megjegyzes[32]; struct sor_lanc *kovetkezo; }; struct kereszthivatkozas{ char *szo; int szlo; struct kereszthivatkozas *elozo, *kovetkezo; struct sor_lanc elso; };

· A struktúrának nem lehet függvény tagja, de függvényre mutató mutató persze lehet tag:
struct pelda{ char *szoveg; int (*hasonlit)(const char *, const char *);};

· A struktúratag azonosítójának egy struktúrán belül kell egyedinek lennie, vagyis másik struktúrában nyugodtan létezhet ugyanilyen nev tag. · A beágyazott struktúra ugyanúgy elérhet, mint a fájl hatáskörben deklarált, azaz a következ példa helyes:
struct a{ int x; struct b{ int y; } v2; } v1; /* . . . */ struct a v3; struct b v4;

· A beágyazott struktúra gyakran névtelen:
struct struki{ struct { int x, y; } pont; int tipus; } v;

224

STRUKTÚRÁK ÉS UNIÓK

10.3 Struktúrák inicializálása A struktúrát konstans kifejezésekbl álló inicializátorlistával láthatjuk el kezd értékkel. Az inicializátorlista elemek értékét a struktúratagok a deklarációbeli elhelyezkedés sorrendjében veszik fel:
struct struki { int i; char lanc[25]; double d; } s = {20, "Jancsika", 3.14};

Pontosítsunk még néhány dolgot! · Lokális élettartamú struktúrák esetén az inicializátor inicializátorlista, vagy kompatibilis struktúra típusú egyszer kifejezés lehet:
struct struki s = {20, "Juliska", 3.14}, s1 = s;

· Lokális (auto) struktúra persze akár ilyen típusú struktúrát visszaadó függvény hívásával is inicializálható:
struct struki fv(int, char *, double); struct struki s2=fv(2, ,,Boszi", 1.4);

· Ha a struktúrának struktúra vagy tömb tagja is van, akkor azt egymásba ágyazott { }-kel lehet inicializálni.
struct struki { int i; long darab[3]; double d; } s = { 20, { 1l, 2l, 3l}, 3.14};

· Tudjuk, hogy az inicializátorlista elemeinek száma nem haladhatja meg az inicializálandó struktúratagok számát! Ha az inicializátorlista kevesebb elem, mint az inicializálandó objektumok száma, akkor a maradék struktúratagok a statikus élettartamú implicit kezdérték adás szabályai szerint tölti fel a fordító, azaz nullázza:
struct struki{ int cipomeret, /* magassag; /* char nev[26]; /* char cim[40]; /* s.cipomeret==42 */ s.magassag==180 */ az s.nev "Magas Lajos" */ s.cim üres karakterlánc ("") kezdérték. */ double fizetes;/* s.fizetes==0.0. */ } s = { 42, 180, "Magas Lajos"};

· Névtelen bitmez tag nem inicializálható! Ha az inicializátorlistában nincs beágyazott inicializátorlista, akkor az ott felsorolt értékek az alaggregátumok, s ket a deklaráció sorrendjében veszik fel az aggregátum elemei. Kapcsos zárójelek ugyanakkor akár

C programnyelv

225

az egyes inicializátorok köré is tehetk, de ha a fordítót nem kívánjuk "becsapni", akkor célszer ket az aggregátum szerkezetét pontosan követve használni!
typedef struct { int n1, n2, n3; } triplet; triplet nlist1[2][3] = { /* Helyes megoldás: */ {{11, 12, 13}, {4, 5, 6}, {7, 18, 9}},/* Els sor. */ {{1, 2, 3}, {14, 15, 16}, {7, 8, 9}} /* 2. sor. */ }; triplet nlist2[2][3] = { /* Hibás megoldás: */ {11, 12, 13}, {4, 5, 6}, {7, 18, 9}, /* Els sor. */ {1, 2, 3}, {14, 15, 16}, {7, 8, 9} /* 2. sor. */ };

A sizeof-ot struktúrákra alkalmazva mindig teljes méretet kapunk akár a típust adjuk meg operandusként, akár az ilyen típusú objektumot. Például:
#include struct st{ char *nev; /* A mutató mérete bájtban. */ short kor; /* + 2 bájt. */ double magassag; }; /* + 8 bájt. */ struct st St_Tomb[ ] = { {"Jancsika", 18, 165.4}, /* St_Tomb[0] */ {"Juliska", 116, 65.4}}; /* St_Tomb[1] */ int main(void){ printf("\nSt_Tomb elemeinek száma = %d\n", sizeof(St_Tomb)/sizeof(struct st); printf("\nSt_Tomb egy elemének mérete = %d\n", sizeof(St_Tomb[0])); return 0; }

10.4 Struktúratagok elérése A struktúra és az uniótagok eléréséhez ugyanazokat a tagelérés operátorokat alkalmazza a nyelv. A tagelérés operátort szelekciós operátornak, tagszelektornak is szokás nevezni. Prioritásuk magasabb az egyoperandusos mveletekénél, s közvetlenül a () és a [] után következik. Kétfajta tagelérés operátor van: · az egyik a közvetlen szelekciós operátor (.) és · a másik a közvetett (->). A közvetlen tagelérés operátor alakja:
utótag-kifejezés.azonosító

Az utótag-kifejezésnek struktúra típusúnak, s az azonosítónak e struktúra típus egy tagja nevének kell lennie. A konstrukció típusa az elért tag típusa, értéke az elért tag értéke, s balérték akkor és csak akkor, ha az utótagkifejezés az, és az azonosító nem tömb.

226

STRUKTÚRÁK ÉS UNIÓK

A közvetett tagelérés operátor formája:
utótag-kifejezés->azonosító

Az utótag-kifejezésnek struktúra típusra mutató mutatónak, s az azonosítónak e struktúra típus egy tagja nevének kell lennie. A konstrukció típusa és értéke az elért tag típusa és értéke. Balérték, ha az elért tag nem tömb. Feltéve, hogy s struct S típusú struktúra objektum, és sptr struct S típusú struktúrára mutató mutató, akkor ha t az struct S struktúrában deklarált, típus típusú tag, az
s.t

és az
sptr->t

kifejezések típusa típus, és mindkett a struct S struktúra t tagját éri el. A következk pedig szinonimák, ill. azt is mondhatjuk, hogy a ­> szelekció operátoros kifejezés a másik rövidítése:
sptr->t (*sptr).t

Az s.t és az sptr->t balértékek, feltéve, hogy t nem tömb típusú. Például:
struct S{ int t; char lanc[23]; double d; } s, *sptr = &s, Stomb[20]={ { 0, "nulla", 0.}, { 1, "egy", 1.}}; /* . . . */ s.t = 3; sptr->d = 4.56;

· Az Stomb 20 elem, struct S struktúrából álló struktúratömb, melynek els (Stomb[0]) és második (Stomb[1]) elemét kivéve nincs explicit kezdértéke, azaz Stomb[2], . . ., Stomb[19] { 0, "", 0,} érték. A következ példák a struktúratömb tagelérést szemléltetik:
Stomb[3].t=3; strcpy(Stomb[3].lanc, "három"); Stomb[3].d=3.3;

vagy:
sptr=Stomb+5; sptr->t=5; strcpy(sptr->lanc, "öt"); sptr->d=5.5; /* Stomb[5].t */ /* Stomb[5].lanc */ /* Stomb[5].d */

C programnyelv

227

· Ha struct B struktúrának van struct A struktúra típusú tagja, akkor az ilyen struct A tagokat csak a tagszelekciós operátorok kétszeri alkalmazásával lehet elérni:
struct A { int j; double x; }; struct B { int i; char *nev; struct A a; double d; } s, *sptr = &s; /* . . . */ s.i = 3; s.a.j = 2; sptr->d = 3.14; (sptr->a).x = 6.28;

· A szelekciós operátorok balról jobbra kötnek, tehát a következk teljesen azonosak:
(sptr->a).x sptr->a.x; (s.a).x s.a.x;

· Említettük már, hogy a tagszelektorok prioritása magasabb az egyoperandusos mveleteknél. Ezért a
++sptr->i ++(sptr->i)

, azaz a kifejezés struct B i tagját inkrementálja, s nem sptr­t. Ha mégis ezt szeretnénk (bár a konkrét példánál ennek semmi értelme sincs), akkor a ++sptr kifejezés részt zárójelbe kell tenni, azaz:
(++sptr)->i

A
++(sptr++)->i

ugyancsak s.i­t inkrementálja, de a kifejezés mellékhatásaként sptr is megn eggyel. Ha a zárójeleket elhagyjuk, persze akkor is idejutunk:
++sptr++->i ++(sptr++)->i

· Ugyanígy a nev címen lev karaktert éri el a
*sptr->nev

, hisz az indirekciót az elbbiekbl következleg az sptr­rel elért nev címen hajtja végre a fordító. Vegyük még a
*sptr++->nev++

228

STRUKTÚRÁK ÉS UNIÓK kifejezést, aminek ugyanaz az értéke, mint az elz kifejezésé, de mellékhatásként sptr és nev inkrementálása is megtörténik.

Ha már mindig hozzárendelési példákat hoztunk, akkor itt kell megemlítenünk, hogy struktúrákat csak akkor lehet egymáshoz rendelni, ha a forrás és a cél struktúra azonos típusú.
struct A { int i, j; double d; } struct B { int i, j; double d; } /* . . . */ a = a1; /* a = b; /* a.i = b.i; /* a.j = b.j; a.d = b.d; a, a1; b; OK, a hozzárendelés megy tagról-tagra. */ HIBÁS, mert eltér a két struktúra típusa.*/ Tagról-tagra persze most is megy a dolog. */

Vegyünk valamilyen példát a struktúratömbökre! Keressük meg a megadott, síkbeli pontok közt a két, egymástól legtávolabbit! A pontok száma csak futás közben dl el (n), de nem lehetnek többen egy fordítási idben változtatható értéknél (N). Az x koordináta bevitelekor üres sort megadva, a bemenet elbb is befejezhet, de legalább két pont elvárandó! Az input ellenrzend, s minden hibás érték helyett azonnal újat kell kérni. Az eredmény közlésekor meg kell jelentetni a két pont indexeit, koordinátáit és persze a távolságot is.
/* PELDA28.C: A két, egymástól legtávolabbi pont megkeresése. */ #include #include #include #include #define INP 28 /* Az input puffer mérete. */ #define N 128 /* Pontok maximális száma. */

A feladatot síkbeli pontot leíró struktúra segítségével fogjuk megoldani:
struct Pont{ /* A Pont struktúra. */ double x, y; }; int getline(char s[],int n){ /* . . . */ } int lebege(char s[]){ /* . . . */ }

A két függvény forrásszövege idemásolandó!
int main(void){ char sor[INP+1]; /* Input puffer. */

A struktúratömb definíciója:

C programnyelv

229

struct Pont p[N]; /* Struktúratömb. */ int n=0; /* Pontok száma. */ double max=-1., d; /* Pillanatnyi maximum és */ int i, j, tavi, tavj; /* segédváltozók. */ printf("A két egyméstól legtávolabbi pont a síkban.\n" "Adja meg a pontok koordinátapárjait rendre!\n" "Vége: üres sor az X koordináta megadásánál.\n\n"); for(n=0; n
Kitnen látszik, hogyan kell elérni a struktúra tömbelem tagjait!
if((d=sqrt((p[j].x-p[i].x)*(p[j].x-p[i].x)+ (p[j].y-p[i].y)*(p[j].y-p[i].y))) > max){ max=d; tavi=i; tavj=j; } printf( "A maximális távolságú két pont:\n" "P[%d]: (%10.1f, %10.1f) és\n" "P[%d]: (%10.1f, %10.1f),\n" "s a távolság: %15.2f\n", tavi, p[tavi].x, p[tavi].y, tavj, p[tavj].x, p[tavj].y, max); return(0); }

Megoldandó feladatok: Ha fokozni kívánja a feladatot, akkor · Dolgozzon térbeli pontokkal! · Rendezze a pontokat az origótól való távolságuk csökken sorrendjében, és jelentesse meg a pontokat és a távolságot fejléccel ellátva, táblázatosan és lapozhatóan! · Esetleg oldja meg, hogy ne lehessen kétszer ugyanazt a pontot megadni!

230

STRUKTÚRÁK ÉS UNIÓK

10.5 Struktúrák és függvények Említettük már, hogy a struktúra másolható, hozzárendelhet, elérhetk a tagjai, képezhet a címe, ill. tömb is elállítható belle, de függvény is visszaadhat struktúrát vagy erre mutató mutatót. Az fv1 visszaadott értéke struct struki struktúra.
struct struki fv1(void);

Az fv2 viszont struct struki struktúrára mutató mutatót szolgáltat.
struct struki *fv2(void);

A függvény paramétere is lehet struktúra e két módon. Az fv3 struct struki struktúrát fogad paraméterként.
void fv3(struct struki s);

Az fv4 viszont struct struki struktúrára mutató mutatót fogad.
void fv4(struct struki *sp);

A következ példa a ,,helytelen" gyakorlatot szemlélteti. A függvény paraméterei és visszaadott értéke egyaránt struktúra. Struktúra persze akármekkora is elképzelhet.
typedef struct{ char nev[20]; int az; long oszt; } STUDENT; STUDENT strurend(STUDENT a, STUDENT b){ return((a.az < b.az) ? a : b); } /* . . . */ STUDENT a, b, c; /* . . . */ c = strurend(a, b);

Amíg a strurend fut, hat darab STUDENT struktúra létezik: a, b, c, aztán a és b másolata és a függvény visszaadott értéke a veremben. Célszer tehát nem a struktúrát, hanem arra mutató mutatót átadni a függvénynek, ill. vissza is kapni tle, ha lehet, azaz:
STUDENT *strurnd(STUDENT *a, STUDENT *b){ return((a­>az < b->az) ? a : b); } /* . . . */ STUDENT *z; /* . . . */ z = strurnd(&a, &b);

Prototípus hatásköre ellenére a benne megadott struct hatásköre globális, azaz figyelmeztet üzenet nélkül nem hívhatjuk meg a következ függvényt:

C programnyelv
void fv(struct S *);

231

A probléma elhárításához deklarálni vagy definiálni kell a struktúrát prototípus elírása eltt:
struct S; /* . . . */ void fv(struct S *);

Készítsünk struktúrát és kezel függvénycsaládot dátumok manipulálására! A datum struktúrában nyilvántartjuk a dátum évét (ev), hónapját (ho), napját (nap), a Krisztus születése óta a dátumig eltelt napok számát: az ún. dátumsorszámot (datumssz), a dátum karakterlánc alakját (datumlanc) és azt, hogy a dátum az év hányadik napja (evnap). Az egészet úgy képzeljük el, hogy · vagy megadják a dátumot év, hó és nap alakban, és a DatumEHN függvénnyel meghatározzuk a struktúra összes többi adatát (a mainben d1 objektum így kap értéket), · vagy karakterlánc alakú dátumból a DatumKAR­ral állítjuk el a datum struktúra tagjainak értékeit (a fprogramban d2 e módon jut értékhez). · Mindkét struct datum objektumnak értéket adó rutin végül dátumellenrzést végez a Datume függvénnyel, s ezt a logikai értéket szolgáltatja. · A NapNev visszaadja a dátum héten belüli napjának nevét. Pontosabban a név karakterláncának címét. · A további rutinok mveleteket végeznek a dátum struktúrákkal. A DatumKul megállapítja két dátum különbségét, s szolgáltatja ezt a napszámot. A DatumMegEgy inkrementálja, és visszaadja a dátum objektumot. A DatumMegint pozitív, egész értéket ad hozzá. · A dátumfüggvények mind a MINDATUM és MAXDATUM közötti tartományban dolgoznak. · A main ki is próbálja az összes dátumfüggvényt. Figyeljük meg, hogy mindegyik függvény struct datum objektumra mutató paraméterként kapja meg a manipulált dátum struktúrá(ka)t! A DatumMegEgy és a DatumMegint visszatérési értéke struct datum struktúra.

232

STRUKTÚRÁK ÉS UNIÓK

Fedezzük fel, hogy a globális MAXDATUM, a hónapi napszámokat tartalmazó honap tömb, és a Datume függvény static tárolási osztálya miatt lokális a DATUM.C modulra! Nincs is prototípus a Datume­re a DATUM.H fejfájlban. Az értéküket nem változtató, de csak a rutin blokkjából elérend oszto változó, és a napnév karakterláncokra mutatókból álló hetnev mutatótömb statikus élettartamúak, de hatáskörük lokális. Vegyük még észre a DATUM.H­ban, hogy a _DATUMH makró egyetlen struct datum definíciót tesz lehetvé akkor is, ha a fordítási egységben többször kapcsolnák be a fejfájlt.
/* DATUM.H: Dátumok kezelése. */ #include #include #include #include #if !defined(_DATUMH) #define _DATUMH struct datum{ int ev, ho, nap, evnap; long datumssz; /* Dátumsorszám. */ char datumlanc[11]; }; #endif const char *NapNev(struct datum *); int DatumEHN(int, int, int, struct datum *); int DatumKAR(const char *, struct datum *); long DatumKul(struct datum *, struct datum *); struct datum DatumMegEgy(struct datum *); struct datum DatumMegint(struct datum *, int); /* DATUM.C: Dátumok kezelése. */ #include "DATUM.H" #define MINDATUM 366 static const long MAXDATUM=9999*365l + 9999/4 ­ 9999/100+9999/400; static int honap[]={0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; const char *NapNev(struct datum *pd){ static char *hetnev[]={"vasárnap", "hétf", "kedd", "szerda", "csütörtök","péntek","szombat"}; return hetnev[(pd->datumssz)%7l]; }

A dátumsorszámból 7­tel képzett modulus alapján állapítja meg a héten belüli napindexet a NapNev rutin.
static int Datume(struct datum *pd){ int i; honap[2]=28+(!(pd->ev%4)&&pd->ev%100||(!pd->ev%400)); if(pd->ev<1 || pd->ev>9999 || pd->ho<1 || pd->ho>12 || pd->nap<1 || pd->nap>honap[pd->ho]){

C programnyelv
pd->evnap=0; pd->datumssz=0l; return 0;} else { sprintf(pd->datumlanc, "%04d.%02d.%02d", pd->ev, pd->ho, pd->nap); pd->evnap=pd->nap; for(i=1; iho; ++i)pd->evnap+=honap[i]; pd->datumssz=(pd->ev-1)*365l+pd->evnap+ pd->ev/4-pd->ev/100+pd->ev/400; return 1; } }

233

A Datume ugyanúgy a dátumot ellenrzi, s logikai választ ad a ,,formálisan jó­e a dátum?" kérdésre, mint a korábbi datume függvények. Nem karakterláncból dolgozik azonban, hanem a struktúra ev, ho, nap tagjaiból, melyeket a DatumEHN, ill. a DatumKAR készítettek oda. Ha hibás a dátum, akkor nullázza a rutin az evnap és a datumssz tagokat. Ha jó a dátum, akkor a Datume képezi a datumlanc­ba a dátum karakterlánc alakját, s meghatározza az evnap és a datumssz értékét. Az evnap a dátum napszámáról indul, s a rutin hozzáadogatja a megelz hónapok maximális napszámait. A dátumsorszám megállapításához a szökév vizsgálatához használatos kifejezést alkalmazza a függvény. Az STDIO.H bekapcsolásával rendelkezésre álló sprintf ugyanúgy mködik, mint printf társa, de nem a szabvány kimenetre, hanem az els paramétereként kapott karaktertömbbe dolgozik. A formátumspecifikációkban a mezszélesség eltt álló 0 hatására a jobbra igazított számok balról nem szóköz, hanem '0' feltöltést kapnak. Magyarán a 932.2.3. dátumból 0900.02.03 karakterlánc lesz. Szóltunk már róla, hogy a Datume nem hívható más forrásmodulból. A DATUM.C­ben is csak a DatumEHN és a DatumKAR idézi meg utolsó lépéseként.
int DatumEHN(int e, int h, int n, struct datum *pd){ pd->ev=e; pd->ho=h; pd->nap=n; if(e>=0&&h>=0&&n>=0&&e<10000&&h<100&&n<100) sprintf(pd->datumlanc, "%04d.%02d.%02d", e, h, n); else *pd->datumlanc=0; return Datume(pd);} int DatumKAR(const char *lanc, struct datum *pd){ pd->ho=pd->nap=0; strncpy(pd->datumlanc, lanc, 10); pd->datumlanc[10] = 0; pd->ev=atoi(lanc); while(isdigit(*lanc))++lanc; if(*lanc!=0){

234

STRUKTÚRÁK ÉS UNIÓK

++lanc; pd->ho=atoi(lanc); while(isdigit(*lanc))++lanc; if(*lanc){ ++lanc; pd->nap=atoi(lanc);}} return Datume(pd); }

A DatumEHN a kapott, int típusú év, hó, nap segítségével tölti fel az utolsó paraméterként elért dátum struktúrát. Az sprintf hívás eltti vizsgálatra azért van szükség, hogy a rutin ne tudja túlírni a 11 elem karaktertömb, datumlanc tagot a memóriában valamilyen egészen ,,zöldség" év, hó, nap paraméter miatt. Ilyenkor üres lesz a datumlanc. A DatumKAR átmásolja a karakterlánc alakban kapott dátum els 10 karakterét a datumlanc­ba. Nullázza a honapot és napot. Látszik, hogy a függvény nem köti meg olyan szigorúan sem az év, sem a hónap és nap jegyszámát, mint a korábbi datume, ill. elválasztó karakterként csak valamilyen nem numerikust vár el. A karakterlánc egésszé konvertált elejét évnek, az els elválasztó karakter utáni részt hónapnak, s a második elválasztó karakter mögöttieket napnak tekinti a rutin, hacsak idközben vége nem lesz a karakterláncnak. Végül mindkét függvény meghívja a Datume­t, s ennek visszatérési értékét szolgáltatja.
long DatumKul(struct datum *pd1, struct datum *pd2){ if(pd1->datumssz>pd2->datumssz) return pd1->datumssz-pd2->datumssz; else return pd2->datumssz-pd1->datumssz; }

A DatumKul képzi a két paraméter dátum struktúra dátumsorszám tagjainak különbsége abszolút értékét.
struct datum DatumMegEgy(struct datum *pd){ int e=pd->ev, h=pd->ho, n=pd->nap; struct datum d; honap[2]=28+(e%4==0 && e%100 || e%400==0); if(++n>honap[h]){ n=1; ++h; if(h>12){ h=1; ++e; }} if(!DatumEHN(e, h, n, &d)) d=*pd; return d; }

C programnyelv

235

A paramétere dátumot inkrementáló DatumMegEgy munkaváltozókba rakja az évet, a hónapot és a napot. Meghatározza az év szerinti február pontos napszámát. Növeli eggyel a napot. Ha ez túlmenne a hónap szerinti maximális napszámon, akkor 1 lesz, és a hónapszám növelése jön. Ha ez 13 lenne, akkor 1 lesz, és az évszám növelése következik. A megállapított, új év, hó, nap alapján a DatumEHN feltölti a lokális, d dátum objektumot. Ha az új dátum érvénytelen volt, akkor a változatlanságot jelzend a rutin hozzárendeli d­hez a paraméter címen lev, eredeti dátumot. A hozzárendelés a paraméter címen lev (ezért kell elé az indirekció) struct datum minden tagját egy az egyben átmásolja a balérték, d, lokális dátum objektum tagjaiba rendre. A visszatérés során a DatumMegEgy létrehoz a veremben egy ideiglenes dátum objektumot, melybe tagról­tagra bemásolja a d lokális dátum változót. Visszatérés után aztán a main hozzárendeli az ideiglenes dátum objektumot a main­ben lokális d­hez.
struct datum DatumMegint(struct datum *pd, int np){ int e=pd->ev, h=pd->ho, n=pd->nap; long dpd = pd->datumssz + np + 365; /* A tiszta jó konstans 365.24223 lenne kézi számítás szerint!!!! */ static double oszto=365.24225; struct datum d=*pd; if(np <= 0) return d; if(dpd > MAXDATUM){ e=9999; h=12; n=31; } else { e= (int)dpd/oszto; n=dpd-e*365l-e/4+e/100-e/400; honap[2]=28+(e%4==0 && e%100 || e%400==0); for(h=1; n > honap[h]; ++h)n-=honap[h]; } if(!DatumEHN(e, h, n, &d)) d=*pd; return d; }

Csak a kezdetét és a végét tekintve a pozitív napszámot a dátumhoz adó DatumMegint a DatumMegEgy­gyel megegyezen dolgozik. Látszik, hogy negatív, hozzáadandó napszámot, vagy a mvelet végén érvénytelen dátumot kapva, az eredeti dátum objektumot szolgáltatja a rutin változatlanul. A dátumsorszámot megnöveli a napszámmal és még 365­tel. Ha így meghaladná a 9999.12.31­et, akkor ezt adná vissza. Ha nem, akkor az új

236

STRUKTÚRÁK ÉS UNIÓK

dátumsorszámot elosztja a tapasztalati alapon a [MINDATUM, MAXDATUM] tartományban érvényes évenkénti átlagos napszámmal, s ez lesz az új évszám. Visszaszámolja belle az új év pontos napszámát, s a két érték különbségébl hónap és napszámot képez.
/* PELDA29.C: A dátumok kezelésének kipróbálása. */ #include "DATUM.H" void main(void) { long kul; struct datum d1, d2, d; printf("Dátum mveletek:\n\n"); DatumEHN(2003, 12, 31, &d1); DatumKAR("2003-2-13", &d2); printf("A(z) %s. és a(z) %s. különbsége %ld nap!\n", d1.datumlanc, d2.datumlanc, (kul=DatumKul(&d1, &d2))); d=DatumMegEgy(&d1); printf("A(z) %s. + 1 a(z) %s.\n", d1.datumlanc, d.datumlanc); d=DatumMegint(&d2, (int)kul); printf("A(z) %s. + %ld a(z) %s.\n", d2.datumlanc, kul, d.datumlanc); printf("A(z) %s. %s.\n", d2.datumlanc, NapNev(&d2)); }

Megoldandó feladatok: Bvítse a DATUM.H és DATUM.C fájlokat a következ funkciókat ellátó függvényekkel! Persze próbálja is ki ket! · A hónapnév karakterlánc elállítása a hónapszám alapján. · Olyan karakterlánc alakú dátum létrehozása, melyben a hónap megnevezése szerepel a hónap száma helyett. · A DatumMegint olyan átírása, hogy a napszám paraméter negatív is lehessen. Készítsen ugyanilyen szellemben struktúrát és kezel függvénycsaládot az idre is! 10.6 Önhivatkozó struktúrák és dinamikus adatszerkezetek Tudjuk, hogy a struktúrának nem lehet · void, · nem teljes és · függvény típusú tagja, de nem lehet tag

C programnyelv · az éppen definíció alatt álló struktúra

237

sem. Lehet viszont tag nem teljes típusú struktúrára, így akár a definíció alatt állóra, mutató mutató. Azt a struktúrát, melynek legalább egy önmagára mutató tagja van, önhivatkozó struktúrának nevezik. A dinamikus adatszerkezeteket [3]: listákat, fákat stb. leíró adatkonstrukciók a C­ben önhivatkozó struktúrák. Nézzünk náhányat! Egyirányú listához például a követekez struktúra lenne használható:
struct List1{ ADAT adat; struct List1 *kov; };

, ahol az ADAT típusú adat tagon valamilyen, a lista egy elemében tárolandó adatokat leíró struktúrát kell érteni. A kov az egyirányú lista következ struct List1 típusú elemére mutat, ill. a lista végét NULL mutató jelzi. A lista kezelhetségéhez ezen túl már csak egy horgonypontra, és esetleg egy seged mutatóra
struct List1 *KezdoPont = NULL, *seged;

van szükség a listát manipuláló programban. Tegyük fel, hogy ismertek az ADATok, s vegyük fel a lista els elemét!
if(!(KezdoPont=seged=(struct List1*)malloc( sizeof(struct List1)))){ printf("Elfogyott a memória!\n"); exit(1); } else{ /* seged->adat vegye fel az ADATok értékét! */ seged->kov=NULL; }

A lista következ eleme:
if(!(seged->kov=(struct List1*)malloc( sizeof(struct List1)))){ printf("Elfogyott a memória!\n"); exit(1); } else{ /* seged->adat vegye fel az ADATok értékét! */ seged->kov=NULL; } /* . . . */

Elég! Írjunk Beszur1 függvényt, mely paraméterként megkapja a beszúrandó ADATokat, s annak a listaelemnek a címét, mely utánra az új listaelem kell, hogy kerüljön! Ha még nincs is lista, akkor ezen a pozíción

238

STRUKTÚRÁK ÉS UNIÓK

kapjon NULL mutatót a rutin! A visszatérési érték legyen a most létesített listaelem címe, ill. NULL mutató, ha elfogyott a memória!
struct List1 *BeSzur1(ADAT a, struct List1 *elozo){ struct List1 *p; if(p=(struct List1 *)malloc(sizeof(struct List1))){ p->adat=a; if(elozo){ p->kov=elozo->kov; elozo->kov=p; } else p->kov=NULL; } return p; }

Ekkor a lista létrehozása a következ:
KezdoPont=seged=BeSzur1(adatok, NULL); while(/* Vannak következ adatok? */&&seged!=NULL) seged=BeSzur1(adatok, seged); if(!seged){ printf("Elfogyott a memória!\n"); exit(1); }

A létrehozott egyirányú lista felhasználás után a következ kóddal semmisíthet meg:
while(KezdoPont){ seged=KezdoPont->kov; free(KezdoPont); KezdoPont=seged; }

Az egyirányú listában lehet új elemet bárhová beszúrni (BeSzur1), bárhonnét törölni, de a listát ­ ahogyan a megsemmisít kód is mutatja ­ csak elrehaladva lehet elérni, visszafelé lépkedve nem. Az oda­ visszahaladáshoz kétirányú lista kell:
struct List2{ ADAT adat; struct List2 *kov, *elo; }; struct List2 *KezdoPont = NULL, *seged;

A visszalépegetés lehetségét a megelz listaelemre mutató, elo mutatótag biztosítja. A lista végét mindkét irányban NULL mutató jelzi. A beszúrás:
struct List2 *BeSzur2(ADAT a, struct List2 *elozo){ struct List2 *p; if(p=(struct List2 *)malloc(sizeof(struct List2))){ p->adat=a; if(elozo){ p->elo=elozo; p->kov=elozo->kov; elozo->kov=p;

C programnyelv
if(p->kov) p->kov->elo=p; } else p->elo=p->kov=NULL; } return p; }

239

A BeSzur2 csak egyet nem tud: a létez els elem elé beszúrni. Ezen így segíthetünk:
seged=BeSzur2(adatok, NULL); seged->kov=KezdoPont; KezdoPont=seged;

A törlés:
struct List2 *Torol2(struct List2 *ezt){ struct List2 *p=NULL; if(ezt){ p=ezt->kov; if(ezt->elo) ezt->elo->kov=ezt->kov; if(ezt->kov) ezt->kov->elo=ezt->elo; free(ezt); } return p; }

A Torol2 függvénynek is csak a létez, legels elem törlésekor kell segíteni, hisz változik a
KezdoPont=Torol2(KezdoPont);

Felhasználás után a kétirányú lista is ugyanúgy semmisíthet meg, mint az egyirányú. A fák közül válasszuk ki a bináris keresfát! Ennek pontjaiban legfeljebb kett az elágazások száma, és a pontokban helyet foglaló struktúrák ADAT része alapján a fa, mondjuk, növekvleg rendezett. Létezik tehát egy
int Hasonlit(ADAT a1, ADAT a2);

rutin, mely zérust szolgáltat, ha a1==a2, pozitív értéket, ha a1>a2, ill. negatívat egyébként. A bináris keresfában a mindenkori aktuális pont bal ágán lev pontok közül egy sem nagyobb, s a jobb ágán helyet foglalók közül viszont egyik sem kisebb az aktuális pontnál. Az adatstruktúra:
struct BinFa{ ADAT adat; struct BinFa *bal, *jobb; }; struct BinFa *KezdoPont = NULL;

A beszúrást végz rekurzív függvény a következ:
struct BinFa *BeSzurBF(ADAT a, struct BinFa *p){ int fel; if(!p){ /* Új pont készül. */ if(p=(struct BinFa *)malloc(sizeof(struct BinFa))){

240

STRUKTÚRÁK ÉS UNIÓK

p->adat=a; p->bal=p->jobb=NULL; } else printf("Elfogyott a memória!\n"); } else if((fel=Hasonlit(a, p->adat))==0) /* Volt már ilyen ADAT, s itt ez a kód van. */ else if(fel<0) /* A bal oldali részfába való. */ p->bal=BeSzurBF(a, p->bal); else /* A jobb oldali részfába való. */ p->jobb=BeSzurBF(a, p->jobb); return(p); }

A következ rekurzív függvény növekvleg rendezett sorrendben végez el minden ponton valamilyen tevékenységet:
void Tevekeny(struct BinFa *p){ if(p){ Tevekeny(p->bal); /* Itt van a tevékenység kódja. */ Tevekeny(p->jobb); } }

Keressük meg egy szövegfájlban a benne elforduló szavakat! Állapítsuk meg ezen kívül, hogy a szavak a szövegfájl mely sorszámú soraiban fordulnak el! Ha ugyanaz a szó egy sorban többször is megtalálható, a sor sorszámát ekkor is csak egyszer kell közölni. Végül közlendk a szavak, és az elfordulási sor sorszámok névsorban! A megoldásban a szavak tárolásához bináris keresfát használunk, s a szóhoz tartozó elfordulási sor sorszámokat minden ponthoz tartozóan egyirányú listában tartjuk nyilván. A PELDA30 programot a parancssorból
PELDA30 < PELDA30.C > EREDMENY.TXT

módon indíthatjuk, s a készült lista az EREDMENY.TXT fájlban tanulmányozható.
/* PELDA30.C: Kiírja a szövegben elforduló szavak listáját és megadja azt is, hogy a szavak milyen sorszámú sorokban fordultak el! */ #include #include #include #include #define MAXSOR 256 /* A beolvasott sor max. hossza. */ /* A sorok sorszámainak egyirányú listája: */ struct List1{ int sorsz; struct List1 *kov; }; /* A bináris keres fa: */ struct BinFa{ /* Alapcsomópont. */

C programnyelv
char *szo; /* A szóra mutat. */ struct List1 *sorok; /* A sorok sorszámai. */ struct BinFa *bal; /* A bal oldali ág. */ struct BinFa *jobb;}; /* A jobb oldali ág. */ /* Függvény prototípusok: */ struct BinFa *BeSzurBF(char *, int, struct BinFa *); void SorMeg(struct BinFa *, int); void Kiir(struct BinFa *); int main(void){ static char szoelv[]=" \t\n\r\f\"\'\\\a\?" ":;,.()[]{}*/%+-&|^~!<>=#"; struct BinFa *KezdoPont=NULL; char sor[MAXSOR], *szo; int sorszam=1; printf("Szavak keresztreferenciája szövegben:\n"); while(sor[MAXSOR-1]=2, fgets(sor, MAXSOR, stdin)){ if(!sor[MAXSOR-1]&&sor[MAXSOR-2]!='\n') printf("Lehet kis elsorszámozás!\n"); szo=strtok(sor, szoelv); while(szo){ KezdoPont=BeSzurBF(szo, sorszam, KezdoPont); szo=strtok(NULL, szoelv); } ++sorszam; } Kiir(KezdoPont); return 0; }

241

A fgets függvény a getline­hoz nagyon hasonlóan dolgozik. Karakterláncot olvas be a szabvány bemenetrl (stdin), melyet az els paraméter címtl kezdve letárol a memóriában. Az olvasás leáll, ha a függvény a második paraméterénél eggyel kevesebb (MAXSOR ­ 1), vagy '\n' karaktert olvasott. A getline­tól eltéren azonban a rutin a '\n'­t is elhelyezi a láncban, és a lánc végéhez még egy '\0'­t is hozzáilleszt. Sikeres esetben az fgets az els paraméter mutatóval tér vissza. Fájlvégen, vagy hiba esetén viszont NULL­t kapunk tle. Ha a sor tömb utolsó pozícióján van a lánczáró zérus, de eltte nincs ott a soremelés karakter, akkor az aktuálisan olvasott sor nem fért el egy menetben a bemeneti pufferben, s ebbl következleg elsorszámozás történik. Az strtok leírása megtalálható a MUTATÓK szakasz Karakterlánc kezel függvények fejezetében!
struct BinFa *BeSzurBF(char *a,int sorszam,struct BinFa *p) { int fel; if(!p){ /* Új szó érkezett */ p=(struct BinFa *)malloc(sizeof(struct BinFa)); if(p&&(p->szo=(char *)malloc(strlen(a)+1))){ strcpy(p->szo, a);

242

STRUKTÚRÁK ÉS UNIÓK

if(p->sorok=(struct List1 *)malloc( sizeof(struct List1))){ p->sorok->sorsz=sorszam; p->sorok->kov=NULL; } p->bal=p->jobb=NULL; } if(!p||!p->szo||!p->sorok){ printf("Elfogyott a memória!\n"); exit(1); } } else if((fel=strcmp(a, p->szo))==0) SorMeg(p, sorszam); else if(fel<0) p->bal=BeSzurBF(a, sorszam, p->bal); else p->jobb=BeSzurBF(a, sorszam, p->jobb); return(p); } /* Sorszám hozzáadása az egyirányú lista végéhez: */ void SorMeg(struct BinFa *p, int sorszam){ struct List1 *seged=p->sorok; while(seged->kov!=NULL && seged->sorsz!=sorszam) seged=seged->kov; if(seged->sorsz!=sorszam){ if(seged->kov=(struct List1 *)malloc( sizeof(struct List1))){ seged->kov->sorsz=sorszam; seged->kov->kov=NULL; } else{ printf("Elfogyott a memória!\n"); exit(1); } } } /* A fa kiírása: */ void Kiir(struct BinFa *p){ struct List1 *seged; int i; if(p){ Kiir(p->bal); printf("%s:\n", p->szo); for(seged=p->sorok, i=0; seged; seged=seged->kov, ++i) printf("%7d|", seged->sorsz); if(i%10!=9) printf("\n"); Kiir(p->jobb); } }

Megoldandó feladatok: Fejlessze tovább a PELDA30.C­ben megoldott feladatot a következképp, s persze próbálja is ki! · ­C parancssori paraméterrel indítva a program ne gyjtse a szabványos C kulcsszavak elfordulásait. · Ha a szabvány kimenet (stdout) nem fájl, akkor bontsa lapokra a listát a szoftver.

C programnyelv

243

· ­N parancssori paraméterrel startolva jelentesse meg a program megsorszámozva, de egyébként változatlanul a bemenetet. 10.7 Struktúra tárillesztése A fordító a struktúratagokat deklarációjuk sorrendjében növekv memória címeken helyezi el. Minden adatobjektum rendelkezik tárillesztési igénnyel is. A fordító olyan eltolással helyezi el az adatobjektumot, hogy az
eltolás % tárillesztési-igény == 0

zérus legyen. Struktúrák esetén ez a szabály a tagok elhelyezésére vonatkozik. Ha példának vesszük a
struct struki { int i; char lanc[3]; double d; } s;

struktúrát, akkor tudjuk, hogy az s objektumot növekv memória címeken úgy helyezi el a fordító, hogy 1. négy bájtot (32 bites esetben) foglal az int tagnak, 2. aztán a 3 bájtos karakterlánc következik, és 3. végül 8 bájtot rezervál a double taghoz. Bizonyos fordító opciók, vagy valamilyen #pragma direktíva segítségével vezérelhetjük a struktúra adatok memóriabeli illeszkedését. Ez azt jelenti, hogy az adatokat 1­gyel, 2­vel, 4­gyel stb. maradék nélkül osztható címeken: bájthatáron, szóhatáron, dupla szóhatáron stb. kell elhelyezni. Felkérjük az olvasót, hogy nézzen utána a dolognak a programfejleszt rendszere segítségében! Bárhogyan is, eme beállítások hatására a fordító minden struktúratagot, az elst követen, olyan határon tárol, mely megfelel a tag tárillesztési igényének. A bájthatárra igazítás azt jelenti, hogy · a struktúra objektum elhelyezése bármilyen címen kezddhet, és · a struktúratagok ugyancsak bármilyen címen elhelyezhetk típusuktól függetlenül. A példa s objektum összesen 15 bájtot foglal el ilyenkor, és a memória térkép a következ:

244 i 4 bájt lanc 3 bájt d 8 bájt

STRUKTÚRÁK ÉS UNIÓK

A szóhatárra igazítás azt jelenti, hogy · a struktúra objektum kezdcíme páros kell, hogy legyen, és · a struktúratagok - a char típustól eltekintve - ugyancsak páros címeken helyezkednek el. A példa s objektum így összesen 16 bájtot foglal el. Egy bájt elveszik, és a memória térkép a következ: i 4 bájt lanc 3 bájt 1 b d 8 bájt

A dupla szóhatárra igazítás azt jelenti, hogy · a struktúra objektum elhelyezése néggyel maradék nélkül osztható címen (dupla szóhatáron) történik meg, · a char típusú struktúratagok bájthatáron kezddnek, · a short típusú tagok szóhatáron indulnak és · a többi típusú tag dupla szóhatáron (néggyel maradék nélkül osztható címen) kezddik. Dupla szóhatárra igazítva az s objektum megegyezik az elzvel. A határra igazítási ,,játék" folytatható értelemszeren tovább. Persze a struktúrát nem ilyen ,,bután" definiálva igazítástól függetlenül elérhetjük, hogy egyetlen bájt elvesztése se következzék be:
struct struki { double d; int i; char lanc[3]; } s;

10.8 UNIÓK Az unió típus a struktúrából származik, de a tagok között zérus a címeltolás. Az unió azt biztosítja, hogy ugyanazon a memória területen több, különféle típusú adatot tárolhassunk. Az
union unio{ int i; double d; char t[5]; } u, *pu = &u, tu[23];

C programnyelv

245

definícióban az u union unio típusú objektum, a pu ilyen típusú objektumra mutató mutató, és a tu egy 23 ilyen típusú elembl álló tömb azonosítója. Az u objektum - például - egyazon memória területen biztosítja az i nev int, a d azonosítójú double és a t nev karaktertömb típusú tagjainak elhelyezését, azaz:
&u &u.i &u.d ...

Ennek szellemében aztán igaz, hogy az unió objektumra mutató mutató annak egyben minden tagjára is mutat.
(pu=&u) &u.i &u.d ...

Természetesen a dolog csak a mutatók értékére igaz, mert az &u (union unio *), az &u.i (int *) és az &u.d (double *), azaz típusban eltérnek. Ha azonban uniót megcímz mutatót explicit típusmódosítással tagjára irányuló mutatóvá alakítjuk, akkor az eredmény mutató magára a tagra mutat:
u.d=3.14; printf("*(&u.d) = %f\n", *(double *)pu);

Az unió helyfoglalása a tárban akkora, hogy benne a legnagyobb bájtigény tagja is elfér, azaz:
sizeof(union unio) sizeof(u) 8.

Tehát 4 bájt felhasználatlan, ha int adatot tartunk benne, ill. 3 bájt elérhetetlen, ha karaktertömböt rakunk bele. Az unió egy idben csak egyetlen tagját tartalmazhatja. Láttuk már, hogy az uniótagokat ugyanazokkal a tagszelektor operátorokkal érhetjük el, mint a struktúratagokat:
u.d = 3.15; printf("u.d=%f\n", u.d); /* OK: u.d=3.15 jelenik meg. */ printf("u.i=%d\n", u.i); /* Furcsa eredmény születik. */ printf("u.t[0]=%c\n", u.t[0]);/* Valami csak megjelenik, vagy sem. */ printf("u.t=%s\n", u.t); /* ,,Csoda" karakterlánc látszik. Ki tudja, hol van a lánc vége!*/ strcpy(pu->t, "Hohó"); printf("u.t=%s\n", pu->t);/* OK: a ,,Hohó" látszik. */ printf("u.i=%d\n", pu->i);/* Furcsa eredmény születik. */ printf("u.d=%f\n", pu->d);/* Nagyon szorítsunk, hogy ne legyen lebegpontos túl vagy alulcsordulás! */

246

STRUKTÚRÁK ÉS UNIÓK

Valahonnan tehát célszer tudni - például úgy, hogy nyilvántartjuk milyen típusú adat is található pillanatnyilag az unió objektumban, és azt szabad csak elérni. Ha egy unió többféle, de azonos kezd szerkezet struktúrával indul, és az unió tartalma e struktúrák egyike, akkor lehetség van az unió közös kezdeti részére hivatkozni. Például:
union{ struct{ int tipus;} t; struct{ int tipus; int iadat;} ti; struct{ int tipus; double dadat;} td; /* . . . */ } u; /* . . . */ u.td.tipus = DOUBLE; u.td.dadat = 3.14; /* . . . */ if(u.t.tipus == DOUBLE) printf("%f\n", u.td.dadat); else if(u.t.tipus == INT) printf("%d\n", u.ti.iadat); else /* . . . */

Uniókkal pontosan ugyanazok a mveletek végezhetk, mint a struktúrákkal. Hozzárendelhetk, másolhatók, hozzáférhetünk a tagjaikhoz, képezhet a címük, átadhatók függvényeknek és rutinok visszatérési értékei is lehetnek. 10.8.1 Uniódeklarációk Az általános deklarációs szabály azonos a struktúráéval. Az eltérések a következk: · Az uniók tartalmazhatnak bitmezket. Mindegyik bitmez azonban az unió kezdetétl indul, s így közülük csak egy lehet aktív. A következ fejezetben tárgyalt bitmezk gépfügg ábrázolására itt is fel szeretnénk hívni külön a figyelmet! · Az unió tagja nem lehet void, nem teljes, vagy függvény típusú. Nem lehet a definíció alatt álló unió egy példánya, de ilyenre mutató mutató persze lehet. · Uniók esetében a deklarációban csak az elsnek deklarált tagnak adható explicit kezdérték. Például:
union unika{ int i; double d; char t[6]; } u = { 24 };

C programnyelv

247

Csak az u.i kaphatott, és kapott is 24 kezdértéket. Ha kicsit bonyolultabb esetet nézünk:
union{ char x[2][3]; int i, j ,k; } y = {{{'1'}, {'4'}}};

Az y unió változó inicializálásakor aggregátum inicializátort használunk, mert az unió els tagja kétdimenziós tömb. Az '1' inicializátor a tömb els sorához tartozik, így az y.x[0][0] felveszi az '1' értéket, s a sor további elemei tiszta zérusok lesznek az implicit kezdérték adás szabályai szerint. A '4' a második sor els elemének inicializátora, azaz y.x[1][0] = '4', y.x[1][1] = 0 és y.x[1][2] = 0. · Lokális élettartamú uniók esetén az inicializátor kompatibilis unió típusú egyszer kifejezés is lehet:
union unika{ int i; double d; char t[6]; } u = { 24 }, u1 = u;

· Az uniódeklarációban is elhagyható az uniócímke. Az uniók elfordulhatnak struktúrákban, tömbökben, és tömbök, ill. struktúrák is lehetnek tagok uniókban:
#define MERET 20 struct { char *nev; int adat; int u_tipus; /* Az unióban aktuálisan tárolt */ union{ /* típus nyilvántartásához. */ int i; float f; char *mutato; } u; } tomb[MERET];

Ilyenkor a tomb i-edik eleme i uniótagjához való hozzáférés alakja:
tomb[i].u.i

és a mutato tag mutatta els karakter elérésének formája:
*tomb[i].u.mutato

10.9 Bitmezk (bit fields) Bitmezk csak struktúra vagy unió tagjaként definiálhatók, de a struktúrában és az unióban akár keverten is elfordulhatnak bitmez és nem bit-

248

STRUKTÚRÁK ÉS UNIÓK

mez tagok. A bitmez struktúratag deklarációs szintaktikája kicsit eltér a normál tagokétól:
típusspecifikátor : konstans-kifejezés;

, ahol a típusspecifikátor csak · signed int, · unsigned int vagy · int lehet az ANSI C szabvány szerint. Az int tulajdonképpen signed int. A deklarátor a bitmez azonosítója, mely el is maradhat. Ilyenkor a névtelen bitmez specifikálta bitekre nem tudunk hivatkozni, s a bitek futásidej tartalma elre megjósolhatatlan. A konstans-kifejezés csak egészérték lehet. Zérus és sizeof(int)*8 közöttinek kell lennie, s a bitmez szélességét határozza meg. Bitmez csak struktúra vagy unió tagjaként deklarálható. Nem képezhet azonban bitmezk tömbje. Függvénynek sem lehet visszaadott értéke a bitmez. Nem megengedett a bitmezre mutató mutató és tilos hivatkozni a bitmez tag címére, azaz nem alkalmazható rá a cím (&) operátor sem. A bitmezk az int területen (dupla szóban, vagy szóban) deklarációjuk sorrendjében az alacsonyabb helyiérték bitpozícióktól a magasabbak felé haladva foglalják el helyüket. Az int pontos mérete, bitmezvel való feltöltésének szabályai és sorrendje a programfejleszt rendszertl függ. Célszer tehát a segítségben utánanézni a dolognak. Maradjunk meg azonban az elz bekezdésben említett szabálynál, és a könnyebb szemléltethetség végett még azt is tételezzük fel, hogy az int 16 bites! Ilyenkor például a:
struct bitmezo{ int i: 2; unsigned j: 5; int : 4, k: 1; unsigned m: 4; } b, *pb = &b;

által elfoglalt szó bittérképe a következ: 15 14 13 12 11 10 9 m k 8 7 6 5 4 j 3 2 1 i 0

Ha az m bitmez tag szélessége 4-nél nagyobb lett volna, akkor új szót kezdett volna a fordító, s az elz szó fels négy bitje kihasználatlan ma-

C programnyelv

249

radt volna. Általánosságban: a (dupla)szón túllógó bitmez új (dupla)szót kezd, s az elz (dupla)szóban a fels bitek kihasználatlanok maradnak. Ha a deklarációban valamely (névtelen) bitmeznél zérus szélességet adunk meg, akkor mesterségesen kényszerítjük ki ezt a következ (dupla) szóhatárra állást. A bitmeznek elég szélesnek kell lennie ahhoz, hogy a rögzített bitminta elférjen benne! Például a következ tagdeklarációk illegálisak:
int alfa : 17; unsigned beta : 32

A bitmezk ugyanazokkal a tagszelektor operátorokkal (. és ->) érhetk el, mint a nem bitmez tagok:
b.i vagy pb->k

A bitmezk kis signed vagy unsigned egész értékekként viselkednek (rögtön átesnek az egész­elléptetésen), azaz kifejezésekben ott fordulhatnak el, ahol egyébként aritmetikai (egész) értékek lehetnek. signed esetben a legmagasabb helyiérték bit (MSB - most significant bit) eljelbitként viselkedik, azaz az int i : 2 lehetséges értékei például:
00: 0, 01: +1, 10: -2, 11: -1

Az unsigned m : 4 lehetséges értékei:
0000: 0, 0001: 1, . . ., 1111: 15

Általánosságban:
unsigned x : szélesség; /* 0 <= x <= 2szélesség-1 */ signed y : szélesség; /* -2szélesség-1<= y <=+2szélesség-1-1 */

A nyelvben nincs sem egész alul, sem túlcsordulás. Ha így a bitmeznek ábrázolási határain kívüli értéket adunk, akkor abból is lesz ,,valami". Méghozzá az érték annyi alsó bitje, mint amilyen széles a bitmez. Például:
b.i = 6; /* 110 10, azaz -2 lesz az értéke! */

A bitmezk ábrázolása gépfügg, mint már mondottuk, azaz portábilis programokban kerüljük el használatukat! Vegyük el ismét a Bit szint operátorok fejezetben tárgyalt dátum és idtárolási problémát! Hogyan tudnánk ugyanazt a feladatot bitmezkkel megoldani?

250 Dátum: év ­ 1980 hónap nap Bitpozíció: 9 - 15 5-8 0-4

STRUKTÚRÁK ÉS UNIÓK Id: óra perc két másodperc Bitpozíció: 11 ­ 15 5 ­ 10 0­4

A dátum és az id adatot egy-egy szóban, azaz C nyelvi fogalmakkal egy-egy unsigned short int-ben tartjuk. A két szó bitfelosztása az ábrán látható! A bitmezs megoldás például a következ is lehetne:
struct datum{ unsigned short nap: 5, ho: 4, ev: 7; } d = { 8, 3, 1996-1980 }; struct ido{ unsigned short mp2: 5, perc: 6, ora: 5; } i = { 2, 59, 11 }; /* . . . */ int ev=1996, ho=3, nap=8, ora=11, perc=59, mp=4; /* Részeibl a dátum és az id elállítása: */ d.ev = ev -1980; d.ho = ho; d.nap = nap; i.ora = ora; i.perc = perc; i.mp2 = mp >> 1; /* Ugyanez visszafelé: */ ev = d.ev +1980; ho = d.ho; nap = d.nap; ora = i.ora; perc = i.perc; mp = i.mp2 << 1;

10.10 Balérték ­ jobbérték Most már tökéletesen pontosíthatjuk a balérték kifejezést a C­ben, mely: · Egész, lebegpontos, mutató, struktúra vagy unió típusú azonosító. · Indexes kifejezés, mely nem tömbbé (hanem elemmé) értékelhet ki. · Tagszelektoros kifejezés (->, .). · Nem tömbre hivatkozó, indirekciós kifejezés. · Balérték kifejezés zárójelben.

C programnyelv

251

A const objektum nem módosítható balérték, hisz csak a deklarációban kaphat kezdértéket. A jobbérték (rvalue) olyan kiértékelhet kifejezés, melynek értékét balérték veheti fel. Például:
a = c + d; c + d = a; /* OK */ /* HIBÁS */

A balérték (lvalue) olyan kifejezés, mely eléri az objektumot (a hozzá allokált memória területet). Triviális például egy változó azonosítója. Lehet azonban *P alakú is, ahol a P kifejezést nem NULL mutatóra értékeli ki a fordító. Onnét is származtatható a két fogalom, hogy a balérték állhat a hozzárendelés operátor bal oldalán, s a jobbérték pedig a jobb oldalán. Beszélhetünk módosítható balértékrl is! Módosítható balérték nem lehet tömb típusú (a tömbazonosító praktikusan cím konstans), nem teljes típusú, vagy const típusmódosítóval ellátott objektum. Módosítható balérték például a konstans objektumra mutató mutató maga, miközben a mutatott konstans objektum nem változtatható. Például
int tomb[20];

esetén balértékek:
tomb[3] = 3; *(tomb+4) = 4;

A következ deklarációban viszont a kar nem balérték, hisz konstansságára való tekintettel értéket egyedül a definíciójában kaphat:
const char kar = 'k';

Azonban ha van egy
char *pozicio(int index);

függvény, akkor balérték lehet a következ is:
*pozicio(5) = 'z';

10.11 Névterületek A névterület az a ,,hatáskör", melyen belül egy azonosítónak egyedinek kell lennie, azaz más­más névterületen konfliktus nélkül használható ugyanaz az azonosító, s a fordító meg tudja különböztetni ket. A névterületeknek a következ fajtái vannak: · Utasítás címke névterület: Az utasítás címkéknek abban a függvényben kell egyedinek lenniük, amelyben definiálták ket. · Struktúra, unió és enum címke névterület: A struktúra, az unió és az enum címkék ugyanazon a névterületen osztoznak. Deklarálásuk

252

STRUKTÚRÁK ÉS UNIÓK blokkjában kell egyedinek bizonyulniuk. Ha minden függvény testén kívül adják meg ket, akkor viszont fájl hatáskörben kell egyedinek lenniük.

· Struktúra és uniótagok (member) névterülete: A tagneveknek abban a struktúrában vagy unióban kell egyedinek lenniük, amelyben deklarálták ket. Különböz struktúrákban és uniókban elfordulhatnak ugyanazon tagazonosítók akár más típussal, s eltolással. Összesítve: mindenegyes struktúra és unió külön névterülettel rendelkezik. · Normál azonosítók névterülete: Idetartozik minden más név, ami nem fért be az elz három névterületbe, azaz a változó, a függvény (beleértve a formális paramétereket, s a lokális változókat) és az enumerátorazonosítók. Abban a hatáskörben kell egyedinek bizonyulniuk, ahol definiálják ket. Például a fájl hatáskör azonosítóknak ugyanebben a hatáskörben kell egyedinek lenniük. · Típusdefiníció (typedef) nevek: Nem használhatók azonosítóként ugyanabban a hatáskörben. Magyarán a típusdefiníciós nevek a normál azonosítók névterületén vannak, de nem futásidej azonosítók! Tehát, ha a helyzetbl eldönthet, akkor lehet a típusdefiníciós név, és például egy lokális hatáskör változó azonosítója egyforma is:
typedef char FT; int fv(int lo){ int FT; /* Ez az FT egy int típusú lokális változó azonosítója. */ /* . . . */ }

Nézzünk néhány példát!
struct s{ int s; /* OK: a struktúratag újabb névterületen helyezkedik el. */ float s;/*HIBÁS: így már két azonos tagnév lenne egy struktúrán belül. */ } s; /* OK: a normál változók névterülete különbözik minden eddig használttól. */ union s{ /* HIBA: az s struktúracímke is ezen a névterületen van. */ int s; /* OK: hisz új tag névterület kezddött. */ float f;/*OK: más azonosítójú tag. */ } f; /* OK: hisz ez az f az normál változók névterületén található. */ struct t{ int s; /* OK: hiszen megint újabb tag névterület kezddött. */

C programnyelv
/* . . . */ } s; /* HIBA: s azonosító most már minden névterületen van. */ goto s; /* OK: az utasítás címke és a struktúracímke más-más névterületen vannak. */ /* . . . */ s: ; /* Utasítás címke. */

253

254

SZABVÁNY, MAGAS SZINT BEMENET, KIMENET

11 MAGAS SZINT BEMENET, KIMENET
A magas szint bemeneten és kimeneten olyan folyam, áram (stream) jelleg fájl, ill. eszköz (nyomtató, billentyzet, képerny stb.) kezelést értünk, ami a felhasználó szempontjából nézve szinte nincs tekintettel a mögöttes hardverre, s így a lehet legflexibilisebb kimenetet, bemenetet biztosítja. A valóságban a folyamot egy FILE típusú struktúrára mutató mutatóval manipuláljuk. Ezt a struktúrát, a folyamkezel függvények prototípusait stb. az STDIO.H fejfájlban definiálták. A struktúra például legyen a következ!
typedef struct{ short level; /* Puffer telítettségi szint. */ unsigned short flags; /* Fájl állapotjelzk. */ char fd; /* Fájl leíró. */ unsigned char hold; /* ungetc kar., ha nincs puffer. */ int bsize; /* A puffer mérete. */ unsigned char *buffer;/* A puffer címe. */ unsigned char *curp; /* Aktuális pozíció a pufferben. */ /* . . . */ } FILE;

A programunkban
FILE *fp;

deklarációs utasítással FILE típusú struktúrára mutató mutatót kell deklarálni, mely értéket a folyamot megnyitó fopen, freopen függvényektl kap. Tehát használat eltt a folyamot meg kell nyitni. Megnyitása a folyamot egy fájlhoz, vagy egy eszközhöz kapcsolja. Jelezni kell azt is ilyenkor, hogy a folyamot csak olvasásra, vagy írásra, vagy mind kettre kívánjuk használni stb. Ezután elvégezhetjük a kívánt bemenetet, kimenetet a folyamon, majd legvégül le kell zárni. 11.1 Folyamok megnyitása FILE *fopen(const char *fajlazonosito, const char *mod); A függvény megnyitja a fajlazonositoval megnevezett fájlt, és folyamot kapcsol hozzá. Visszaadja a fájlinformációt tartalmazó FILE struktúrára mutató mutatót, mely a rákövetkez mveletekben azonosítani fogja a folyamot, ill. NULL mutatót kapunk tle, ha a megnyitási kísérlet sikertelen volt. A fajlazonosito természetesen tartalmazhat (esetleg meghajtó nevet) utat is, de a maximális összhossza FILENAME_MAX karakter lehet.

C programnyelv

255

A második paraméter mod karakterlánc meghatározza a késbbi adatátvitel irányát, helyét és a folyam típusát. Nézzük a lehetségeket! r w a r+ a+ Megnyitás csak olvasásra. Létrehozás írásra. A már létez, ilyen azonosítójú fájl tartalma megsemmisül. Hozzáfzés: megnyitás írásra a fájl végén, vagy létrehozás írásra, ha a fájl eddig nem létezett. Egy létez fájl megnyitása felújításra (írásra és olvasásra). Megnyitás hozzáfzésre: a fájl végén felújításra, vagy új fájl létrehozása felújításra, ha a fájl eddig nem létezett.

w+ Új fájl létrehozása felújításra. A létez fájl tartalma elvész.

A folyam típusa szöveges (text), vagy bináris lehet. A szöveges folyam a bemenetet és a kimenetet sorokból állóknak képzeli el. A sorok végét egy '\n' (LF) karakter jelzi. Lemezre történ kimenet esetén a folyam a sorlezáró '\n' karaktert "\r\n" karakter párral (CR-LF) helyettesíti. Megfordítva: lemezes bemenetnél a CR-LF karakter párból ismét LF karakter lesz. Ezt a manipulációt transzlációnak nevezzük. Bemenet esetén a folyam a 0X1A érték karaktert fájlvégnek tekinti. Összegezve: a szöveges folyam bizonyos, kitüntetett karaktereket speciálisan kezel, míg a bináris folyam ilyent egyetlen karakterrel sem tesz. Elismerjük természetesen, hogy nincs transzláció mindenegyes operációs rendszerben. A mod karakterláncban expliciten megadhatjuk a folyam típusát. A szövegest a 't', a binárist a 'b' jelöli. A folyamtípus karakter a karakterláncban az els bet után bárhol elhelyezhet, azaz megengedettek az
rt+, r+t stb.

Nem kötelez azonban a folyamtípust a mod karakterláncban expliciten megadni. Ha elhagyjuk, alapértelmezés a szöveges. Ha a folyamot felújításra (update) nyitották meg, akkor megengedett mind a bemenet, mind a kimenet. A kimenetet azonban fflush, vagy pozícionáló (fseek, rewind stb.) függvény hívása nélkül nem követheti közvetlenül bemenet. A fordított adatirányváltás is csak fájlvégen, vagy e függvények hívásának közbeiktatásával valósítható meg.

256

SZABVÁNY, MAGAS SZINT BEMENET, KIMENET

11.2 Folyamok pufferezése A fájlokhoz kapcsolt folyamok szokásosan pufferezettek, s a puffer lefoglalása megnyitáskor automatikusan megtörténik malloc hívással. Ez is megengedi azonban az ,,egy karakteres szint" bemenetet, kimenetet (getc, putc), ami nagyon gyors. A pufferrel kapcsolatos információkat a FILE struktúra tagjai írják le: curp level buffer bsize , ahol buffer a puffer kezdcíme és bsize a mérete. A curp a pufferbeli aktuális pozícióra mutat, s level pedig számlálja, hogy még hány karakter van hátra a pufferben. A teljes pufferezettség azt jelenti, hogy kiírás automatikusan csak akkor történik, ha a puffer teljesen feltelt, ill. olvasás csak akkor következik be, ha a puffer teljesen kiürült. Egy karakter írása, vagy olvasása a curp pozícióról, ill. pozícióra történik, s a mvelet mellékhatásaként a curp eggyel n, s a level eggyel csökken. A pufferezetlenség azt jelenti, hogy a bájtok átvitele azonnal megtörténik a fájlba (fájlból), vagy az eszközre (eszközrl). A mai operációs rendszerek legtöbbje a kisebb fájlokat megnyitásuk után valamilyen rendszer területen (cash) tartja, s a pufferek is csak a memóriabeli fájllal vannak kapcsolatban. Célszer tehát, a programfejleszt rendszer segítségében utánanézni, hogy az azonnali fájlba írás, vagy olvasás pontosan hogyan valósítható meg, ha igazán szükség van rá. A setbuf és a setvbuf függvényhívásokkal kijelölhetünk saját puffert, módosíthatjuk a használatos puffer méretét, vagy pufferezetlenné tehetjük a bemenetet és a kimenetet. void setbuf(FILE *stream, char *puff); A függvény az automatikusan allokált (malloc) puffer helyett a puff puffert használtatja a stream folyammal adatátvitel esetén. Ha a puff paraméter NULL mutató, akkor a folyam pufferezetlen lesz, máskülönben a folyam teljesen pufferezett. A puffer különben BUFSIZ méret. A szabvány bemenet (stdin) sorpufferezett és a szabvány kimenet (stdout) pufferezetlen, ha nincsenek az operációs rendszerben átirányítva, mert ekkor mindkett teljesen pufferezett. A sorpufferezettség azt jelenti, hogy ha a puffer üres, a következ bemeneti mvelet megkísérli a teljes

C programnyelv

257

puffer feltöltését. Kimenet esetén mindig kiürül a puffer, ha teljesen feltelik, ill. amikor '\n' karaktert írunk bele. Elre megjósolhatatlan hiba következik be, ha a setbuf függvényt nem közvetlenül a folyam megnyitása után hívják meg. Legális lehet még a pufferezetlen folyamra vonatkozó setbuf hívás, bárhol is következik be. Vigyázzunk a puffer auto tárolási osztályú deklarációjával, mert akkor csak abból a függvénybl lesz elérhet, ahol deklaráltuk! Még ,,szarvasabb" a hiba, ha kilépünk a folyam lezárása nélkül abból a függvénybl, melyre nézve a pufferünk lokális volt. int setvbuf(FILE *stream, char *puff, int tipus, size_t meret); A függvény ugyanazt teszi, mint a setbuf. Látható azonban, hogy expliciten megadható a puffer tipusa és merete. A size_t típusból következleg nagy méret puffer is elírható. A pufferezetlenség ezzel a függvénnyel a tipus paraméter megfelel megadásával érhet el, ugyanis ha az aktuális puff paramétert NULL mutatónak választjuk, akkor a rutin malloc­kal foglal memóriát a puffernek. A tipus paraméter lehetséges értékei a következk: · _IOFBF: A fájl teljesen pufferezett. Ha kiürül, a következ bemeneti mvelet megkísérli teljesen feltölteni a puffert. Kimenet esetén fájlba írás automatikusan csak akkor történik, ha a puffer teljesen feltelt. · _IOLBF: A fájl sorpufferezett. · _IONBF: A fájl pufferezetlen. A puff és a meret paraméter figyelmen kívül marad. Minden bemeneti és kimeneti mvelet közvetlen adatátvitelt jelent a fájlba. A setvbuf zérust ad vissza sikeres esetben, és nem zérust kapunk, ha a megadott tipus, vagy a meret paraméter érvénytelen, vagy nincs elég memória a puffer allokálásához. Nézzünk egy példát!
#include char puff[BUFSIZ]; void main(void) { FILE *input, *output; if((input=fopen("file.in","r"))!=NULL){ if(output=fopen("file.out","w")){ if(setvbuf(input,puff,_IOFBF,BUFSIZ)) printf("Sikertelen a saját input puffer " "allokálása!\n");

258

SZABVÁNY, MAGAS SZINT BEMENET, KIMENET

/* A bemeneti folyam saját puffert használva, minimális lemezhez fordulással mveletre kész. */ if(setvbuf(output,NULL,_IOLBF,128)) printf("Az output puffer allokálása " "sikertelen!\n"); else{ /* A kimeneti folyam sorpufferezetten, malloc hívással allokált pufferrel mveletre kész. */ /* Itt intézhet a fájlkimenet és bemenet! */ } /* Fájlok lezárása. */ fclose(output); } else printf("Az output fájl megnyithatatlan!\n"); fclose(input); } else printf("Az input fájl megnyitása sikertelen!\n"); }

Eddig csak a pufferek automatikus ürítésérl beszéltünk. Lehetséges azonban a pufferek kézi ürítése is. St, adatátviteli irányváltás eltt a kimeneti puffert ki is kell üríteni. Lássuk a függvényt! int fflush(FILE *stream); Kimenetre nyitott folyam esetén a rutin kiírja a puffer tartalmát a kapcsolt fájlba. Bemeneti folyamnál a függvény eredménye nem definiálható, de többnyire törli a puffer tartalmát. Mindkét esetben nyitva marad a folyam. Pufferezetlen folyamnál e függvény hívásának nincs hatása. Sikeres esetben zérust kapunk vissza. Hibás esetben a szolgáltatott érték EOF. Az fflush(NULL) üríti az összes kimeneti folyamot. 11.3 Pozícionálás a folyamokban A folyamokat rendszerint szekvenciális fájlok olvasására, írására használják. A magas szint bemenet, kimenet a fájlt bájtfolyamnak tekinti, mely a fájl elejétl (0 pozíció) indul és a fájl végéig tart. A fájl utolsó pozíciója a fájlméret - 1. Az adatátvitel mindig az aktuális fájlpozíciótól kezddik, megtörténte után a fájlpozíció a fájlban következ, át nem vitt bájtra mozdul. A fájlpozíciót fájlmutatónak is szokás nevezni. Eszközhöz kapcsolt folyam mindig csak szekvenciálisan (zérustól induló, monoton növekv fájlpozícióval) érhet el. Lemezes fájlhoz kapcsolt folyam bájtjai azonban direkt (random) módon is olvashatók és írhatók. Lemezes fájlok esetén a fájlmutató adatátvitel eltti beállítását az

C programnyelv int fseek(FILE *stream, long offset, int ahonnet);

259

függvénnyel végezhetjük el, mely a stream folyam fájlmutatóját offset bájttal az ahonnet paraméterrel adott fájlpozíción túlra állítja be. Szöveges folyamokra az offset zérus lehet, vagy egy az ftell függvény által visszaadott érték. Az ahonnet paraméter a következ értékeket veheti fel: · SEEK_SET: A fájl kezdetétl. · SEEK_CUR: Az aktuális fájlpozíciótól. · SEEK_END: A fájl végétl. A függvény elvet minden a bemenetre ungetc­vel visszarakott karaktert. Az ungetc­rl a következ fejezetben lesz szó! Felújításra megnyitott fájl esetén az fseek után mind bemenet, mind kimenet következhet. A függvény törli a fájlvég jelzt. Lásd a fájl állapotjelzi közt még ebben a fejezetben! A függvény zérust ad vissza, ha a fájlpozícionálás sikeres volt, ill. nem zérust kapunk hiba esetén. Írjunk fájlméretet megállapító függvényt!
#include long fajlmeret(FILE *stream) { long aktpoz, hossz; aktpoz=ftell(stream); fseek(stream, 0L, SEEK_END); hossz=ftell(stream); fseek(stream, aktpoz, SEEK_SET); return(hossz); }

A fajlmeret elteszi a pillanatnyi pozíciót az aktpoz változóba, hogy a fájl végére állítás után helyre tudja hozni a fájlmutatót. A lekérdezett fájlvég pozíció éppen a fájlméret. Nézzük a további fájlpozícióval foglalkozó függvényeket! long int ftell(FILE *stream); A rutin visszaadja a stream folyam aktuális fájlpozícióját sikeres esetben, máskülönben -1L-t kapunk tle. A void rewind(FILE *stream);

260

SZABVÁNY, MAGAS SZINT BEMENET, KIMENET

a stream folyam fájlmutatóját a fájl elejére állítja. Felújításra megnyitott fájl esetén a rewind után mind bemenet, mind kimenet következhet. A függvény törli a fájlvég és a hibajelz biteket. A FILE struktúra flags szava bitjei (állapotjelzi) a következ jelentések lehetnek!
#define #define #define #define #define #define #define #define /* . . . _F_RDWR _F_READ _F_WRIT _F_BUF _F_LBUF _F_ERR _F_EOF _F_BIN */ 0x0003 0x0001 0x0002 0x0004 0x0008 0x0010 0x0020 0x0040 /* /* /* /* /* /* /* /* olvasás és írásjelz */ csak olvasható fájl */ csak írható fájl */ malloc pufferelt */ sorpufferelt fájl */ hibajelz */ fájlvég jelz */ bináris fájl jelz */

A megadott stream folyam aktuális fájlpozícióját helyezi el az int fgetpos(FILE *stream, fpos_t *pos); a pos paraméterrel adott címen. Ez a érték felhasználható az fsetpos­ban. A visszaadott érték zérus hibátlan, és nem zérus sikertelen esetben. Az int fsetpos(FILE *stream, const fpos_t *pos); a stream folyam fájlmutatóját állítja be a pos paraméterrel mutatott értékre. Felújításra megnyitott fájl esetén az fsetpos után mind bemenet, mind kimenet következhet. A függvény törli a fájlvég jelz bitet, és elvet minden, e fájlra vonatkozó ungetc karaktert. A visszakapott érték egyezik az fgetpos­nál írottakkal. Vegyük észre, hogy az fseek és az ftell long értékekkel dolgozik. A maximális fájlméret így 2GB lehet. Az fpos_t adattípus e korlát áttörését biztosítja, hisz mögötte akár 64 bites egész is lehet. 11.4 Bemeneti mveletek int fgetc(FILE *stream); A folyam következ unsigned char karakterét adja vissza eljel kiterjesztés nélkül int­té konvertáltan, s eggyel elbbre állítja a fájlpozíciót. Sikertelen esetben, ill. fájl végén EOF­ot kapunk. A

C programnyelv int getc(FILE *stream);

261

makró, mint ahogyan ez a lehetséges definíciójából is látszik, ugyanezt teszi:
#define getc(f) \ ((--((f)->level)>=0) ? (unsigned char)(*(f)->curp++) :\ _fgetc(f))

int ungetc(int c, FILE *stream); A függvény visszateszi a stream bemeneti folyamba a c paraméter unsigned char típusúvá konvertált értékét úgy, hogy a következ olvasással ez legyen az els elérhet karakter. A szabályos mködés csak egyetlen karakter visszahelyezése esetén garantált, de a visszatett karakter nem lehet az EOF. Két egymást követ ungetc hívás hatására már csak a másodiknak visszatett karakter érhet el, mondjuk, a következ getc­vel, azaz az els elveszik. Gondoljuk csak meg, hogyha nincs puffer, akkor a visszatételhez a FILE struktúra egyetlen hold tagja áll rendelkezésre! Az fflush, az fseek, az fsetpos, vagy a rewind törli a bemenetre visszatett karaktert. Sikeres híváskor az ungetc a visszatett karaktert adja vissza. Hiba esetén viszont EOF­ot kapunk tle. Az char *fgets(char *s, int n, FILE *stream); karakterláncot hoz be a stream folyamból, melyet az s címtl kezdve helyez el a memóriában. Az átvitel leáll, ha a függvény n - 1 karaktert, vagy '\n'­t olvasott. A rutin a '\n' karaktert is kiteszi a láncba, és a végéhez még záró '\0'­t is illeszt. Sikeres esetben az fgets az s karakterláncra mutató mutatóval tér vissza. Fájlvégen, vagy hiba esetén viszont NULL­t szolgáltat. Vegyük észre, hogy a jegyzet eleje óta használt getline függvény csak annyiban tér el az fgets­tl, hogy: · A beolvasott karakterlánc méretét adja vissza. · A szabvány bemenetrl (stdin) olvas, s nem más folyamból, így eggyel kevesebb a paramétere. · n karaktert hoz be legfeljebb, vagy '\n'­ig, de magát a soremelés karaktert nem teszi be az eredmény karakterláncba. size_t fread(void *ptr, size_t size, size_t n, FILE *stream);

262

SZABVÁNY, MAGAS SZINT BEMENET, KIMENET

A függvény n * size bájtot olvas a stream folyamból, melyet a ptr paraméterrel mutatott címen helyez el. Visszaadott értéke nem a beolvasott bájtok száma, hanem a
beolvasott bájtok száma / size

sikeres esetben. Hiba, vagy fájlvég esetén ez persze nem egyezik n­nel. Az eddig ismertetett bemeneti függvények nem konvertálták a beolvasott karakter(lánco)t. Az int fscanf(FILE *stream, const char *format<, cim, ...>); viszont a stream folyamból karakterenként olvasva egy sor bemeneti mezt vizsgál. Aztán minden mezt a format karakterláncnak megfelelen konvertál, és letárol rendre a paraméter cimeken. A format karakterláncban ugyanannyi konverziót okozó formátumspecifikációnak kell lennie, mint ahány bemeneti mez van. A jelölésben a <> az elhagyhatóságot, a ... a megelz paraméter tetszleges számú ismételhetségét jelenti. A bemeneti mez definíciója, a formázás és a konvertálás részletei a scanf függvény leírásában találhatók meg! Az fscanf a sikeresen vizsgált, konvertált és letárolt bemeneti mezk számával tér vissza. Ha a függvény az olvasást a fájl végén kísérelné meg, vagy valamilyen hiba történne, akkor EOF-ot kapunk tle vissza. A rutin zérussal is visszatérhet, ami azt jelenti, hogy egyetlen vizsgált mezt sem tárolt le. 11.5 Kimeneti mveletek int fputc(int c, FILE *stream); A függvény a c unsigned char típusúvá konvertált értékét írja ki a stream folyamba. Sikeres esetben a c karaktert kapjuk vissza tle, hiba bekövetkeztekor viszont EOF­ot. A int putc(int c, FILE *stream); makró, mint ahogyan ez a lehetséges definíciójából is látszik, ugyanezt teszi:
#define putc(c,f) \ ((++((f)->level)<0) ? (unsigned char)(*(f)->curp++)=(c)) :\ _fputc((c),f))

int fputs(const char *s, FILE *stream);

C programnyelv

263

A függvény az s karakterláncot kiírja a stream folyamba. Nem fz hozzá '\n' karaktert, és a lezáró '\0' karakter sem kerül át. Sikeres esetben nem negatív értékkel tér vissza. Hiba esetén viszont EOF­ot kapunk tle. Az size_t fwrite(const void *ptr, size_t size, size_t n, FILE *stream); a ptr címmel mutatott memória területrl n * size bájtot ír ki a stream folyamba. Visszaadott értéke nem a kiírt bájtok száma, hanem a
kiírt bájtok száma / size

sikeres esetben. Hiba bekövetkeztekor ez nem egyezik n­nel. Az eddigi kimeneti függvények nem végeztek konverziót. Az int fprintf(FILE *stream, const char *format<, parameter, ...>); fogad egy sor parametert, melyeket a format karakterláncnak megfelelen formáz (konvertál), és kivisz a stream folyamba. A format karakterláncban ugyanannyi konverziót okozó formátumspecifikációnak kell lennie, mint ahány parameter van. A jelölésben a <> az elhagyhatóságot, a ... a megelz paraméter tetszleges számú ismételhetségét jelenti. A formázás és a konvertálás részletei a printf függvény leírásában találhatók meg! Az fprintf a folyamba kivitt karakterek számával tér vissza sikeres esetben, ill. EOF-ot kapunk tle hiba bekövetkeztekor. 11.6 Folyamok lezárása int fclose(FILE *stream); A rutin lezárja a stream folyamot. Ez eltt azonban üríti a folyamhoz tartozó puffert, s a pufferhez automatikusan allokált memóriát fel is szabadítja. Ez utóbbi nem vonatkozik a setbuf, vagy a setvbuf függvényekkel hozzárendelt pufferekre. Ezek ,,ügyei" csak a felhasználóra tartoznak. Sikeres esetben az fclose zérussal tér vissza. Hiba esetén viszont EOF­ ot kapunk tle. 11.7 Hibakezelés Tudjuk, hogy a szabvány könyvtári függvények ­ így a magas szint bemenetet, kimenetet kezelk is ­ a hibát, a kivételes esetet úgy jelzik, hogy valamilyen speciális értéket (EOF, NULL mutató, HUGE_VAL stb.) adnak vissza, és az errno globális hibaváltozóba beállítják a hiba

264

SZABVÁNY, MAGAS SZINT BEMENET, KIMENET

kódját. A hibakódok az ERRNO.H fejfájlban definiált, egész, nem zérusérték szimbolikus állandók. Programunk indulásakor a szabvány bemeneten (stdin) és kimeneten (stdout) túl a hibakimenet (stderr) is rendelkezésre áll, s a hibaüzeneteket ez utóbbin célszer megjelentetni. Az stderr a képerny (karakteres ablak) alapértelmezés szerint, s nem is irányítható át fájlba a legtöbb operációs rendszerben, mint ahogyan a bemenettel (<) és a kimenettel (>) ez megtehet volt a programot futtató parancssorban. Mit jelentessünk meg hibaüzenetként az stderr­en? Természetesen bármilyen szöveget kiírathatunk, de a hibakódokhoz programfejlszt rendszertl függen hibaüzenet karakterláncok is tartoznak, s ezek is megjelentethetk. A hibakodhoz tartozó hibaüzenet karakterlánc kezdcímét szolgáltatja a szabványos #include char *strerror(int hibakod); függvény, s az üzenet meg is jelentethet
fprintf(stderr, "Hiba: %s\n", strerror(hibakod));

módon. A void perror(const char *s); kiírja az stderr­re azt a hibaüzenetet, melyet a legutóbbi hibát okozó, könyvtári függvény hívása idézett el. Elször megjelenteti az s karakterláncot a rutin, aztán kettspontot (:) tesz, majd az errno aktuális értékének megfelel üzenet karakterláncot írja ki lezáró '\n'-nel. Tehát például a perror("Hiba: ") megfelel a
fprintf(stderr, "Hiba: %s\n", strerror(errno));

függvényhívásnak. Vigyázat! Az errno értékét csak közvetlenül a hibát okozó rutin hívása után szabad felhasználni, mert a következ könyvtári függvény megidézése felülírhatja e globális változó értékét. Ha a hibakóddal mégis késbb kívánnánk foglalkozni, akkor tegyük el az errno értékét egy segédváltozóba! Folyamokkal kapcsolatban a perror s paramétere a fájlazonosító szokott lenni. Meg kell tárgyalnunk még három, csak a folyamok hibakezelésével foglalkozó függvényt! A

C programnyelv void clearerr(FILE *stream);

265

nullázza a stream folyam fájlvég és hibajelzjét. Ha a folyam hibajelz bitje egyszer bebillent, akkor minden a folyamon végzett mvelet hibával tér vissza mindaddig, míg a hibajelzt e függvénnyel, vagy a rewind­dal nem törlik. A fájlvég jelz bitet egyébként minden bemeneti mvelet nullázza. Az int feof(FILE *stream); többnyire makró, mely a következ lehetséges
#define feof(f) ((f)->flags & _F_EOF)

definíciója miatt, visszaadja a fájlvég jelz bit állapotát, azaz választ ad a fájlvég van-e kérdésre. Az egyszer bebillent fájlvég jelz bit a következ, e folyamra vonatkozó bemeneti, pozícionáló mveletig, vagy clearerr­ig 1 marad. Az int ferror(FILE *stream); makró ebben a szellemben
#define ferror(f) ((f)->flags & _F_ERR)

a hiba jelz bit állapotát adja vissza. Az egyszer bebillent hiba jelz bitet csak a clearerr és a rewind függvények törlik. Ha a kérdéses folyammal kapcsolatban a bebillent hiba jelz bit törlésérl nem gondoskodunk, akkor minden e folyamra meghívott további függvény hibát jelezve fog visszatérni. Írjuk meg az fputc segítségével az fputs függvényt!
int fputs(const char *s, FILE *stream){ int c; while(c=*s++) if(c!=fputc(c, stream)) break; return ferror(stream) ? EOF : 1; }

Készítsünk szoftvert komplett hibakezeléssel, mely az els parancssori paramétere fájlt átmásolja a második paramétere azonosítójú fájlba! Ha a programot nem elég parancssori paraméterrel indítják, akkor ismertesse használatát! A másolási folyamat elrehaladásáról tájékoztasson feltétlenül!
/* PELDA31.C: Els paraméter fájl másolása a másodikba. */ #include #include /* strerror miatt! */

266

SZABVÁNY, MAGAS SZINT BEMENET, KIMENET

#include /* Az errno végett! */ #define KENT 10 /* Hányanként jelenjen meg a számláló.*/ #define SZELES 10 /* Mezszélesség a számló közléséhez. */ int main(int argc, char *argv[]){ FILE *be, *ki; /* A be és kimeneti fájlok. */ long szlo=0l; /* A számláló. */ int c; /* A köv. karakter és segédváltozó. */ printf("Az els paraméter fájl másolása a másodikba:\n"); if(argc<3){ fprintf(stderr, "Programindítás:\n" "PELDA30 forrásfájl célfájl\n"); return 1; } if(!(be=fopen(argv[1],"rb"))){ perror(argv[1]); return 1; } if(!(ki=fopen(argv[2],"wb"))){ perror(argv[2]); fclose(be); return 1; } printf("%s --> %s:\n%*ld",argv[1],argv[2], SZELES, szlo);

A formátumspecifikációbeli * SZELES mezszélességet eredményez.
while((c=fgetc(be))!=EOF){/* Olvasás fájlvégig, vagy hibáig. */ if(c==fputc(c,ki)){ /* Kiírás rendben. */ if(!(++szlo%KENT)){ for(c=0; c
C programnyelv
return 0; }

267

Az stdout­ra irányuló mveletek hibakezelésével azért nem foglalkoztunk, mert ahol az sem mködik, ott az operációs rendszer sem megy. Megoldandó feladatok: Fokozzuk kicsit a PELDA31.C­ben megvalósított feladatot! A fájlba írás normál esetben akkor nem megy, ha betelik a lemez. Ezen próbáljunk meg úgy segíteni, hogy a forrásfájl megnyitása után állapítsuk meg a méretét! A célfájlnak is foglaljunk helyet (fseek) ugyanekkora méretben, majd felújítva írjuk ki rá a forrás tartalmát! Készítsen szoftvert, mely eldönti az indító parancssorban megadott azonosítójú fájl típusát, azaz hogy szöveges, vagy bináris! Ha parancssori paraméter nélkül futtatják a programot, akkor ismertesse a használatát! Írjon szoftvert, mely az indító parancssorban megadott szövegfájlokat egyesíti a megadás sorrendjében a parancssorban utolsóként elírt azonosítójú szövegfájlba! Ha parancssori paraméter nélkül indítják a programot, akkor ismertesse a képernyn, hogyan kell használni! Ha csak egy fájlazonosító van a parancssorban, akkor a szabvány bemenet másolandó bele. A fájlok egyesítése során a folyamat elrehaladásáról tájékoztatni kell a képernyn! A szabvány bemenet másolása esetén végül közlend még az eredményfájl mérete! 11.8 Elre definiált folyamok Egy idben legfeljebb FOPEN_MAX, vagy OPEN_MAX folyam (fájl) lehet megnyitva. Ennek megfelel a globális FILE struktúratömb
extern FILE _streams[];

mérete is, melybl ráadásul még az els három bizonyosan foglalt is:
#define #define #define stdin (&_streams[0]) stdout (&_streams[1]) stderr (&_streams[2])

A globális FILE struktúratömb neve persze lehet ettl eltér is. Ezek az elre definiált folyamok, melyek programunk futásának megkezdésekor már megnyitva rendelkezésre állnak.

268 Név stdin stdout stderr

SZABVÁNY, MAGAS SZINT BEMENET, KIMENET B/K bemenet kimenet kimenet Típus szöveges szöveges szöveges Folyam szabványos bemenet szabványos kimenet szabvány hibakimenet Alapértelmezés CON: CON: CON:

Az stdin és az stdout átirányítható a programot indító parancssorban szövegfájlba.
program < bemenet.txt > kimenet.txt

Ha nincsenek átirányítva, akkor az stdin sorpufferezett, s az stdout pedig pufferezetlen. Ilyen az stderr is, tehát pufferezetlen. A legtöbb operációs rendszerben cs (pipe) is használható. Például:
program1 | program2 | program3

program1 a rendszerben beállított szabvány bemenettel rendelkezik. Szabvány kimenete szabvány bemenete lesz program2­nek, aminek szabvány kimenete program3 szabvány bemenete. Végül program3 szabvány kimenete az, amit a rendszerben beállítottak. Mindhárom elre definiált folyam átirányítható a programban is, azaz ha nem felelne meg az alapértelmezés szerint a folyamhoz kapcsolt eszköz, akkor ezt kicserélhetjük az FILE *freopen(const char *fajlazonosito, const char *mod, FILE *stream); függvénnyel a fajlazonositojú fájlra. A rutin els két paraméterének értelmezése és visszaadott értéke egyezik az fopen­ével. A harmadik viszont az elre definiált folyam: stdin, stdout vagy stderr. Az freopen persze nem csak elre definiált folyamokra használható, hanem bármilyen mással is, de ez a legjellemzbb alkalmazása. Készítsünk programot, mely a szabvány bemenetrl érkez karaktereket a parancssori paraméterként megadott szövegfájlba másolja! Ha indításkor nem adnak meg parancssori paramétert, akkor csak echózza a szoftver a bementet a kimeneten! A feladatot az stdout átirányításával oldjuk meg.
/* PELDA32.C: Bemenet másolása fájlba stdout-ként. */ #include #include /* A system rutin miatt! */ #define PUFF 257 /* A bemeneti puffer mérete. */

C programnyelv
int main(int argc, char *argv[]){ char puff[PUFF]; /* Bemeneti puffer. */ if(system(NULL)) system("CLS"); printf("A szabvány bemenet fájlba másolása " "Ctrl+Z-ig:\n"); if(argc<2) printf("A program indítható így is:\n" "PELDA32 szövegfájl\n\n"); else if(!freopen(argv[1],"wt", stdout)){ perror(argv[1]); return 1; } while(fgets(puff, PUFF, stdin)){ if(fputs(puff, stdout)<0){ perror(argv[1]); clearerr(stdout); if(!fclose(stdout)) remove(argv[1]); else perror(argv[1]); return 1; } } return 0; }

269

Az STDLIB.H bekapcsolásával rendelkezésre álló int system(const char *parancs); rutin parancs paraméterét átadja végrehajtásra az operációs rendszernek (a parancsértelmeznek), azaz végrehajtatja a rendszerrel a parancsot. A függvény visszatérési értéke a programfejleszt rendszertl függ, de többnyire a parancsértelmez által szolgáltatott érték az. Ha a parancs NULL, akkor a rutin a parancsértelmez létezésérl számol be, azaz ha van, nem zérussal tér vissza, és zérust szolgáltat, ha nincs. 11.8.1 Bemenet az stdin-rl int getchar(void); A függvény makró, azaz:
#define getchar() getc(stdin)

A char *gets(char *s); az fgets­hez hasonlóan karaktereket olvas az stdin­rl, melyeket rendre elhelyez a paraméter s karaktertömbben. A visszaadott értéke is egyezik az fgets­ével, azaz normál esetben s­t szolgáltatja, s fájlvég vagy hiba bekövetkeztekor NULL­t. Az stdin­rl való olvasás azonban az els '\n' karakterig tart. Magát az LF karaktert nem viszi át az s tömbbe, hanem helyette a karakterláncot záró '\0'­t ír oda. A konverziót is végz

270

SZABVÁNY, MAGAS SZINT BEMENET, KIMENET

int scanf(const char *format<, cim, ...>); függvény az fscanf­hoz hasonlóan ­ de az stdin folyamból ­ olvasva egy sor bemeneti mezt vizsgál. Aztán minden mezt a format karakterláncnak megfelelen formáz (konvertál), és letárol rendre a paraméter címeken. A jelölésben a <> az elhagyhatóságot, a ... a megelz paraméter tetszleges számú ismételhetségét jelenti. A bemeneti mez definíciójára rögtön kitérünk! A scanf a sikeresen vizsgált, konvertált és letárolt bemeneti mezk számával tér vissza. A vizsgált vagy akár konvertált, de le nem tárolt mezk ebbe a számba nem értendk bele. Ha a függvény az olvasást a fájl végén kísérelné meg, vagy valamilyen hiba következne be, akkor EOF­ot kapunk tle vissza. A függvény zérussal is visszatérhet, ami azt jelenti, hogy egyetlen vizsgált mezt sem tárolt le. A format karakterláncban ugyanannyi formátumspecifikációnak kell lennie, mint ahány bemeneti mez van, és ahány cim paramétert megadtak a hívásban. Ha a formátumspecifikációk többen vannak, mint a cimek, akkor ez elre megjósolhatatlan hibához vezet. Ha a cim paraméterek száma több mint a formátumspecifikációké, akkor a felesleges cimeket egyszeren elhagyja a scanf. A format karakterlánc három féle objektumból áll: · fehér karakterekbl, · nem fehér karakterekbl és · formátumspecifikációkból. Ha fehér karakter következik a format karakterláncban, akkor a scanf olvassa, de nem tárolja a bemenetrl érkez fehér karaktereket egészen a következ nem fehér karakterig. Nem fehér karakter minden más a '%' kivételével. Ha a format karakterláncban ilyen karakter következik, akkor a scanf olvas a bemenetrl, de nem tárol, hanem elvárja, hogy a beolvasott karakter egyezzen a format karakterláncban levvel. A formátumspecifikációk vezérlik a scanf függvényt az olvasásban, a bemeneti mezk konverziójában és a konverzió típusában. A konvertált értéket aztán a rutin elhelyezi a soron következ paraméterrel adott cim­ en. A formátumspecifikáció általános alakja:
% <*> típuskarakter

C programnyelv

271

, ahol a <> az elhagyhatóságot és a | a vagylagosságot jelöli. Nézzük a részleteket! · Minden formátumspecifikáció % karakterrel indul, és típuskarakterrel végzdik. Az általános alakban elhagyhatónak jelölt részek csak az ott megadott sorrendben kerülhetnek a % és a típuskarakter közé. · A * elnyomja a következ bemeneti mez hozzárendelését. A scanf a "%*típuskarakter" hatására olvassa, ellenrzi és konvertálja a vonatkozó bemeneti mezt, de nem helyezi el a kapott értéket az idetartozó cim paraméteren. Tehát a bemeneti mez tartalmának ilyenkor is meg kell felelnie a konverziós típuskarakternek. · A szélesség maximális mezszélességet határoz meg, azaz a scanf legfeljebb ennyi karaktert olvashat, de olvashat ennél kevesebbet is, ha fehér, vagy konvertálhatatlan karakter következik a bemeneten. · A h, az l és az L a cim paraméter alapértelmezés szerinti típusát módosítja. A h short int. Az l long int, ha a típuskarakter egész konverziót specifikál, ill. double, ha a típuskarakter lebegpontos átalakítást ír el. Az L pedig a long double módosítója. A következ táblázatban felsoroljuk az aritmetikai konverziót okozó típuskaraktereket: Típuskarakter d i o u x e, E f g, G Az elvárt bemenet decimális egész decimális, oktális vagy hexadecimális egész oktális egész (vezet 0 nélkül is annak minsül a szám) eljel nélküli decimális egész hexadecimális egész (vezet 0x vagy 0X nélkül is az a szám) lebegpontos valós lebegpontos valós lebegpontos valós A paraméter típusa int * int * int * unsigned int * int * float * float * float *

· A %d, a %i, a %o, a %x, a %D, a %I, a%O, a %X, a %c és a %n konverziók esetén unsigned char­ra, unsigned int­re, vagy unsig-

272

SZABVÁNY, MAGAS SZINT BEMENET, KIMENET ned long­ra mutató mutatók is használhatók azoknál az átalakításoknál, ahol a char­ra, az int­re, vagy a long­ra mutató mutató megengedett.

· A %e, a %E, a %f, a %g és a %G lebegpontos konverziók esetén a bemeneti mezben lev valós számnak ki kell elégítenie a kvetkez formát:
<+|-> ddddddddd <.> dddd <+|-> ddd

ahol d decimális, oktális, vagy hexadecimális számjegyet, a <> elhagyhatóságot és a | vagylagosságot jelöl. A mutató konverzió típuskarakterei: Típuskarakter Az elvárt bemenet A paraméter típusa n Nincs. int *. A %n-ig sikeresen olvasott karakterek számát tárolja ebben az int-ben a scanf.

p

Megvalósítástól void * függ formában, de általában hexadecimálisan.

A karakteres konverzió típuskarakterei: Típuskarakter Az elvárt bemenet A paraméter típusa c karakter Mutató char­ra, ill. mutató char tömbre, ha mezszélességet is megadtak. Pl.: %7c. Nincs konverzió. Magát a % karaktert tárolja. Mutató char tömbre. Mutató char tömbre. Mutató char tömbre.

% s [kereskészlet]

% karakter karakterlánc karakterlánc

[^kereskészlet] karakterlánc

· A %c hatására a scanf a következ karaktert (akár fehér, akár nem) olvassa a bemenetrl. Ha a fehér karaktereket át kívánjuk lépni, használjuk a %1s formátumspecifikációt!

C programnyelv

273

· A %szélességc specifikációhoz tartozó cim paraméternek legalább szélesség elem karaktertömbre kell mutatnia. · A %s specifikációhoz tartozó cim paraméternek legalább akkora karaktertömbre kell mutatnia, melyben a vonatkozó bemeneti mez minden karaktere, és a karakterláncot lezáró '\0' is elfér. · A %[kereskészlet] és a %[^kereskészlet] alakú specifikáció teljes mértékben helyettesíti az s típuskaraktert. A vonatkozó cim paraméternek karaktertömbre kell ekkor is mutatnia. A szögletes zárójelben lev karaktereket kereskészletnek nevezzük. · %[kereskészlet] esetében a scanf addig olvassa a bemenetet, míg a bejöv karakterek egyeznek a kereskészlet valamelyik karakterével. A karaktereket kiteszi rendre a rutin '\0'­val lezártan a paraméter karaktertömbbe. Például a %[abc]-vel az 'a', a 'b' és a 'c' karakterek valamelyikét kerestetjük a bemeneti mezben. A %[]xyz] viszont a ']', az 'x', a 'y' és a 'z' után kutat. · %[^kereskészlet] a scanf bármilyen olyan karaktert keres, ami nincs benn a kereskészletben. Például a %[^]abc] hatására addig tart a bemenet olvasása, míg róla ']', 'a', 'b' vagy 'c' nem érkezik. Néhány programfejleszt rendszer esetén a kereskészletben tartomány is megadható, azaz például a %[0123456789]-et a %[0-9] teljes mértékben helyettesíti. A tartomány kezd karaktere kódjának azonban kisebbnek kell lenni a tartomány vég karaktere kódjánál. Nézzünk néhány példát! · %[-+*/]: A négy aritmetikai operátort keresi. · %[0-9A-Za-z]: Alfanumerikus karaktert keres. · %[+0-9-A-Z]: A '+', a '-', a szám és a nagybet karaktereket keresi. · %[z-a]: A 'z', a '-' és az 'a' karaktereket keresi. Tisztázzuk végre a bemeneti mez fogalmát! · Minden karakter a következ fehér karakterig, de a fehér karakter maga már nem tartozik bele. · Minden karakter az els olyan karakterig, mely az aktuális típuskarakter szerint nem konvertálható. · Minden karakter, míg a megadott mezszélesség ki nem merül.

274

SZABVÁNY, MAGAS SZINT BEMENET, KIMENET

· Kereskészlet esetén addig tart a bemeneti mez, míg a kereskészlet feltételeinek meg nem felel karakter nem érkezik a bemenetrl. A bemeneti mez második alternatívája miatt, nem javasoljuk a scanf függvény széleskör használatát programokban. Helyette olvassuk be a bemeneti karakterláncot, végezzük el rajta az összes formai ellenrzést! Ha aztán minden rendben volt, a konverzió megvalósítható egy menetben az int sscanf(const char *puffer, const char *format<, cim, ...>); függvénnyel, mely ugyanazt teszi, mint a scanf, de bemeneti mezit nem az stdin­rl, hanem az els paraméterként kapott karakterláncból veszi. 11.8.2 Kimenet az stdout-ra int putchar(int c); A függvény makró, azaz:
#define putchar(c) putc((c), stdout)

A int puts(const char *s); függvény a '\0' lezárású s karakterláncot az stdout folyamba írja a '\0' nélkül, mely helyett viszont kitesz még egy '\n' karaktert. Sikeres esetben nem negatív értékkel tér vissza. Hiba bekövetkeztekor viszont EOF-ot kapunk tle. A konverziót is végz int printf(const char *format<, parameter, ...>); rutin fogad egy sor parametert, melyek mindegyikéhez hozzárendel egy, a format karakterláncban lév formátumspecifikációt, és az ezek szerint formázott (konvertált) adatokat kiviszi az stdout folyamba. A jelölésben a <> az elhagyhatóságot, a ... a megelz paraméter tetszleges számú ismételhetségét jelenti. A format karakterláncban ugyanannyi formátumspecifikációnak kell lennie, mint ahány parameter van. Ha kevesebb a paraméter, mint a formátumspecifikáció, akkor ez elre megjósolhatatlan hibához vezet. Ha több a paraméter, mint a formátumspecifikáció, akkor a felesleges paramétereket egyszeren elhagyja a printf.

C programnyelv

275

A rutin a folyamba kivitt bájtok számával tér vissza sikeres esetben, ill. EOF­ot kapunk tle hiba bekövetkeztekor. A format karakterlánc kétféle objektumot tartalmaz: · sima karaktereket és · formátumspecifikációkat. A sima karaktereket változatlanul kiviszi az stdout-ra a printf. A formátumspecifikációhoz veszi a következ parameter értékét, konvertálja, és csak ezután teszi ki az stdout-ra. A formátumspecifikáció általános alakja a következ:
% <.pontosság> típuskarakter

· Minden formátumspecifikáció % karakterrel kezddik, és típuskarakterrel végzdik. · Ha a '%' karaktert szeretnénk az stdout­ra vinni, akkor meg kell duplázni (%%). · Az általános alakban elhagyhatónak jelölt részek csak az ott megadott sorrendben kerülhetnek a % és a típuskarakter közé. A következkben leírjuk a típuskarakterek értelmezését arra az esetre, ha a formátumspecifikációban a % jelet csak a típuskarakter követi. Nézzük elbb az aritmetikai konverziót okozó típuskaraktereket:

276

SZABVÁNY, MAGAS SZINT BEMENET, KIMENET Elvárt paraméter int int int int int int double double double double double A kimenet formája Eljeles decimális egész. Eljeles decimális egész. Eljel nélküli oktális egész vezet 0 nélkül. Eljel nélküli decimális egész. Eljel nélküli hexadecimális egész (a, b, c, d, e, f-fel), de vezet 0x nélkül. Eljel nélküli hexadecimális egész (A, B, C, D, E, F-fel), de vezet 0X nélkül. <->dddd.dddd alakú eljeles érték. <->d.ddd...e<+|->ddd alakú eljeles érték. <->d.ddd...E<+|->ddd alakú eljeles érték. Az adott értéktl és a pontosságtól függen e, vagy f alakban eljeles érték. Ugyanaz, mint a g forma, de az e alak használata esetén az exponens részben E van.

Típuskarakter d i o u x X f e E g G

e vagy E típuskarakter esetén a vonatkozó paraméter értékét a printf
<->d.ddd...e<+|->ddd

alakúra konvertálja, ahol: · Egy decimális számjegy (d) mindig megelzi a tizedes pontot. · A tizedes pont utáni számjegyek számát a pontosság határozza meg. · A kitev rész mindig legalább két számjegyet tartalmaz. f típuskarakternél a vonatkozó paraméter értékét a printf
<->ddd.ddd...

alakúra konvertálja, s a tizedes pont után kiírt számjegyek számát itt is a pontosság határozza meg. g vagy G típuskarakter esetén a printf a vonatkozó paraméter értékét e, E, vagy f alakra konvertálja · Olyan pontossággal, melyet a szignifikáns számjegyek száma meghatároz.

C programnyelv

277

· A követ zérusokat levágja az eredményrl, és a tizedes pont is csak akkor jelenik meg, ha szükséges, azaz van még utána értékes tört számjegy. · A g e, vagy f formájú, a G pedig E, vagy f alakú konverziót okoz. · Az e, ill. az E formát akkor használja a printf, ha a konverzió eredményében a kitev nagyobb a pontosságnál, vagy kisebb ­4­nél. A karakteres konverzió típuskarakterei: Típuskarakter % c s Elvárt paraméter nincs int char * A kimenet formája Nincs konverzió. Maga a % karakter jelenik meg. Egyetlen karakter. A karakterlánc karakterei megjelennek a záró '\0'­t kivéve. Ha megadtak pontosságot, akkor legfeljebb annyi karaktert ír ki a printf.

278

SZABVÁNY, MAGAS SZINT BEMENET, KIMENET

A mutató konverzió típuskarakterei: Típuskarakter n Elvárt paraméter int * A kimenet formája A paraméter által mutatott int-ben letárolja az eddig kiírt karakterek számát. Nincs különben semmilyen konverzió. A paramétert mutatóként jelenteti meg. A kijelzés formátuma programfejleszt rendszer függ, de általában hexadecimális.

p

void *

Lássuk a jelzket! Az eredmény balra igazított, és jobbról szóközzel párnázott. Ha a - jelzt nem adják meg, akkor az eredmény jobbra igazított, és balról szóközökkel, vagy zérusokkal párnázott. Eljeles konverzió eredménye mindig plusz, vagy mínusz eljellel kezddik. Ha a + jelzvel együtt szóköz jelzt is megadnak, akkor a + jelz van érvényben. Ha az érték nem negatív, a kimenet egy szóközzel kezddik a plusz eljel helyett. A negatív érték ilyenkor is mínusz eljelet kap. Azt határozza meg, hogy a paramétert alternatív formát használva kell konvertálni. A # hatása a paraméterre Nincs hatás. Az eredményben mindenképpen lesz tizedes pont még akkor is, ha azt egyetlen számjegy sem követi. Normálisan ilyenkor nem jelenik meg a tizedes pont. Ugyanaz, mint e és E, de az eredménybl a követ zérusokat nem vágja le a printf. 0-t ír a konvertált, nem zérus paraméter érték elé. Ez az oktális szám megjelentetése. 0x, 0X elzi meg a konvertált, nem zérus paraméter értéket.

+

szóköz

#

Az alternatív formák a típuskaraktertl függnek: Típ.kar. c,s,d,i,u e,E,f

g,G o x, X

C programnyelv

279

A szélesség a kimeneti érték minimális mezszélességét határozza meg, azaz a megjelen eredmény legalább ilyen szélesség. A szélességet két módon adhatjuk meg: · vagy expliciten beírjuk a formátumspecifikációba, · vagy a szélesség helyére * karaktert teszünk. Ilyenkor a printf hívásban a következ parameter csak int típusú lehet, s ennek az értéke definiálja a kimeneti érték mezszélességét. Bármilyen szélességet is írunk el, a printf a konverzió eredményét nem csonkítja! A lehetséges szélesség specifikációk: Szélesség n Hatása a kimenetre A printf legalább n karaktert jelentet meg. Ha a kimeneti érték n karakternél kevesebb, akkor szóközzel n karakteresre párnázza (jobbról, ha a ­ jelzt megadták, máskülönben balról). Legalább n karakter jelenik meg ekkor is. Ha a kimeneti érték n­nél kevesebb karakterbl áll, akkor balról zérus feltöltés következik. A paraméter lista szolgáltatja a szélesség specifikációt, de ennek a paraméter listában meg kell elznie azt a paramétert, amire az egész formátumspecifikáció vonatkozik.

0n

*

A pontosság specifikáció mindig ponttal (.) kezddik. A szélességhez hasonlóan ez is megadható közvetlenül, vagy közvetve (*) a paraméter listában. Utóbbi esetben egy int típusú paraméternek meg kell elznie azt a paramétert a printf hívásban, amire az egész formátumspecifikáció vonatkozik. Megemlítjük, hogy a szélességet és a pontosságot is megadhatjuk egyszerre közvetetten. Ilyenkor a formátumspecifikációban *.* van. A printf hívás paraméter listájában két int típusú paraméter elzi meg (az els a szélesség, a második a pontosság) azt a paramétert, amire az egész formátumspecifikáció vonatkozik. Lássunk egy példát!
printf("%*.*f", 6, 2, 6.2);

A 6.2­et f típuskarakterrel kívánjuk konvertáltatni úgy, hogy a mezszélesség 6 és a pontosság 2 legyen.

280

SZABVÁNY, MAGAS SZINT BEMENET, KIMENET

A pontosság specifikációk a következk: Pontosság .* nincs megadva Hatása a kimenetre Lásd elbbre! Érvénybe lépnek a típuskaraktertl függ alapértelmezés szerinti értékek. Ezek: · · · 1 : d, i, o, u, x, X esetén, 6 : e, E, f típuskaraktereknél, minden szignifikáns számjegy g és G­nél,

· s típuskarakternél a teljes karakterlánc megy a kimenetre és · .0 a c típuskarakterre nincs hatással. · Az e, E, f típuskaraktereknél nem jelenik meg a tizedes pont. · A d, i, o, u, x, X esetén pedig az alapértelmezés szerinti pontosság lép érvénybe (1). Ha ilyenkor a kiírandó paraméter értéke ráadásul zérus is, akkor csak egyetlen szóköz jelenik meg. .n A printf legfeljebb n karaktert, vagy decimális helyiértéket jelentet meg. Ha a kimenet n-nél több karakterbl áll, akkor csonkul, vagy kerekíti a rutin a vonatkozó típuskaraktertl függen: · d, i, o, u, x és X esetén legalább n számjegy jelenik meg. Ha a kimenet n­nél kevesebb jegybl áll, akkor balról zérus feltöltés történik. Ha a kimeneti n­nél többjegy, akkor sem csonkul. · e, E, f­nél a printf n számjegyet jelentet meg a tizedes ponttól jobbra. Ha szükséges, a legalacsonyabb helyiértéken kerekítés lesz. · · g, G esetén legfeljebb n szignifikáns jegy jelenik meg. A c típuskarakterre nincs hatása.

· Az s típuskarakternél legfeljebb n karakter jelenik meg, azaz a hosszabb karakterlánc csonkul.

C programnyelv

281

Legvégül nézzük még a h, l és L méretmódosító karaktereket! A méretmódosítók annak a paraméternek a hosszát módosítják, melyre az egész formátumspecifikáció vonatkozik. · A d, i, o, u, x és X típuskarakterekkel kapcsolatban csak a h és az l méretmódosítók megengedettek. Jelentésük: h esetén a vonatkozó paramétert a printf tekintse short int­nek, l esetén pedig long int­nek. · Az e, E, f, g, és G típuskarakterekkel kapcsolatban csak az l és az L méretmódosítók megengedettek. Jelentésük: l esetén a vonatkozó paramétert a printf tekintse double­nek, L­nél pedig long double­nek. Jelentessük meg a 2003. március 2. dátumot ÉÉÉÉ­HH­NN alakban!
printf("%04d-%02d-%02d", 2003, 3, 2); printf("%.4d-%.2d-%.2d", 2003, 3, 2);

Mindkét hívás 2003­03­02­t szolgáltat. Szemléltessük a 0, a #, a + és a ­ jelzk hatását d, o, x, e és f típuskarakterek esetén!
/* PELDA33.C: A printf jelzinek szemléltetése néhány típuskarakterre. */ #include #include #define E 555 #define V 5.5 int main(void){ int i,j,k,m; char prefix[7], format[100], jelzok[]=" 0# + -", *tk[]={"6d", "6o", "8x", "10.2e", "10.2f"}; #define NJ (sizeof(jelzok)-2)*2 #define NTK sizeof(tk)/sizeof(tk[0]) printf("prefix 6d 6o 8x" " 10.2e 10.2f\n" "------+-------+-------+-----" "----+-----------+-----------+\n"); for(i=NJ-1; i>=0; --i){ strcpy(prefix, "%"); for(j=k=1; k
Az i 15­rl indul, s zérusig csökken egyesével, azaz eközben leírja az összes lehetséges négybites bitkombinációt. A k felvett értékei 1, 2, 4 és 8, s a jelzok tömb épp ezen index elemeiben találhatók meg a jelz karakterek.
strcpy(format, "%5s |"); for(m=0; m
282

SZABVÁNY, MAGAS SZINT BEMENET, KIMENET

strcat(format, prefix); strcat(format, tk[m]); strcat(format, " |"); } strcat(format, "\n"); printf(format, prefix, E, E, E, V, V); } return(0); }

A program futtatásakor megjelen kimenet:
prefix 6d 6o 8x 10.2e 10.2f ------+-------+-------+---------+-----------+-----------+ %0#+- |+555 |01053 |0x22b |+5.50e+000 |+5.50 | %#+- |+555 |01053 |0x22b |+5.50e+000 |+5.50 | %0+- |+555 |1053 |22b |+5.50e+000 |+5.50 | %+- |+555 |1053 |22b |+5.50e+000 |+5.50 | %0#- |555 |01053 |0x22b |5.50e+000 |5.50 | %#- |555 |01053 |0x22b |5.50e+000 |5.50 | %0- |555 |1053 |22b |5.50e+000 |5.50 | %- |555 |1053 |22b |5.50e+000 |5.50 | %0#+ |+00555 |001053 |0x00022b |+5.50e+000 |+000005.50 | %#+ | +555 | 01053 | 0x22b |+5.50e+000 | +5.50 | %0+ |+00555 |001053 |0000022b |+5.50e+000 |+000005.50 | %+ | +555 | 1053 | 22b |+5.50e+000 | +5.50 | %0# |000555 |001053 |0x00022b |05.50e+000 |0000005.50 | %# | 555 | 01053 | 0x22b | 5.50e+000 | 5.50 | %0 |000555 |001053 |0000022b |05.50e+000 |0000005.50 | % | 555 | 1053 | 22b | 5.50e+000 | 5.50 |

int sprintf(char *puffer, const char *format<, parameter, ...>); A függvény ugyanazt teszi, mint a printf, de a kimenetét nem az stdout­ ra készíti, hanem az els paraméterként megkapott karaktertömbbe '\0'­ lal lezárva. A vfprint, a vprintf és a vsprintf rutinokat már megemlítettük a Változó paraméterlista fejezetben! Megoldandó feladatok: Készítsen char * kozepre(char *mit, int szeles) függvényt, mely a saját helyén középre igazítja a mit karakterláncot szeles szélességben, és viszszaadja az eredmény lánc kezdcímét! A középre igazítást csak szeles­nél rövidebb láncok esetében kell elvégezni. A kétoldali párnázó karakter indulásként lehet szóköz, de lehessen ezt fordítási idben változtatni! Írjon szoftvert, mely igazított táblázatot hoz létre az alábbi tartalmú TABLA fájl
Szöveg Forint Egész Papadopulosz 111222.3 1456 Sodik_sor 2.2 345

C programnyelv szabvány bemenetkénti átirányításával. Az eredmény táblázat:

283

+------------------------+-------------------+------------+ | Szöveg | Forint | Egész | +------------------------+-------------------+------------+ | Papadopulosz | 111222.30Ft | 1456 | +------------------------+-------------------+------------+ | Sodik_sor | 2.20Ft | 345 | +------------------------+-------------------+------------+

, ahol az els oszlop balra-, a második jobbra-, s a harmadik középre igazított. A tábla egy sorának szerkezete:
| MEZO1| MEZO2Ft | MEZO3 |

, ahol MEZO1, MEZO2 és MEZO3 bruttó adatszélességek a mutatott módon. Fokozhatja még a feladatot úgy, hogy az adatokat lehessen billentyzetrl is megadni! 11.9 Egyéb függvények Csak lezárt fájlokkal foglalkozik a következ két függvény. A int remove(const char *fajlnev); törli az akár komplett úttal megadott azonosítójú fájlt. Sikeres esetben zérust, máskülönben -1-et szolgáltat a rutin. A int rename(const char *reginev, const char *ujnev); a reginev azonosítójú fájlt átnevezi ujnev­re. Ha az ujnev meghajtónevet is tartalmaz, akkor az nem térhet el attól, ahol a reginev azonosítójú fájl elhelyezkedik. Ha viszont az ujnev a fájl eredeti helyétl eltér utat tartalmaz, akkor az átnevezésen túl megtörténik a fájl átmozgatása is. A függvény egyik paramétere sem lehet globális fájlazonosító! Sikeres esetben zérust szolgáltat a rutin. A problémát a -1 visszaadott érték jelzi. FILE *tmpfile(void); A rutin "wb+" móddal ideiglenes fájlt hoz létre, melyet lezárásakor, vagy normális programbefejezdéskor automatikusan töröl a rendszer. A visszaadott érték az ideiglenes fájl FILE struktúrájára mutat, ill. NULL jön létrehozási probléma esetén. char *tmpnam(char s[L_tmpnam]);

284

SZABVÁNY, MAGAS SZINT BEMENET, KIMENET

tmpnam(NULL) módon hívva olyan fájlazonosítót kapunk, mely egyetlen létez fájl nevével sem egyezik. A szolgáltatott mutató bels, statikus karaktertömböt címez, ahol a fájlazonosító karakterlánc található. A fájlazonosító karakterlánc nem marad ott örökké, mert a következ tmpnam hívás felülírja. Nem NULL mutatóval hívva a rutin kimásolja a fájlazonosítót az s karaktertömbbe, s ezzel is tér vissza. Az s tömb legalább L_tmpnam méret kell, legyen. Többszöri hívással legalább TMP_MAX darab, különböz fájlazonosító generálása garantált. Vigyázat! A függvény fájlazonosítókat generál és nem fájlokat!

C programnyelv

285

12 IRODALOMJEGYZÉK
[1] Kiss J. ­ Raffai M. ­ Szijártó M. ­ Szörényi M.: A számítástechnika alapjai NOVADAT Bt., Gyr, 2001 [2] Marton L. ­ Pukler A. ­ Pusztai P.: Bevezetés a programozásba NOVADAT Bt., Gyr, 1993 [3] Marton László: Bevezetés a Pascal nyelv programozásba NOVADAT Bt., Gyr, 1998 [4] B. W. Kernighan ­ D. M. Ritchie: A C programozási nyelv Mszaki Könyvkiadó, Budapest, 1985 [5] B. W. Kernighan ­ D. M. Ritchie: A C programozási nyelv, az ANSI szerint szabványosított változat Mszaki Könyvkiadó, Budapest, 1996 [6] Benk Tiborné ­ Benk László ­ Tóth Bertalan: Programozzunk C nyelven ComputerBooks, Budapest, 1999 [7] Benk Tiborné ­ Urbán Zoltán: IBM PC programozása TURBO C nyelven 2.0 BME Mérnöktovábbképz Intézet, Budapest, 1989

286

IRODALOMJEGYZÉK

13 TARTALOMJEGYZÉK
BEVEZETÉS ............................................................................................................ 2 JELÖLÉSEK............................................................................................................. 4 ALAPISMERETEK.................................................................................................. 5 3.1 Forrásprogram................................................................................................... 5 3.2 Fordítás ............................................................................................................. 5 3.3 Kapcsoló­szerkesztés (link).............................................................................. 9 3.4 Futtatás............................................................................................................ 10 3.5 Táblázat készítése ........................................................................................... 10 3.6 Bemenet, kimenet............................................................................................ 19 3.7 Tömbök ........................................................................................................... 26 3.8 Függvények..................................................................................................... 29 3.9 Prodzsekt......................................................................................................... 32 3.10 Karaktertömb és karakterlánc ......................................................................... 35 3.11 Lokális, globális és bels, küls változók ....................................................... 39 3.12 Inicializálás ..................................................................................................... 44 4 TÍPUSOK ÉS KONSTANSOK .............................................................................. 47 4.1 Elválasztó-jel................................................................................................... 48 4.2 Azonosító ........................................................................................................ 49 4.3 Típusok és konstansok a nyelvben .................................................................. 50 4.3.1 Egész típusok és konstansok ................................................................... 53 4.3.2 Felsorolás (enum) típus és konstans........................................................ 57 4.3.3 Valós típusok és konstans ....................................................................... 60 4.3.4 Karakter típus és konstans....................................................................... 62 4.4 Karakterlánc (string literal):............................................................................ 67 4.5 Deklaráció ....................................................................................................... 70 4.5.1 Elemi típusdefiníció (typedef)................................................................. 74 5 MVELETEK ÉS KIFEJEZÉSEK ........................................................................ 76 5.1 Aritmetikai mveletek (+, -, *, / és %)............................................................ 78 5.1.1 Multiplikatív operátorok (*, / és %) ........................................................ 79 5.1.2 Additív operátorok (+ és -)...................................................................... 82 5.1.3 Matematikai függvények......................................................................... 82 5.2 Reláció operátorok ( >, >=, <, <=, == és !=)................................................... 84 5.3 Logikai mveletek ( !, && és ||)...................................................................... 85 5.4 Implicit típuskonverzió és egész­elléptetés .................................................. 87 5.5 Típusmódosító szerkezet................................................................................. 89 5.6 sizeof operátor................................................................................................. 90 5.7 Inkrementálás (++), dekrementálás (--) és mellékhatás .................................. 90 5.8 Bit szint operátorok ( ~, <<, >>, &, ^ és |) .................................................... 92 5.9 Feltételes kifejezés ( ? :).................................................................................. 96 5.10 Hozzárendelés operátorok............................................................................... 97 5.11 Hozzárendelési konverzió ............................................................................... 99 5.12 Vessz operátor............................................................................................. 101 5.13 Mveletek prioritása ..................................................................................... 102 6 UTASÍTÁSOK ..................................................................................................... 106 6.1 Összetett utasítás ........................................................................................... 106 6.2 Címkézett utasítás ......................................................................................... 107 6.3 Kifejezés utasítás........................................................................................... 107 1 2 3

C programnyelv

287

6.4 Szelekciós utasítások..................................................................................... 108 6.5 Iterációs utasítások........................................................................................ 111 6.6 Ugró utasítások ............................................................................................. 116 7 ELFELDOLGOZÓ (PREPROCESSOR)........................................................... 119 7.1 Üres (null) direktíva ...................................................................................... 120 7.2 #include direktíva.......................................................................................... 121 7.3 Egyszer #define makró................................................................................ 121 7.4 Elredefiniált makrók ................................................................................... 123 7.5 #undef direktíva ............................................................................................ 123 7.6 Paraméteres #define direktíva ....................................................................... 124 7.7 Karaktervizsgáló függvények (makrók)........................................................ 125 7.8 Feltételes fordítás .......................................................................................... 128 7.8.1 A defined operátor ................................................................................ 130 7.8.2 Az #ifdef és az #ifndef direktívák ......................................................... 130 7.9 #line sorvezérl direktíva.............................................................................. 131 error direktíva ........................................................................................................... 132 pragma direktívák ..................................................................................................... 132 8 OBJEKTUMOK ÉS FÜGGVÉNYEK.................................................................. 133 8.1 Objektumok attribútumai .............................................................................. 133 8.1.1 Tárolási osztályok ................................................................................. 134 8.1.2 Élettartam (lifetime, duration)............................................................... 140 8.1.3 Hatáskör (scope) és láthatóság (visibility) ............................................ 142 8.1.4 Kapcsolódás (linkage)........................................................................... 144 8.2 Függvények................................................................................................... 146 8.2.1 Függvénydefiníció ................................................................................ 147 8.2.2 Függvény prototípusok.......................................................................... 152 8.2.3 Függvények hívása és paraméterkonverziók......................................... 155 8.2.4 Nem szabványos módosítók, hívási konvenció..................................... 157 8.2.5 Rekurzív függvényhívás........................................................................ 159 9 MUTATÓK........................................................................................................... 162 9.1 Mutatódeklarációk ........................................................................................ 162 9.1.1 Cím operátor (&)................................................................................... 163 9.1.2 Indirekció operátor (*) .......................................................................... 164 9.1.3 void mutató ........................................................................................... 165 9.1.4 Statikus és lokális címek ....................................................................... 166 9.1.5 Mutatódeklarátorok ............................................................................... 166 9.1.6 Konstans mutató.................................................................................... 167 9.2 Mutatók és függvényparaméterek ................................................................. 168 9.3 Tömbök és mutatók....................................................................................... 169 9.3.1 Index operátor ....................................................................................... 171 9.3.2 Tömbdeklarátor és nem teljes típusú tömb............................................ 173 9.4 Mutatóaritmetika és konverzió...................................................................... 175 9.4.1 Összeadás, kivonás, inkrementálás és dekrementálás ........................... 175 9.4.2 Relációk ................................................................................................ 176 9.4.3 Feltételes kifejezés ................................................................................ 177 9.4.4 Konverzió.............................................................................................. 178 9.5 Karaktermutatók............................................................................................ 179 9.5.1 Karakterlánc kezel függvények........................................................... 179 9.5.2 Változó paraméterlista .......................................................................... 185 9.6 Mutatótömbök............................................................................................... 187

288

IRODALOMJEGYZÉK

9.7 Többdimenziós tömbök................................................................................. 189 9.7.1 Véletlenszám generátor......................................................................... 191 9.7.2 Dinamikus memóriakezelés .................................................................. 193 9.8 Tömbök, mint függvényparaméterek ............................................................ 198 9.9 Parancssori paraméterek ............................................................................... 200 9.9.1 Programbefejezés .................................................................................. 203 9.10 Függvény (kód) mutatók ............................................................................... 205 9.10.1 atexit függvény...................................................................................... 207 9.10.2 Típusnév................................................................................................ 210 9.11 Típusdefiníció (typedef)................................................................................ 211 9.12 Ellenrzött bemenet ...................................................................................... 213 10 STRUKTÚRÁK ÉS UNIÓK ............................................................................ 218 10.1 Struktúradeklaráció ....................................................................................... 219 10.1.1 Típusdefiníció ....................................................................................... 222 10.2 Struktúratag deklarációk ............................................................................... 222 10.3 Struktúrák inicializálása ................................................................................ 224 10.4 Struktúratagok elérése................................................................................... 225 10.5 Struktúrák és függvények.............................................................................. 230 10.6 Önhivatkozó struktúrák és dinamikus adatszerkezetek................................. 236 10.7 Struktúra tárillesztése.................................................................................... 243 10.8 UNIÓK.......................................................................................................... 244 10.8.1 Uniódeklarációk .................................................................................... 246 10.9 Bitmezk (bit fields) ..................................................................................... 247 10.10 Balérték ­ jobbérték .................................................................................. 250 10.11 Névterületek .............................................................................................. 251 11 MAGAS SZINT BEMENET, KIMENET ..................................................... 254 11.1 Folyamok megnyitása ................................................................................... 254 11.2 Folyamok pufferezése ................................................................................... 256 11.3 Pozícionálás a folyamokban.......................................................................... 258 11.4 Bemeneti mveletek...................................................................................... 260 11.5 Kimeneti mveletek ...................................................................................... 262 11.6 Folyamok lezárása......................................................................................... 263 11.7 Hibakezelés ................................................................................................... 263 11.8 Elre definiált folyamok ............................................................................... 267 11.8.1 Bemenet az stdin-rl ............................................................................. 269 11.8.2 Kimenet az stdout-ra ............................................................................. 274 11.9 Egyéb függvények......................................................................................... 283 12 IRODALOMJEGYZÉK.................................................................................... 285

Hasonló témájú dokumentumok
C
- 2009-02-01 19:30:30
- 2009-01-06 14:37:36
- 2007-11-28 17:41:12
- 2009-01-06 14:56:36
- 2009-05-10 17:24:08
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! - Küldj üzenetet a szakod vagy évfolyamod összes hallgatója számára. Hasznos lehet ha választ keresel egy kérdésre, vagy mindenkivel tudatni akarsz egy információt. Ehhez használd az Üzeneteken belül a baloldali dobozban az Üzenet írását.

Cimkefelhő

2009. május 21. 4. 8. előadás adó ady alkotmánytörténet alkszámtech alternatív energiaforrások andorka antropológiatörténet architektúra b1 biofizika citrátkör civilisztika deriválás dinamika etika etnográfia fejlődés folyami duzzasztómű forgó mozgás formanyomtatvány gamf fizika vizsga gazdasági jog genetika géptan hull jogfosztás karrier képlékenyalakítás kormány közigtöri marketing mb mechanika 2 nemzeti kisebbség órai dia prax pricing strategies reklámjog sejttan számítógép architektúrák számvitel ii. szerződés szocializáció tankönyv társadalom történet tőkeelmélet vám