Adatbevitel és adatkivitel
6. FEJEZET Tartalom 8. FEJEZET

7. FEJEZET:

Adatbevitel és adatkivitel

Az adatbeviteli és adatkiviteli szolgáltatás nem része a C nyelvnek, így idáig nem fordítottunk rá nagy figyelmet. Nyilvánvaló viszont, hogy a programok a környezettel a korábban bemutatottnál sokkal bonyolultabb kapcsolatban is lehetnek. Ezen kapcsolat kialakítása érdekében ebben a fejezetben leírjuk a standard könyvtárat, ami függvények gyűjteménye. Ezek a függvények a C nyelvű programok adatbeviteli és adatkiviteli, karaktersorozat-kezelési, tárkezelési, matematikai műveletekkel kapcsolatos és más egyéb szolgáltatásait látják el.

Az ANSI szabvány ezeket a könyvtári funkciókat pontosan definiálja, így ezek bármely C nyelvet használó számítógép és operációs rendszer számára kompatibilis formában léteznek. Azok a programok, amelyek az operációs rendszerrel való kölcsönhatásukat a standard könyvtáron keresztül bonyolítják, minden változtatás nélkül átvihetők az egyik számítógépről a másikra.

A könyvtári függvények tulajdonságait több mint egy tucat header állomány specifikálja, amelyek közül néhánnyal (<stdio.h>, <string.h>, <ctype.h>) már találkoztunk. Most nem a teljes könyvtárat fogjuk ismertetni, hanem számos érdekes C nyelvű programot írunk a könyvtári függvények felhasználásával. Magának a könyvtárnak a részletesebb leírása a B. Függelékben található.

7.1. A standard adatbevitel és adatkivitel

Amint azt az 1. fejezetben már elmondtuk, a könyvtár adatátvitelt kezelő része a szöveges adatbevitelre és adatkivitelre kialakított egyszerű modell alapján működik. A modell szerint a szövegáram egymást követő sorokból áll és minden sor egy újsorkarakterrel zárul. Ha a rendszer nem működik másképpen, akkor a könyvtári eljárás feladata annak eldöntése, hogy mi a teendő az újsor-karakter megjelenésekor. Például a könyvtári eljárás a bemenetről érkező kocsivissza- és soremelés-karaktereket egy újsor-karakterré alakítja, majd kiírás esetén elvégzi a visszaalakítást.

A legegyszerűbb adatbeviteli mechanizmus, hogy egy időben egy karaktert olvasunk be a standard bemeneti eszközről (szokásos módon a billentyűzetről) a getchar függvénnyel:

int getchar(void)
A getchar függvény minden hívásakor visszatér a következő karakterrel vagy az EOF jellel, ha az állomány végét érzékelte. Az EOF szimbolikus állandó az <stdio.h> headerben van definiálva. Az EOF értéke általában -1, de az ellenőrzéseknél inkább a szimbolikus állandót használjuk, hogy a program függetlenné váljék az adott számértéktől.

A legtöbb környezetben egy állomány helyettesítheti a billentyűzetet a megállapodás szerinti < átirányítási jelet használva. Ha a prog program a getchar függvényt használja, akkor a

prog <allomanyban
parancssor hatására a rendszer a karaktereket az allomanyban nevű adatállományból fogja beolvasni. A bemeneti eszköz átkapcsolása úgy történik, hogy a prog maga nem érzékeli a változást, az "<allomanyban" karaktersorozat nem kerül be az argv parancssor-argumentumba. A bemenet átirányítása szintén nem érzékelhető a prog számára, ha a bemeneti adatait egy másik programtól az ún pipeing mechanizmussal (láncolással) kapja. Sok rendszerben ez a
masprog | prog
parancssorral kérhető. Ennek hatására a masprog standard kimenete, mintegy csővezetéken keresztül rákapcsolódik a prog bemenetére. A standard kimenetet az
int putchar(int)
függvény állítja elő. A putchar(c) a c karaktert adja a standard kimeneti eszközre, ami alapfeltételezés szerint a képernyő. A putchar függvény a hívása után a kiírt karakterrel vagy ha valamilyen hiba fordult elő, akkor az EOF jelzéssel tér vissza. A standard kimenet is átirányítható egy adatállományba a >állománynév parancskiegészítéssel. Ha a prog használja a putchar függvényt, akkor az átirányítás a
prog >kiallomany
parancssorral történhet, és ennek hatására a standard kimenet helyett a kiallomany nevű adatállományba íródik a kimenet. A pipeing mechanizmus szintén megvalósítható a
prog | masprog
parancssorral, ami a prog standard kimenetét a masprog standard bemenetére irányítja.

A printf függvény szintén a standard kimenetet használja. A putchar és printf függvények hívásai vegyesen is előfordulhatnak és a kimenet a hívások sorrendjében jön létre.

Minden forrásállományban, amely hivatkozik az adatbeviteli-adatkiviteli könyvtár függvényeire, kell hogy szerepeljen az

#include <stdio.h>
sor az első függvényhivatkozás előtt. Amikor a rendszer megtalálja a hegyes zárójelek között a header nevét, akkor azt standard könyvtárban (pl. UNIX operációs rendszer esetén az /usr/include könyvtárban) kezdi keresni.

Nagyon sok program csak egyetlen bemeneti adatáramot használ és csak egyetlen kimeneti adatáramot hoz létre. Az ilyen programok adatbeviteli és adatkiviteli feladatainak ellátására a getchar, putchar és printf függvények teljesen elegendőek, és ez az induláshoz szintén elegendő. Ez különösen igaz, ha kihasználjuk az átirányítás lehetőségét, amellyel az egyik program kimenetét a másik bemenetéhez kapcsoljuk. Példaként vizsgáljuk meg a karakteres adatbevitelt és adatkivitelt használó lower programot, amely a bemenetére adott szöveget kisbetűs szöveggé alakítja.

#include <stdio.h>
#include <ctype.h>

main() /* a bemenetet kisbetűssé alakítja */
{
   int c;
   while ( (c = getchar()) != EOF)
      putchar(tolower(c));
   return 0; 
}
A tolower függvény a <ctype.h> headerben van definiálva és a nagybetűket kisbetűkké alakítja, más karakterekkel érintetlenül visszatér a hívó függvénybe. Mint korábban már említettük, az <stdio.h> getchar és putchar, valamint a <ctype.h> tolower „függvényei” gyakran makróként vannak megvalósítva, mivel így az egy karakterre eső műveletszám (és így a futási idő is) csökkenthető. Azt, hogy ez hogyan valósítható meg, a 8.5. pontban fogjuk bemutatni. Függetlenül attól, hogy egy adott gépen ezek a függvények hogyan vannak megvalósítva, a program használja azokat és semmiféle ismerettel nem kell rendelkeznie a karakterkészletről.

7.1. gyakorlat. Írjunk programot, amely a hívásakor az argv[0]-ban elhelyezett paramétertől függően a nagybetűket kisbetűvé vagy a kisbetűket nagybetűvé alakítja!

7.2. A formátumozott adatkivitel – a printf függvény

A printf kimeneti függvény a gépen belüli értékeket karakterekké alakítja. A printf függvényt már a korábbi fejezetekben is használtuk és ott megadtuk a tipikus alkalmazásokra vonatkozó, de korántsem teljes leírását. A teljes leírás a B. Függelékben található. A függvény alakja:
int printf(char *format, arg1, arg2, ...)
A printf függvény a format vezérlése alatt konvertálja, formátumozza és a standard kimenetre írja az argumentumai értékét, majd visszatéréskor megadja a kiírt karakterek számát.

A format karaktersorozata kétféle objektumot tartalmaz: közönséges (nyomtatható) karaktereket, amelyek közvetlenül átmásolódnak a kimeneti adatáramba és konverziós specifikációkat, amelyek mindegyike a printf soron következő argumentumának konverzióját és kiírását eredményezi. Mindegyik konverziós specifikáció a % jellel kezdődik és egy konverziós karakterrel végződik. A % jel és a konverziós karakter között sorrendben a következők lehetnek:

A konverziós karaktereket a 7.1. táblázat tartalmazza. Ha a % jel utáni karakter nem konverziós specifikáció akkor a printf viselkedése definiálatlan.


7.1. táblázat. A printf függvény konverziós karakterei
A konverziós karakter Az argumentum típusa A nyomtatás módja
d, i int decimális szám
o int előjel nélküli oktális szám vezető nullák nélkül)
x, X int előjel nélküli hexadecimális szám (a vezető 0x vagy 0X nélkül), a 10...15 jelzése az abcdef vagy ABCDEF karakterekkel
u int előjel nélküli decimális szám
c int egyetlen karakter
s char* karaktersorozatból karaktereket nyomtat a '\0' végjelzésig vagy a pontossággal megadott darabszámig
f double [-]m.dddddd alakú decimális szám, ahol d számjegyeinek számát a pontosság adja meg (alapfeltételezés szerint d=6)
e, E double [-]m.dddddde xx vagy [-]m.ddddddE xx alakú decimális szám, ahol d számjegyeinek számát a pontosság adja meg (alapfeltételezés szerint d=6
g, G double %e vagy %E alakú kiírást használ, ha a kitevő < -4 vagy >=  pontosság, különben a %f alakú kiírást használja. A tizedespont és az utána következő értéktelen nullák nem íródnak ki
p void * mutató a géptől függő kiírási formában
n int * a printf függvény aktuális hívásakor kiírt karakterek száma beíródik az argumentumba. Az argumentum nem konvertálódik
% nincs konvertálandó argumentum egy % jelet ír ki


A szélesség vagy pontosság a * jellel is megadható, és ebben az esetben az érték a következő argumentum (amely kötelezően int típusú kell, hogy legyen) konverziójakor jön létre. Például ha az s karaktersorozatból legfeljebb max számú karaktert akarunk kiírni, akkor ez a
printf("%.*s", max, s);
utasítással érhető el.

A formátumkonverziók többségét a korábbi fejezetekben már bemutattuk, az egyetlen kivételt a karaktersorozatok kinyomtatásánál megadott pontosság hatásának elemzése képezi. A következő példasoron bemutatjuk a pontosság előírásának hatását a „Halló mindenki!” 15 karakteres karaktersorozat kiírására. Az egyes mezők két szélén kettőspontokat íratunk ki, hogy jobban kiemeljük a mezőt.

:%s:     :Halló mindenki!:
:%10s:   :Halló mindenki!:
:%.10s:              :Halló mind:
:%-10s:              :Halló mindenki!:
:%.20s:              :Halló mindenki!:
:%-18s:              :Halló mindenki!   :
:%18.10s:            :     Halló mind:
:%-18.10s:           :Halló mind        :
Figyelem! A printf függvény az első argumentumát használja annak eldöntésére, hogy még hány argumentum következik és azoknak mi a típusa. Programhiba keletkezik és hibás kiírást kapunk, ha nincs elegendő argumentum vagy azok nem a megfelelő típusúak. Ezt jól szemlélteti az alábbi két printf hívás összehasonlítása.
printf(s); /* hibás, ha s % jelet is tartalmaz */
printf("%s", s); /* ez így biztonságos */
A sprintf függvény ugyanazt a konverziót hajtja végre, mint a printf, de a kimenetet egy karaktersorozatban tárolja. A függvény:
int sprintf(char *string, char *format, arg1, arg2, ...)
A sprintf függvény először a format formátummegadás szerint formatálja az arg1, arg2 stb. argumentumokat, majd az eredményt a standard kimenet helyett a string karaktersorozatba helyezi. A string karaktersorozatnak elegendően nagynak kell lenni ahhoz, hogy az eredményt tárolni tudja. Az sprintf függvényt főleg arra használhatjuk, hogy részekből egy átmeneti tárolóban állítsuk össze a kiírandó információt, majd amikor minden rész a rendelkezésünkre áll, akkor ezt az átmeneti változót a printf függvénnyel kiíratjuk.

7.2. gyakorlat. Írjunk programot, amely a tetszőleges bemeneti szöveget értelmes módon írja ki! A minimális igény, hogy a nem nyomtatható karaktereket a helyi szokásoknak megfelelően oktális vagy hexadecimális számként írjuk ki és a túl hosszú sorokat tördeljük rövidebb sorokra!

7.3. A változó hosszúságú argumentumlisták kezelése

Ebben a pontban megírjuk a printf függvény minimális igényeket kielégítő változatát, hogy bemutassuk, hogyan lehet olyan hordozható függvényeket írni, amelyek képesek egy változó hosszúságú argumentumlista feldolgozására. Mivel főleg az argumentumok feldolgozására helyeztük a hangsúlyt, az általunk írt minprintf függvény csak a formátumot leíró karaktersorozatot és az argumentumokat dolgozza fel, a formátumkonverziót a valódi printf függvény hívásával valósítja meg. A printf deklarációja:
int printf(char *fmt, ...)
ahol a ... azt jelzi, hogy az argumentumok száma és típusa változhat. A deklarációban a ... csak az argumentumlista végén jelenhet meg. Az általunk írt minprintf függvény deklarációja
void minprintf(char *fmt, ...)
mivel mi nem adjuk vissza a kiírt karakterek számát, mint a printf.

Egy kicsit „trükkös”, ahogyan a minprintf végigmegy az argumentumlistán, miközben az egyetlen nevet sem tartalmaz. A <stdarg.h> headerben néhány makródefiníció található, amelyek lehetővé teszik az argumentumlistán való lépkedést. Ennek a headernek a konkrét kialakítása számítógéptől függ, de a makrók szoftver-interfésze egységes.

A va_list típust fogjuk használni a sorjában következő egyes argumentumokra hivatkozó változók deklarálására. A minprintf függvényben ezt a változót am-nek nevezzük, az „argumentummutató” kifejezés alapján. A va_start makró úgy inicializálja az am változót, hogy az az első név nélküli argumentumra mutasson. Ezért am használata előtt a va_start makrót egyszer végre kell hajtatni. A listában legalább egy névvel ellátott argumentumnak kell lenni és ezt az utolsó, névvel ellátott argumentumot használja a va_start a meg nem nevezett argumentumok listáján való elinduláshoz.

A va_arg minden egyes hívása után egy argumentummal tér vissza és az am mutatót tovább lépteti a következő argumentumra. A va_arg visszatérésekor az argumentum értéke olyan típusú lesz, mint amit hívásakor megadtunk. Az argumentumlista feldolgozási folyamatát a minprintf függvényt záró return utasítás kiadása előtt a va_end makró hívásával kell lezárni. (Az argumentumlistát feldolgozó makrók leírása a B. Függelék 7. pontjában található.)

Ezekkel a jellemzőkkel kialakított minprintf függvény:

#include <stdio.h>
#include <stdarg.h>
#include <ctype.h>

/* minprintf: változó hosszúságú argumentumlistát
kezelő, egyszerűsített printf függvény */
void minprintf(char *fmt, ...)
{
   va_list am; /* sorjában az egyes név nélküli
         argumentumokra mutat */
   char *p, *sert;
   int iert;
   double dert;

   va_start(am, fmt); /* hatására am az első név
         nélküli argumentumra fog mutatni */
   for (p = fmt; *p; p++) {
      if (*p != '%') {
         putchar(*p);
         continue;
      }
   switch (*++p) {
      case 'd':
         iert = va_arg(am, int);
         printf("%d", iert);
         break;
      case 'f':
         dert = va_arg(am, double);
         printf ("%f", dert);
         break;
      case 's':
         for (sert = va_arg(am, char *); *sert; sert++)
            putchar(*sert);
         break;
      default:
            putchar(*p);
            break;
      }
   }
   va_end(am); /* a listafeldolgozás lezárása */
}

7.3. gyakorlat. Egészítsük ki a minprintf programot újabb, printf függvényben megengedett lehetőségekkel!

7.4. Formátumozott adatbevitel – a scanf függvény

Az adatbevitelt végző scanf függvény a printf analógiája, és többségében azonos (de ellentétes irányú) adatkonverziókat képes elvégezni. Általános alakja:
int scanf(char *format, ...)
A scanf függvény a standard bemenetről karaktereket olvas és a format-ban megadott specifikációk szerint értelmezi azokat, majd az eredményt eltárolja a további argumentumokban. A formátumot leíró argumentumot hamarosan részleteiben is tárgyaljuk, a többi argumentumnak viszont kötelezően mutatónak kell lenni, ami arra a helyre mutat, ahová a konvertált értéket helyezzük. Csakúgy, ahogyan a printf esetén, itt is csak a scanf legfontosabb jellemzőit írjuk le és nem megyünk bele a részletekbe.

A scanf függvény működése befejeződik, ha feldolgozta a formátumot leíró karaktersorozatot vagy hibát érzékel (az aktuális bemenet nem illeszkedik a vezérlő specifikációhoz). A függvény visszatérési értéke a sikeresen illesztett (konvertált) és argumentumokban elhelyezett adatok száma. Ebből eldönthető, hogy a scanf hány bemeneti tételt talált. Az adatállomány végének elérésekor a visszaadott érték EOF. Ügyeljünk arra, hogy ez egy nullától különböző érték és azt jelenti, hogy a következő bemeneti karakter nem illeszkedik a formátumot leíró karaktersorozat első specifikációjához! Ha a scanf függvényt többször egymás után hívjuk, akkor a következő hívás esetén a keresés közvetlenül az utoljára visszaadott és már konvertált karakter után folytatódik.

A scanf függvénynek létezik egy sscanf változata, ami a sprintf-hez hasonló és a standard bemenet helyett egy karaktersorozatból olvas:

int sscanf(char *string, char *format, arg1, arg2, ...)
A sscanf függvény a format-ban megadott formátum szerint kiolvassa a string karaktersorozatot és a konvertált értékeket elhelyezi az arg1, arg2 stb. argumentumokban, Természetesen ezek az argumentumok is mutatók.

A formátumot leíró karaktersorozat konverziós specifikációkat tartalmaz, amelyeket a bemenet átalakításának vezérlésére használunk. A formátumvezérlő karaktersorozat a következő karaktereket tartalmazhatja:

A konverziós specifikáció irányítja a következő bemeneti mező átalakítását. Normális esetben az eredmény a megfelelő argumentummal (mint mutatóval) címzett vátlozóba kerül. Ha a hozzárendelés-elnyomó * karaktert alkalmaztuk, akkor a scanf a bemeneti mezőt átlépi és nem történik meg az érték változóhoz rendelése. Egy bemeneti mező alatt a nem üres karakterekből álló karaktersorozatot értjük, ami a következő üres karakterig tart, vagy addig, amíg a mezőszélesség (ha megadtuk) el nem fogy. Ebből következik, hogy a scanf átmegy a sorhatárokon is a bemeneti adat keresése közben, mivel az újsor-karakter is üres karakternek számít. (Üres karakter a szóköz, a tabulátor, a kocsi-vissza, a soremelés, a függőleges tabulátor és a lapdobás.)

A konverziós karakter adja meg a bemeneti mező értelmezését. A neki megfelelő argumentumnak mutatónak kell lenni, ahogyan ezt a C nyelv érték szerinti hívása megköveteli. A konverziós karaktereket a 7.2. táblázat tartalmazza.

A d, i, o, u és x konverziós karakterek előtt álló h azt jelzi, hogy a mutató az argumentumlistában szereplő int típus short változatára mutat, és hasonlóan az l a long változatra utal. Az e, f és g konverziós karakterek előtt megjelenő l arra utal, hogy a float típusú argumentum double változatú.



7.2. táblázat. A scanf függvény konverziós karakterei
A konverziós karakter Az argumentum típusa A beolvasott adat
d int * decimális egész
i int * egész szám, ami lehet oktális (vezető nullákkal) vagy hexadecimális (vezető 0x vagy 0X karakterekkel)
o int * oktális egész szám (vezető nullákkal vagy azok nélkül)
u unsigned int* előjel nélküli decimális egész szám
x int * hexadecimális egész szám (a vezető 0x, ill. 0X karakterekkel vagy azok nélkül)
c char * karakterek. A következő bemeneti karakterek (alapfeltételezés szerint 1) elhelyezése a kijelölt mezőben. Az üres helyek átlépését (mint normális esetet) elnyomja, ha a következő nem üres karaktert akarjuk beolvastatni, akkor a %1s specifikációt kell használni
s char * karaktersorozat (aposztrófok nélkül). A char * mutató egy elegendően nagy karaktersorozatra mutat és a záró '\0' jelzést a beolvasás után automatikusan elhelyezi
e, f, g float * lebegőpontos szám, opcionális előjellel opcionális tizedesponttal és opcionális kitevővel
p void * mutató, olyan formában, ahogyan azt a printf("%p") kiírta
n int * az aktuális scanf hívással beolvasott karakterek száma beíródik az argumentumba. Nem történik adatbeolvasás, a konvertált tételek száma nem nő
[...] char * a bemeneti karakteráramból beolvassa a zárójelek közötti karakterekkel (illeszkedési halmazzal) megegyező karakterekből álló leghosszabb nem üres karaktersorozatot és lezárja a '\0' végjellel. A []...] formában megadott halmaz esetén a ] karakter a halmaz része lesz
[^...] char * az illeszkedési halmazzal nem megegyező karakterekből álló karaktersorozat beolvasása és '\0' végjellel történő lezárása. A [^]...] formában megadott halmaz esetén a ] karakter a halmaz része lesz
% nincs hozzárendelés % jel mint karakteres állandó



A scanf használatának bemutatását kezdjük a 4. fejezetben ismertetett egyszerű kalkulátor programjának módosításával! A programban a bemeneti adatok átalakítását oldjuk meg a scanf függvénnyel.

#include <stdio.h>
main() /* egyszerű kalkulátor */
{
   double sum, v;

   sum = 0;
   while (scanf("%lf", &v) == 1)
      printf ("\t%.2f\n", sum += v);
   return 0;
}
A következő példában tegyük fel, hogy
1994 december 25
alakban írt dátumot akarunk beolvasni. Ez a scanf függvénnyel
int nap, ev;
char honapnev[20];
scanf("%d %s %d", &ev, honapnev, &nap);
formában valósítható meg a beolvasás. Vegyük észre, hogy a honapnev előtt nem írtuk ki az & jelet, mivel egy tömbnév mindig mutató!

A scanf formátumot leíró karaktersorozatában karakteres állandók (literálisok) is megjelenhetnek, de azoknak illeszkedni kell a bemenetről érkező ugyanolyan karakterekhez. Ha pl. a dátumot éé/hh/nn alakban akarjuk beolvastatni a scanf függvénnyel, akkor az

int nap, honap, ev;
scanf("%d/%d/%d", &ev, &honap, &nap);
formátumleírást kell alkalmazni.

A scanf függvény beolvasáskor kiszűri a formátumot megadó karaktersorozatban lévő szóközöket és tabulátorokat, sőt átlépi az üreshely-karaktereket (szóköz, tabulátor, új sor stb.) a beolvasott adatáramban is a beolvasandó érték keresése közben. Ha a bemeneti adatáram nem rögzített formátumú, akkor célszerű egyszerre egy egész adatsort beolvasni és az sscanf függvénnyel részleteiben elővenni és feldolgozni. Ha például olyan sort akarunk beolvasni, ami a korábbi két dátumírási mód bármelyikével írt dátumot tartalmaz, akkor ezt úgy tehetjük meg, hogy a getline függvénnyel beolvassuk a teljes sort, majd az sscanf függvénnyel feldolgozzuk.

while (getline(sor, sizeof(sor)) > 0) {
   if (sscanf (sor, "%d %s %d", &ev, honapnev, &nap) == 3)
      printf ("érvényes: %s\n", sor);
         /* 1994 december 25 alakú dátum */
   else if (sscanf(sor, "%d/%d/%d", &ev, &honap, &nap) == 3)
      printf("érvényes: %s\n", sor);
         /* éé/hh/nn alakú dátum */
   else
      printf("érvénytelen: %s\n", sor);
         /* érvénytelen alakú dátum */
}
A scanf és más adatbeviteli függvények hívásai egymással keverhetők. Bármelyik adatbeviteli függvény következő hívásakor a beolvasás az első, scanf által már be nem olvasott karakterrel kezdődik.

Befejezésül még egyszer kihangsúlyozzuk, hogy a scanf és sscanf függvények argumentumainak mutatóknak kell lennie! Nagyon gyakori hiba, hogy

scanf ("%d", n); /* HIBÁS!!! */
alakban írjuk a függvényhívást a
scanf("%d", &n); /* Helyes! */
helyett. Ez a hiba általában nem derül ki a program fordítása közben.

7.4. gyakorlat. Írjuk meg a scanf függvény egyszerűsített változatát az előző pontban látott minprintf mintájára!

7.5. gyakorlat. Írjuk meg a 4. fejezetben ismertetett postfix adatbeírási formátumú kalkulátorprogram új változatát, amely a bemeneti adatok és számok átalakítását a scanf és/vagy sscanf függvénnyel valósítja meg!

7.5. Hozzáférés adatállományokhoz

Az eddigi példaprogramok mindegyike a standard bemenetről olvasta az adatokat és a standard kimenetre írta az eredményt. A standard bemenetet és kimenetet a helyi operációs rendszer automatikusan hozzárendeli a programhoz.

A következőkben egy olyan programot mutatunk be, amely egy adatállományhoz fér hozzá, amit nem az operációs rendszer rendelt átirányítással a programhoz. Az adatállományokhoz való hozzáférés szükségességét és megvalósítási lehetőségét illusztráló cat program megnevezett adatállományok halmazát gyűjti egybe (konkatenálja az állományokat) és az eredményt a standard kimenetre írja. A cat fő alkalmazási területe, hogy állományokat gyűjtsön egybe és nyomtassa ki azokat, vagy az önálló, név szerinti állomány-hozzáférésre nem felkészített programok bemeneti információit gyűjtse be. Például a

cat x.c y.c
parancs a standard kimenetre írja az x.c és y.c nevű állományok (és csak ezen állományok) tartalmát.

A program kialakításánál a fő kérdés, hogy hogyan érhetjük el a megnevezett állományok beolvasását, vagyis azt, hogy a felhasználó által szabadon választott külső állománynevet az adatbeolvasó utasításhoz rendeljük.

A szabály rendkívül egyszerű! Mielőtt az állományból olvasnánk vagy abba írnánk, az állományt a fopen könyvtári függvénnyel meg kell nyitni. A fopen veszi a külső állománynevet (mint pl. x.c vagy y.c), mindenféle belső adminisztrációt végez, felveszi a kapcsolatot az operációs rendszerrel (amelynek részleteivel nem kell törődnünk), majd egy mutatót ad vissza, ami ezt követően az állomány olvasásánál vagy írásánál használható.

Ez a mutató, amit állomány-mutatónak (fájlpointernek) neveznek, egy struktúrát címez, ami az állományra vonatkozó információkat (a puffer helye; az aktuális karakterpozíció a pufferban; annak jelzése, hogy az állományt írásra vagy olvasásra vesszük-e igénybe; a hiba vagy állományvég előfordulásakor szükséges teendők stb.) tartalmazza. A felhasználónak nem szükséges a részleteket ismerni, mivel a struktúra FILE néven definiálva van az <stdio.h> standard headerben. Az állománykezeléshez csak az állománymutatót kell deklarálni a következő minta szerint:

FILE *fp;
FILE *fopen(char *nev, char *mod);
A deklaráció azt mondja ki, hogy fp egy FILE típusú struktúrához tartozó mutató és a fopen egy FILE típusú struktúrát címző mutatóval tér vissza. Vegyük észre, hogy FILE egy típusnév, mint pl. az int, és nem igazi struktúranév. A FILE típust typedef utasítással deklarálták a könyvtárban (egyébként a fopen UNIX operációs rendszer alatti megvalósításának részleteit a 8.5. pontban írjuk le). A fopen függvény egy programból az
fp = fopen(nev, mod);
utasítással hívható. A fopen első argumentuma egy karaktersorozat, amely a megnyitandó állomány nevét tartalmazza. A második argumentum szintén egy karaktersorozat, ami azt jelzi, hogy a felhasználó hogyan akarja használni az állományt (használati mód). A használati mód karaktersorozatában használható karakterek: olvasás ("r"), írás ("w") és hozzáfűzés ("a", append). Néhány rendszer különbséget tesz szöveges és bináris állományok között, ilyenkor bináris állomány esetén a használati módot a "b" karakterrel kell kiegészíteni.

Ha írásra vagy hozzáfűzésre egy nem létező állományt akarunk megnyitni, akkor az adott nevű állomány (ha lehetséges) létrejön. Egy létező állományt írásra megnyitva, annak korábbi tartalma elvész, hozzáfűzésre megnyitva viszont megmarad. Ha megpróbálunk olvasásra megnyitni egy nem létező állományt, akkor hibajelzést kapunk. Más hibaokok is előfordulhatnak, pl. ha olvasni akarunk egy létező, de számunkra nem hozzáférhető (nem engedélyezett) állományt. Ha bármilyen állománykezelési hiba fordul elő, akkor a fopen NULL értékű mutatóval tér vissza. A hiba oka pontosabban is meghatározható, az erre alkalmas hibakezelő függvények leírása a B. Függelék 1. részében található.

A cat program megírásához szükséges következő tudnivaló, hogy hogyan lehet a már megnyitott állományt olvasni vagy írni. Ennek számos lehetősége van, amelyek közül a legegyszerűbb a getc és putc függvények alkalmazásán alapszik. A getc függvény az állományból kiolvasott következő karakterrel tér vissza, és hívásakor az állománymutató megadásával kell közölni, hogy melyik állományból olvasson. Általános alakja:

int getc(FILE *fp)
A függvény az fp-vel címzett adatáramból vett következő karakterrel vagy hiba esetén EOF jelzéssel tér vissza.

A putc adatkiviteli függvény

int putc(int c, FILE *fp)
alakú, és hívásakor a függvény az fp-vel címzett állományba kiírja a c karaktert, és visszatér a kiírt karakterrel vagy hiba esetén az EOF jellel. Hasonlóan a getchar és putchar eljárásokhoz, gyakran a getc és putc eljárásokat is makrók formájában írják meg, nem pedig függvényként, de ez a használatukat nem befolyásolja.

Egy C nyelvű program indításakor az operációs rendszer három állományt mindig automatikusan megnyit és az ezekhez tartozó állománymutatókat a program rendelkezésére bocsátja. Ez a három állomány a standard bemenet, a standard kimenet és a standard hibaállomány, amelyek állománymutatói a <stdio.h> headerben vannak deklarálva stdin, stdout és stderr néven. Normális esetben az stdin a billentyűzethez, stdout a képernyőhöz kapcsolódik, de ahogy azt a 7.1. pontban már leírtuk, az stdin és stdout minden további nélkül átirányítható adatállományokhoz vagy a pipeing mechanizmussal egy másik programhoz.

A getc, putc, stdin és stdout felhasználásával a getchar és putchar a következő módon definiálható:

#define getchar () getc(stdin)
#define putchar(c) putc((c), stdout)
Adatállományok formátumozott olvasására vagy írására az fscanf és fprintf függvényeket használhatjuk. Ezek használata lényegében megegyezik a scanf vagy printf függvények használatával, kivéve, hogy az első argumentumuknak az olvasandó vagy írandó állományt kijelölő állománymutatónak kell lennie és a formátumot leíró karaktersorozat a második argumentum. A függvények általános alakja:
int fscanf (FILE *fp, char *format, ...)
int fprintf (FILE *fp, char *format, ...)
Ezen előzetes ismeretek birtokában most már hozzákezdhetünk az állományokat összekapcsoló cat program írásához! A program felépítése megegyezik a korábbi programoknál már bevált felépítéssel: ha a programot parancssor-argumentummal hívjuk, akkor az argumentumok az állománynevek, ha nincs argumentum, akkor a standard bemenetről jövő adatok kerülnek feldolgozásra. A program:
#include <stdio.h>

/* cat: állományok konkatenálása - 1. változat */
main(int argc, char *argv[])
{
   FILE *fp;
   void filecopy (FILE *, FILE *);

   if (argc ==1) /* nincs argumentum,
         a standard bemenetet másolja */
      filecopy (stdin, stdout);
   else
      while (--argc > 0)
         if ((fp = fopen(*++argv, "r")) == NULL) {
            printf ("cat: nem nyitható meg %s\n", *argv);
            return 1;
         } else {
            filecopy (fp, stdout);
            fclose (fp);
         }
   return 0; 
}

/* filecopy: az ifp-vel címzett állományt
az ofp-vel cimzett állományba másolja */
void filecopy(FILE *ifp, FILE *ofp)
{
   int c;

   while ((c = getc(ifp)) != EOF)
      putc (c, ofp);
}
Az stdin és stdout állománymutatók FILE * típusú objektumok. Az stdin és stdout állandók, amelyeket a standard könyvtárban definiáltak, és nem pedig változók, így értéket sem rendelhetünk hozzájuk. Az
int fclose (FILE *fp)
függvény a fopen inverze, és hatására megszakad a kapcsolat az állománymutató és a külső állománynév között, az állománymutató felszabadul és más állományhoz rendelhető. A legtöbb operációs rendszer korlátozza az egy program futása során egyidejűleg nyitott állományok számát, ezért amikor már nincs szükség rá, célszerű felszabadítani az állománymutatót, ahogy ezt a cat programban is tettük. Az fclose másik fontos feladata, hogy kimeneti állományok esetén kiírja az állományba a putc által használt puffer tartalmát. (Normális esetben a puffer tartalma csak akkor íródna ki, ha már a puffer megtelt. Az utolsó, általában nem egész puffernyi tartalmat az fclose függvénnyel kell kiíratni.) A fclose automatikusan végrehajtódik minden egyes megnyitott állományra, ha a program normálisan fut le. A standard bemenetet és kimenetet szintén lezárhatjuk, ha nincs szükségünk rájuk. Amennyiben újra szükségessé válik a használatuk, akkor a freopen könyvtári függvénnyel nyithatók meg újra.

7.6. Hibakezelés – az stderr és exit függvények

A cat program hibakezelése messze nem ideális. A problémát az okozza, hogy ha egy állomány valamilyen hiba folytán hozzáférhetetlenné válik, a hibaüzenet a konkatenált kimeneti állomány (vagyis az eredmény) végére íródik ki. Ez megfelelő lehet, ha a kimeneti eszköz a képernyő, viszont elfogadhatatlan, ha egy állományba írunk vagy az eredményt a pipeing mechanizmussal egy másik programnak adjuk át.

A hibák jobb kezelése érdekében a programhoz automatikusan egy második kimeneti állományt is hozzárendel az operációs rendszer (hasonlóan az stdin és stdout állományokhoz), és ez az stderr hibaállomány. Az stderr állományba írt üzenetek normális esetben mindig a képernyőn jelennek meg, még akkor is, ha a standard kimenetet átirányítottuk.

Ennek alapján módosítsuk a cat programot és a hibaüzenetet írassuk a standard hibaállományba!

#include <stdio.h>
#include <stdlib.h>

/* cat: állományok konkatenálása - 2. változat */
main (int argc, char *argv[])
{
   FILE *fp;
   void filecopy(FILE *, FILE *);
   char *prog = argv[0]; /* a program neve hiba esetén */
   
   if (argc == 1) /* nincs parancssor-argumentum,
         a standard bemenetről másol */
      filecopy(stdin, stdout);
   else
      while (--argc > 0)
         if ((fp = fopen(*++argv, "f")) == NULL) {
            fprintf(stderr, "%s: nem nyitható meg: %s\n",
                  prog, *argv);
            exit (1);
         } else {
            filecopy(fp, stdout);
            fclose (fp);
         }
      if (ferror(stdout)) {
         fprintf(stderr, "%s: hiba stdout írásakor\n",
               prog);
         exit (2);
      }
   exit(0);
}
A program a hibákat kétféle módon jelzi. Először is az fprintf által létrehozott hibaüzenetek az stderr állományba íródnak, tehát mindig a képernyőn jelennek meg, ahelyett, hogy a kimeneti (eredmény) állományba íródnának vagy a pipeing mechanizmussal egy másik program bemenetére kerülnének. A hibaüzenet kiírásába belefoglaltuk a program argv[0]-ból vett nevét, így ha a programot más programokkal együtt használjuk, azonosítható a hiba forrása.

Az stderr állományon keresztüli hibajelzésen kívül a program használja az exit standard könyvtári függvényt, ami hívásakor leállítja a program futását. Az exit argumentuma bármely, az exit-et hívó eljárás rendelkezésére áll, így a program sikeres vagy hibás végrehajtása egy másik, ezt a programot alprogramként használó eljárásból ellenőrizhető. Megállapodás szerint a 0 argumentum a program sikeres lefutását, a nem nulla argumentum pedig valamilyen, normálistól eltérő működését jelzi. Az exit függvény automatikusan hívja az fclose függvényt és minden egyes megnyitott kimeneti állományba kiírja a kimeneti pufferének tartalmát.

A main eljárásban a return kifejezés utasítás egyenértékű az exit (kifejezés) utasítással. Viszont az exit előnye, hogy az más függvényekből is hívható és a hívás helye az 5. fejezetben leírt mintakereső programhoz hasonló programmal meghatározható.

Az ferror függvény nem nulla értékkel tér vissza, ha az fp állománymutatóval megcímzett állomány írása vagy olvasása közben hiba fordult elő. A függvény általános alakja:

int ferror(FILE *fp)
A gyakorlatban az adatkiviteli hibák ritkábban fordulnak elő, mint a beolvasási hibák (bár pl. hibát jelent, ha lemezre írás közben elfogy az üres hely), de a programot mindenképpen célszerű ellenőrizni.

A feof (FILE * ) függvény analóg a ferror függvénnyel és szintén nem nulla értékkel tér vissza, ha a megadott állományhoz való hozzáférés során hiba fordult elő. A függvény általános alakja:

int feof(FILE *fp)
A kis mintaprogramunk kapcsán általában nem érdemes túlzottan aggódni a kilépéskori állapotjelzés miatt, de nagyobb programok esetén gondot kell fordítani az értelmes és jól használható állapotjelzésekre.

7.7. Szövegsorok beolvasása és kiírása

A standard könyvtár tartalmazza az fgets bemeneti függvényt, ami hasonló a korábbról már ismert getline függvényhez, és általános alakja:
char *fgets(char *sor, int maxsor, FILE *fp)
Az fgets függvény az fp állománymutatóval megadott állományból beolvassa a következő bemenő sort (az újsor-karaktert is beleértve) és elhelyezi a sor nevű karakteres tömbben. A függvény legfeljebb maxsor-1 karaktert fog beolvasni és a beolvasott sort a '\0' jellel fogja lezárni. Normális esetben az fgets a sor tömbbel tér vissza, de állomány vége vagy hiba esetén a visszatérési érték NULL. (A korábban használt getline függvény a sor hosszával tért vissza, ami nagyon hasznos volt, mivel így a nulla jelenthette az állomány végét.)

Adatkiírásra az fputs függvény használható, amivel egy karaktersorozat (ami nem szükségszerű, hogy újsor-karaktert tartalmazzon) írható ki egy megadott állományba. A függvény általános alakja:

int fputs(char *sor, FILE *fp)
A függvény EOF jellel tér vissza, ha a kiíráskor hibát érzékel és nulla értékkel, ha minden rendben volt.

A könyvtári gets és puts függvények hasonlóak az fgets és fputs függvényekhez, de mindig az stdin és stdout állománymutatókkal kijelölt állományt kezelik. Használatuk során zavaró lehet, hogy a gets törli a billentyűzetről érkező '\n' újsor-karaktert, a puts pedig mindig azzal zárja a kiírást.

Annak bizonyítására, hogy az fgets vagy fputs függvényekben nincs semmi speciális, idemásoltuk a könyvtári függvények eredeti programkódját:

/* fgets: beolvas egy legfeljebb n karakteres sort
az iop állománymutatóval kijelölt állományból */
char *fgets(char *s, int n, FILE *iop)
{
   register int c;
   register char *cs;

   cs = s;
   while (--n > 0 && (c = getc(iop)) != EOF)
      if ((*cs++ = c) == '\n')
         break;
   *cs = '\n';
   return (c == EOF && cs == s) ? NULL : s;
}

/* fputs: kiírja az s karaktersorozatot
az iop állománymutatóval kijelölt állományba */
int fputs(char *s, FILE *iop)
{
   int c;

   while (c = *s++)
      putc(c, iop);
   return ferror(iop) ? EOF : 0; 
}
A szabvány azt írja elő, hogy a ferror függvény nem nulla értékkel tér vissza, ha hiba volt, ezzel szemben az fputs hiba esetén EOF jelzéssel, minden más esetben nem negatív értékkel tér vissza.

Az fgets függvény felhasználásával már egyszerűen megvalósíthatjuk a getline függvényt:

/* getline: beolvas egy sort és visszatér
a sor hosszával */
int getline(char *sor, int max)
{
   if (fgets(sor, max, stdin) == NULL)
      return 0;
   else
      return strlen(sor);
}

7.6. gyakorlat. Írjunk programot, amely összehasonlít két állományt, és kiírja az első olyan sort, ahol különböznek!

7.7. gyakorlat. Módosítsuk az 5. fejezetben ismetetett mintakereső programot úgy, hogy bemenetét a parancssor-argumentumként megadott állománynevek listájából, vagy ha nincs argumentum, akkor a standard bemenetről vegye! Ki kell-e íratni az állomány nevét, ha a program egymáshoz illeszkedő sorokat talál?

7.8. gyakorlat. Írjunk programot, amely több állományt ír ki egymás után, minden állományt új oldalon kezdve! A program minden állomány kiírása előtt írja ki a lap tetejére a címet és az oldalakat állományonként folyamatosan számozza!

7.8. További könyvtári függvények

A standard könyvtárban számos, különböző feladatok megoldására alkalmas függvény található. Ebben a pontban néhány hasznos függvény rövid leírását adjuk. A részletes leírás, ill. a könyvtár további függvényeinek ismertetése a B. Függelékben található.

7.8.1. Karaktersorozatokat kezelő függvények

Mint már korábban említettük, az strlen, strcpy, strcat, ill. strcmp függvények deklarációja a <string.h> standard headerben található, a többi, karaktersorozatot kezelő függvény deklarációjával együtt. A következő leírásban s és t char * típusú karaktersorozatot, c és n int típusú adatot jelöl. Az egyes karaktersorozat-kezelő függvények:


strcat (s , t) a t karaktersorozatot az s végéhez fűzi (konkatenálja);
strncat (s , t, n) a t karaktersorozat n darab karakterét az s végéhez fűzi;
strcmp (s, t) negatív, nulla vagy pozitív értékkel tér vissza, ha s < t, s = t vagy s > t;
strncmp (s, t, n) ugyanúgy működik, mint az strcmp, de az összehasonlítást csak az első n karakterrel végzi el;
strcpy (s, t) a t karaktersorozatot s-be másolja;
strncpy (s, t, n) a t karaktersorozat első n karakterét s-be másolja;
strlen (s) a visszatérési érték az s karaktersorozat hossza;
strchr (s, c) visszatér a c karakter s karaktersorozatbeli első előfordulási helyének mutatójával, vagy NULL értékkel, ha c nem fordul elő s-ben;
strrchr (s, c) visszatér a c karakter s karaktersorozatbeli utolsó előfordulási helyének mutatójával, vagy NULL értékkel, ha c nem fordul elő s-ben.


7.8.2. Karaktervizsgáló és -átalakító függvények

A <ctype.h> standard header számos, karakterek vizsgálatára vagy átalakítására alkalmas függvényt tartalmaz. A leírt függvények visszatérési értéke int típusú, a leírásukban szereplő c int típusú, és unsigned char vagy EOF adatként értelmezhető.


isalpha (c) visszatérési érték nem nulla, ha c alfabetikus karakter és nulla, ha nem az;
isupper (c) visszatérési érték nem nulla, ha c nagybetűs karakter és nulla, ha nem az;
islower (c) visszatérési érték nem nulla, ha c kisbetűs karakter és nulla, ha nem az;
isdigit (c) visszatérési érték nem nulla, ha c számjegykarakter és nulla, ha nem az;
isalnum (c) visszatérési érték nem nulla, ha c alfabetikus vagy számjegykarakter és nulla, ha nem az;
isspace (c) visszatérési érték nem nulla, ha c szóköz-, tabulátor-, újsor-, kocsivissza-, lapemelés- vagy függőlegestabulátor-karakter és nulla, ha nem az;
toupper (c) visszatérési érték c nagybetűssé alakított értéke;
tolower (c) visszatérési érték c kisbetűssé alakított értéke.


7.8.3. Az ungetc függvény

A standard könyvtárban megtalálható a 4. fejezetben megírt ungetch függvény egy szűkített változata, az ungetc. A függvény általános alakja:
int ungetc (int c, FILE *fp)
Az ungetc függvény a c karaktert visszahelyezi az fp állománymutatóval kiválasztott állományba, és visszatér magával a c értékkel, vagy hiba esetén az EOF jelzéssel. Állományonként csak egy karakter visszahelyezése megengedett. Az ungetc függvény minden scanf, getc vagy getchar jellegű bemeneti függvénnyel használható.

7.8.4. Parancsvégrehajtás

A system (char *s) függvény végrehajtja az s karaktersorozatban elhelyezett parancsot, ha az éppen futó programban rá kerül a vezérlés. Az s karaktersorozat megengedett tartalma (a megengedett parancsok halmaza) nagymértékben függ a használt operációs rendszertől. A system függvény alkalmazására jó példa a UNIX operációs rendszer esetén kiadott
system ("date");
utasítás, ami a rendszer date parancsának végrehajtását, azaz a dátum és a napi idő standard kimeneten való kiírását idézi elő. A system a használt operációs rendszertől függő egész állapotjelzéssel tér vissza a végrehajtott parancsból. UNIX operációs rendszer esetén a system visszatérési állapotjelzése megegyezik az exit visszatérési értékével.

7.8.5. Tárkezelő függvények

A tárolóban adott méretű terület dinamikus lefoglalása a malloc és calloc függvényekkel lehetséges. A malloc függvény általános alakja:
void *malloc (size_t n)
A függvény egy n bájtos, inicializálatlan tárterületet címző mutatóval, vagy ha a helyfoglalási igény nem elégíthető ki, a NULL értékű mutatóval tér vissza. A
void *calloc(size_t n, size_t meret)
általános alakú calloc függvény n darab megadott méretű objektum számára elegendő helyet biztosító tömb mutatójával, vagy ha a helyigény nem elégíthető ki, akkor NULL értékű mutatóval tér vissza.

A malloc vagy calloc függvények visszatérési értékeként kapott mutató a megadott objektumnak megfelelő helyre mutat, de kényszerített típuskonverzióval a kívánt típusúvá kell alakítani, pl. az

int *ip;
ip = (int *) calloc (n, sizeof(int));
módon.

A free (p) függvény felszabadítja a p mutatóval megcímzett helyet, ahol p egy eredendően malloc vagy calloc hívásával kapott mutató. Arra vonatkozóan, hogy melyik helyet szabadítjuk fel, nincs semmiféle megszorítás, de fatális hiba a következménye, ha nem a malloc vagy calloc függvény hívásával lefoglalt helyet akarunk felszabadítani.

Szintén programhibát okoz, ha a hely felszabadítása után akarjuk használni az adott hely tartalmát. Az alábbi, egy lista helyeit felszabadító ciklus tipikus, de inkorrekt programot ad:

for(p = fej; p != NULL; p = p->kovetkezo) /* HIBÁS! */
   free(p);
A feladatot helyesen csak úgy tudjuk megoldani, ha a hely felszabadítása előtt annak tartalmát elmentjük egy segédváltozóba:
for (p = fej; p != NULL; p = q) {
   q = p->kovetkezo;
   free(p); 
}
A 8.7. pontban bemutatjuk egy malloc-hoz hasonló tárhelykiosztó függvény megvalósítását. Az általa kiosztott tárterületek tetszőleges sorrendben szabadíthatók fel.

7.8.6. A matematikai függvények

A <math.h> standard headerben húsznál több matematikai függvény van deklarálva, itt most csak a leggyakrabban használatosakat mutatjuk be. Mindegyik függvénynek egy vagy két double típusú argumentuma van és viszatérési értéke double típusú.


sin(x) a radiánban adott x érték szinusza;
cos (x) a radiánban adott x érték koszinusza;
atan2 (y, x) az y/x árkusz tangense radiánban;
exp (x) az ex exponenciális függvény értéke;
log (x) x természetes (e alapú) logaritmusa (x>0);
log10 (x) x tízes alapú logaritmusa (x>0);
pow (x, y) az xy függvény értéke;
sqrt (x) x négyzetgyöke (x>0);
fabs (x) x abszolút értéke.


7.8.7. Véletlenszám-generálás

A rand függvény egész számok pszeudovéletlen-szerű sorozatát állítja elő. A kapott számok nulla és az <stdlib.h> standard headerben definiált RAND_MAX érték közé esnek. Ennek felhasználásával a 0 <= x < 1 tartományba eső lebegőpontos véletlenszámok a
#define frand() ((double) rand() / (RAND_MAX+1.0))
definícióval állíthatók elő. (Ha az adott gépen futó C rendszer könyvtárában már létezik a lebegőpontos véletlenszám-generáló függvény, akkor az valószínűleg kedvezőbb statisztikai tulajdonságokkal rendelkezik, mint az így definiált véletlen szám.)

A rand függvény kiindulási értéke a srand(unsigned) függvénnyel állítható be. A rand és srand függvények szabványban javasolt hordozható változatát a 2.7. pontban ismertettük.

7.9. gyakorlat. Az isupper-hez hasonló függvények helytakarékos vagy időtakarékos változatban írhatók meg. Vizsgálja meg, hogyan lehetséges mindkét változat kialakítása!



6. FEJEZET Tartalom 8. FEJEZET