C programozás 4. FEJEZET

 

3. FEJEZET Tartalom 5. FEJEZET

4. FEJEZET:

Függvények és a program szerkezete

A függvényekkel a nagyobb számítási feladatok kisebb egységekre oszthatók, így a programozó felhasználhatja a már meglévő egységeket és nem kell minden alkalommal elölről kezdeni a munkát. A függvények a működésük részleteit gyakran elrejtik a program többi része elől, de jól megírt függvények esetén nincs is szükség ezekre a részletekre.A C nyelvet úgy tervezték meg, hogy a függvények hatékonyak és jól használhatók legyenek. A C nyelvű programok sokkal inkább több, kisebb függvényből állnak, mint egy nagyobból. A program egy vagy több forrásállományban helyezkedhet el. A forrásállományok egymástól függetlenül fordíthatók és a már korábban lefordított könyvtári függvényekkel együtt tölthetők be. Ennek menete az alkalmazott operációs rendszertől függ, ezért pillanatnyilag nem foglalkozunk vele.

Az ANSI szabvány a függvények deklarációja és definíciója terén változtatta meg leginkább a C nyelvet. Mint az 1. fejezetben már említettük, lehetővé vált az argumentumok típusának deklarálása a függvény deklarálásával egyidejűleg. A függvény definiálásának szintaxisa szintén megváltozott, ami lehetővé teszi a fordítóprogramnak, hogy a korábbi változatokhoz képest sokkal több hibát felderítsen. Az új szabvány további előnye, hogy helyesen deklarált argumentumok esetén létrejön a kényszerített automatikus típuskonverzió.

A szabvány tisztázza a nevek hatáskörének kérdését is, különösen azzal, hogy megköveteli az egyes külső objektumok egyszeri definícióját. Az inicializálás teljesen általánosan működik, és az automatikus tárolási osztályú tömbök, ill. struktúrák egyszerűen inicializálhatók.

A bevezetőben fontos megemlíteni, hogy a C előfeldolgozó rendszert is továbbfejlesztették. A feltételes fordítási direktívák korábbinál sokkal teljesebb készlete új lehetőségekkel bővült, ami elsősorban a makrokifejtés hatékonyabb vezérlésében jelentkezik, valamint abban, hogy a makro argumentumából aposztófokkal határolt karaktersorozat generálható.

4.1. A függvényekkel kapcsolatos alapfogalmak

Az alapfogalmak bemutatásához írjunk egy programot, amely a bemenetére adott szöveg minden olyan sorát kiírja, amiben megtalálható egy adott minta (karaktersorozat). A program a UNIX grep segédprogramjának egy speciális változata. Például a következő szövegben keressük a „dal” mintát az egyes sorokban.*

Óh lakodalmi kar-dal,
   éneklő diadal,
ki is verseng e dallal,
   oly édes, fiatal,
hiába itt a harc, meddő a viadal.
*[A feladatot (mint általában eddig is) magyarítottuk. A példában szereplő versszak Percy Bysshe Shelley: Egy mezei pacsirtához című verséből van, Kosztolányi Dezső fordításában. (A fordító)]

A program eredményül a következő szöveget hozza létre:

Óh lakodalmi kar-dal,
éneklő diadal,
ki is verseng e dallal,
hiába itt a harc, meddő a viadal.

A feladat három alapvető részre osztható, és így a program szerkezete:

while (van további sor)
   if (a sor tartalmazza a mintát)
      nyomtassuk ki

Bár a teljes programot egyetlen egységként (egyetlen main függvényként) is megírhatjuk, mégis célszerű a fenti programszerkezet előnyeit kihasználni és az egyes részeket önálló függvényként megírni. A három kis résszel könnyebben megbirkózunk, mint az egyetlen naggyal, mivel a lényegtelen részletkérdések „belevesznek” az egyes függvényekbe és a nem kívánt kölcsönhatások esélye minimális lesz. Ezenkívül bármely programrészt (függvényt) más programokban is felhasználhatjuk.A „van még további sor” feladatrészt az 1. fejezetben már megírt getline függvénnyel, a „nyomtassuk ki” feladatrészt pedig a printf függvénnyel valósítjuk meg. Ez azt jelenti, hogy csak azt a programrészt kell megírnunk, amely eldönti, hogy a keresett minta megtalálható-e a vizsgált sorban.

Ezt a programrészt az strindex(s, t) függvénnyel oldjuk meg. A strindex függvény első argumentuma (s) a vizsgált sort tartalmazó karaktertömb, a második argumentuma (t) a keresett mintát tartalmazó karaktertömb, és visszatérési értéke a t tömb kezdetének s tömbbeli indexe, vagy -1, ha s nem tartalmazza a keresett mintát. Mivel egy C-beli tömb kezdetének indexe nulla, a visszaadott index nulla vagy pozitív lesz, ezért a -1 érték jól használható az extra esetek jelzésére. A későbbiekben, amikor majd a mintakereső program fejlettebb változatát írjuk, csak az strindex függvényt cseréljük le egy intelligensebbre, és a program többi részét változatlanul hagyjuk. (A standard könyvtár tartalmazza az strstr függvényt, amelynek feladata hasonló az strindex függvényéhez, de nem a kezdőpont indexét, hanem egy mutatót ad értékül.)

A kezdeti programtervezés után már viszonylag gyorsan megírhatjuk a részletes programot. A szerkezetet követve jól látható az egyes részek egymáshoz kapcsolódása. Egyelőre ne a legáltalánosabb esettel kezdjük: a keresett minta literálisként megadott karaktersorozat legyen. Hamarosan visszatérünk még a karaktertömbök inicializálására és az 5. fejezetben megmutatjuk, hogyan tehetjük a keresett mintát a program futása közben megadható paraméterré. A mintakereső programban megadjuk a getline függvény egy újabb változatát. Célszerű, ha ezt összehasonlítjuk az 1. fejezetben leírt változattal. Ezután lássuk a programot!

#include <stdio.h>
#define MAXSOR 1000 /* a sor maximális hossza */

int getline(char sor[ ], int max);
int strindex(char forras[ ], char keresett[ ]);
char minta[ ] = "dal"; /* a keresett minta */

/* megkeresi a mintát tartalmazó összes sort */
main( )
{
   char sor[MAXSOR];
   int talalt = 0;

   while (getline(sor, MAXSOR) > 0)
      if (strindex(sor, minta) >= 0) { 
         printf("%s", sor);
         talalt++;
      }
   return talalt;
}
/* getline: egy sort beolvas s-be és megadja a hosszát */
int getline(char s[], int hatar)
{
   int c, i;

   i = 0;
   while (--hatar > 0 && (c = getchar( )) != EOF
            && c != '\n')
      s[i++] = c;
   s[i] = '\0';
   return i;
}
/* strindex: visszaadja t indexét s-ben, ill.
-1-et, ha a keresett minta nincs a sorban */
int strindex(char s[ ], char t[ ])
{
   int i, j, k;

   for (i = 0; s[i] != '\0'; i++) {
      for (j = i, k = 0; t[k] != '\0' && s[j] == t[k];
            j++, k++)
         ;
   if (k > 0 && t[k] == '\0')
      return i;
   }
return -1;
}

A programban használt összes függvény definíciója

visszatérési-típus függvénynév (argumentumdeklarációk)
{
   deklarációk és utasítások
}

alakú, és a definícióból egyes részek hiányozhatnak. A legegyszerűbb és legrövidebb függvény a

dummy( ) { }

amely nem csinál semmit és nem ad vissza értéket. Az ilyen üres (semmit nem csináló) függvény beépítése a programba gyakran hasznos, ha a későbbi programfejlesztéshez le akarjuk foglalni egy függvény helyét. Ha a definícióból hiányzik a visszatérési típus, akkor a rendszer az int alapfeltételezéssel él.A program lényegében nem más, mint változók és függvények definícióinak halmaza. A függvények közötti információcsere a függvény argumentumain és visszatérési értékén, valamint a külső változókon keresztül jön létre. A függvények a program forrásállományában tetszőleges sorrendben helyezkedhetnek el, és a forrásprogram több állományra bontható, de egy függvény nem vágható szét két forrásállományba.

A hívott függvény a hívó függvénynek a return utasítással adhat vissza értéket. A return utasítást tetszőleges kifejezés követheti. Általános alakja:

return kifejezés;

Ha szükséges, akkor a kifejezés típusa a visszatérési típusra konvertálódik. Gyakran a kifejezést zárójelbe tesszük, de ez opcionális.A hívó függvénynek jogában áll figyelmen kívül hagyni a visszatérési értéket, sőt nem kötelező a return utáni kifejezés sem. Ez utóbbi esetben a hívott függvény nem ad vissza értéket a hívó függvénynek. A vezérlés akkor is érték nélkül tér vissza a hívó függvényhez, ha a végrehajtás kilép a függvényből, elérve a függvény végét jelző jobb oldali kapcsos zárójelet. Az ilyen kilépés nem tilos, de valószínűleg valamilyen hibát jelez, ha a függvény értéket ad vissza az egyik helyről és nem ad értéket a másik helyről történő visszatéréskor. Ha egy függvénynek nincs visszatérési értéke, akkor a (formálisan kapott) visszatérési érték határozatlan („szemét”).

Az előbbi mintakereső programunk a main-ből való kilépéskor egy állapotjelzést ad, ami a találatok száma. Ezt az értéket a programot hívó környezet tetszőleges célra használhatja.

A több forrásállományba szétosztott C programok fordítási és betöltési folyamata rendszertől függő. Például a UNIX operációs rendszer alatt az 1. fejezetben már említett cc paranccsal lehet a C programok fordítását vezérelni. Tegyük fel, hogy a három függvényünk három különböző állományban van, amelyek neve main.c, getline.c és strindex.c. Ekkor a

cc main.c getline.c strindex.c

parancs lefordítja a három forrásállományt, létrehozva ezzel a main.o, getline.o és strindex.o tárgykódú állományokat, majd ezekből összeszerkeszti az a.out nevű végrehajtható programállományt. Ha a fordítás közben hiba volt, pl. a main.c állományban, akkor a hiba kijavítása után a main.c állomány újra fordítható és összeszerkeszthető a korábban kapott tárgykódú állományokkal. Ez a

cc main.c getline.o strindex.o

paranccsal érhető el. A cc parancs a forráskódú és tárgykódú állományokat a .c és .o névkiterjesztések alapján különbözteti meg.

4.1. gyakorlat. Írjuk meg az strindex(s, t) függvénynek azt a változatát, amely a t minta s-beli legutolsó előfordulásának indexével, vagy ha t nem található meg s-ben, akkor -1-gyel tér vissza!

4.2. Nem egész értékkel visszatérő függvények

A korábban bemutatott példaprogramok függvényei egész típusú értékkel vagy érték nélkül (void típus) tértek vissza. Mi van akkor, ha a függvénynek más típusú értékkel kell visszatérni? Számos matematikai függvény, mint az sqrt, sin, cos stb. double típusú értékkel tér vissza, más speciális függvények esetén más a visszatérési típus. Ennek megvalósítását bemutatandó írjuk meg az atof(s) függvényt, amely az s karaktertömbben megadott adatot kétszeres pontosságú lebegőpontos számmá alakítja. Az atof a 2. és a 3. fejezetben, különböző változatokban bemutatott atoi függvény kiterjesztése: lehetővé teszi az opcionálisan megadott előjel és tizedespont kezelését, valamint az egész, ill. törtrész megléte vagy hiánya esetén is használható. Mindezek ellenére ez a program nem egy minden igényt kielégítő bemeneti konverziós eljárás, egy ilyen eljárás megírása több helyet igényelne. A standard könyvtár tartalmaz egy atof függvényt az <stdlib.h> headerben.

#include <ctype.h>

/* atof: az s karaktersorozat duplapontos számmá alakítása */
double atof(char s[ ])
{
   double val, power;
   int i, sign;

   for (i = 0; isspace(s[i]); i++)
      /* az üres helyek átugrása */
      ;
   sign = (s[i] == '-') ? -1 : 1;
   if (s[i] == '+' || s[i] == '-')
      i++ ;
   for (val = 0.0; isdigit (s[i]); i++)
      val = 10.0 * val + (s[i] - '0');
   if (s[i] == '.')
      i++;
   for (power = 1.0; isdigit(s[i]); i++) {
      val = 10.0 * val + (s[i] - '0');
      power *= 10.0; 
   }
   return sign * val / power;
}

Először is az atof függvénynek deklarálnia kell a visszatérési típusát, mivel az most nem int. A visszatérési típus megadása a függvény neve előtt történik. Másodszor, és ez legalább ilyen fontos, a hívó eljárással is tudatni kell, hogy az atof visszatérési értéke nem int típusú. Ennek egyik módja, hogy az atof függvényt explicit módon deklaráljuk a hívó eljárásban. A deklaráció módját a következő primitív kalkulátorprogramon keresztül mutatjuk be. A program soronként egy-egy számot olvas be, amely előtt előjel is lehet, majd a számokat összeadja és az eredményt minden adatbeolvasás után kiírja.

#include <stdio.h>
#define MAXSOR 100

/* primitív kalkulátorprogram */
main( )
{
   double sum, atof(char [ ]);
   char sor[MAXSOR];
   int getline(char sor[ ], int max);

   sum = 0;

   while (getline(sor, MAXSOR) > 0)

      printf("\t%g\n", sum += atof(sor));
   return 0; 
}

A

double sum, atof(char[ ]);

deklaráció azt mondja ki, hogy a sum egy double típusú változó, valamint az atof függvénynek egyetlen, char[ ] típusú argumentuma van és visszatérési értéke is double típusú.Az atof függvényt következetesen, egymással összhangban kell deklarálni és definiálni. Ha egyetlen forrásállományon belül ellentmondás van az atof típusa és a main-beli hívásának típusa között, akkor ezt a hibát a fordítóprogram észreveszi és jelzi. De ha az atof függvényt önállóan fordítjuk le (és legtöbbször így van), akkor a hiba nem derül ki. Az atof visszatér a double típusú eredménnyel, amit a main int típusúként kezel és ez értelmetlen eredményre vezet.

Az elmondottak alapján látszik, hogy a deklaráció és a definíció összhangjára vonatkozó szabályt komolyan kell venni. A típusillesztési hiba főképp olyankor fordul elő, ha nincs függvényprototípus, és a függvény a kifejezésbeli első előfordulásakor, implicit módon van deklarálva. Ilyen pl. a

sum += atof(sor);

kifejezés. Ha egy korábban még nem deklarált név fordul elő egy kifejezésben és a nevet bal oldali kerek zárójel követi, akkor arról a rendszer a programkörnyezet alapján feltételezi, hogy függvény és a kifejezés megfelelő részét a függvény deklarációjának tekinti. A fordítóprogram ugyancsak feltételezi, hogy az így deklarált függvény egész típusú (int) visszatérési értéket ad, viszont semmit sem tételez fel az argumentumairól. Abban az esetben, ha a függvénydeklarációban nincs argumentum, mint pl. a

double atof( );

deklarációban, akkor a fordítóprogram ismét nem tételez fel semmit az argumentumokról és a teljes paraméter-ellenőrzést kikapcsolja. Ennek az a célja, hogy az üres paraméterlistájú deklarációkat tartalmazó régebbi programok is lefordíthatok legyenek az új fordítóprogramokkal. Mindezek ellenére az üres paraméterlista nyújtotta lehetőségeket ne alkalmazzuk az új programokban.Ha adott a megfelelően deklarált atof függvény, akkor azt felhasználva egyszerűen megírhatjuk az atoi függvényt is (amely egy karaktersorozatot int típusú számmá alakít):

/* atoi: az s karaktersorozat egésszé alakítása */
int atoi(char s[ ])
{
   double atof(char s[ ]);
   return (int) atof(s);
}

Figyeljük meg a deklarációk szerkezetét és a return utasítást! A

return kifejezés;

utasításban a kifejezés értéke a visszatéréskor automatikusan int típusúvá konvertálódik, ami az atoi típusa. Így az automatikus konverzió a double típusú atof értéke esetén is létrejön, ha az megjelenik a return utasításban. Ezért a visszatéréskor megadott int típuskijelölés felesleges, bár a fordítóprogram figyeli és felhasználja. A kényszerített típuskijelölést csak azért használtuk, hogy elkerüljük a fordítóprogram bármiféle figyelmeztető jelzését.

4.2. gyakorlat. Bővítsük ki az atof függvényt úgy, hogy az pl. az 123.45e-6 alakú tudományos jelölésmódot is kezelni tudja! A bemeneti karaktersorozat a kitevő jelzésére e vagy E karaktereket használhatja és utána előjeles kitevő következhet.

4.3. A külső változók

A C nyelvű program külső objektumok – változók vagy függvények – halmaza. A külső jelzőt a belső ellentéteként használjuk, ami a függvények belsejében definiált argumentumok és változók leírására alkalmas. A külső változókat a függvényen kívül definiáljuk, így elvileg több függvényben is felhasználhatók. A függvények maguk mindig külső típusúak, mivel a C nyelv nem engedi meg, hogy egy függvény belsejében újabb függvényt definiáljunk. Alapértelmezés szerint a külső változókra vagy függvényekre azonos néven való hivatkozás (még akkor is, ha külön fordított függvényből történik) mindig azonos dolgot jelent (a szabvány ezt a tulajdonságot külső csatolásnak, external linkage-nek nevezi). Ilyen értelemben a C külső változói analógak a FORTRAN COMMON változóival vagy a Pascal programok legkülső blokkjában használt változókkal. A későbbiekben majd látni fogjuk, hogy hogyan lehet olyan külső változókat és függvényeket definiálni, amelyek csak egyetlen forrásállományban láthatók.Mivel a külső változók globálisan hozzáférhetők, jól használhatók a függvények argumentumai és visszatérési értékei helyett, azaz a függvények közti adatforgalomban. Bármely függvény hozzáférhet egy külső változóhoz a neve alapján, ha az valahol ezen a néven deklarálva volt.

Ha a függvények közötti adatforgalomhoz sok változó kell, akkor a külső változók használata kényelmesebb és hatékonyabb, mint a hosszú argumentumlista. De ahogy már az 1. fejezetben is említettük, ezt a megállapítást fenntartással kell fogadnunk, mivel a sok külső változó rontja a program áttekinthetőségét, és túl sok (sokszor nem kívánt) adatkapcsolathoz vezet.

A külső változók a nagyobb hatáskörük és élettartamuk miatt is hasznosak. Az automatikus változók csak a függvény belsejében léteznek: létrejönnek a függvénybe való belépéskor és megszűnnek a kilépéskor. A külső változók állandóak, értékük az egyik függvényhívástól a másikig megmarad. Így ha két függvény azonos adathalmazzal dolgozik (de egyik sem hívja a másikat), akkor kényelmesebb a közös adathalmazt külső változóként használni az argumentumlistán keresztüli körülményes adatátadás helyett.

Az eddigi gondolatokat vizsgáljuk meg egy nagyobb példán keresztül. A feladat egy kalkulátorprogram írása, amely már végrehajtja a +, -, *, / alapműveleteket is. Az egyszerű megvalósíthatóság miatt a kalkulátor használja a fordított lengyel jelölésmódot a szokásos infix jelölésmód helyett. (Néhány zsebszámológép, valamint a Forth és Postcript programnyelvek is a fordított lengyel jelölésmódot használják.)

A fordított lengyel jelölésmódban az operátor az operandusok után következik. Az infix formában írt

(1-2)*(4+5)

kifejezés fordított lengyel jelölésmódban

12-45+*

tehát így kell majd a programnak beadni. A zárójelekre nincs szükség, a jelölésmód teljesen egyértelmű mindaddig, amíg tudjuk, hogy az egyes operátorok hány operandust várnak.A fordított lengyel jelölésmódot feldolgozó program megvalósítása nagyon egyszerű: az egyes operandusokat egy verembe tesszük, majd ha megérkezett az operátor, akkor a megfelelő számú (általában kettő) operandust kivesszük a veremből, alkalmazzuk rájuk az operátort, majd az eredményt visszatesszük a verembe. A fenti példában először 1, majd 2 kerül a verembe, amiből az első az operátor megérkezése után helyettesítődik az eredménnyel (-1-gyel). Ezután a 4 és 5 kerül a verembe, amit az összegükkel (9-cel) helyettesítünk. Most a veremben -1 és 9 van, amit a szorzatukkal helyettesítünk. A műveletsor befejeztével a verem tetején lévő értéket kivesszük és kiírjuk. A műveletsor a bemenetre adott sor végét elérve zárul.

A program lényegében egy ciklusból áll, amely végrehajtja az egyes operátorokkal és operandusokkal a megfelelő műveleteket. A program váza:

while (a következő operandus vagy operátor nem állományvége-jel)
   if(szám)
      tedd a verembe
   else if (operátor)
      vedd elő az operandusokat
      hajtsd végre a műveletet
      tedd az eredményt a verembe
   else if (újsor-jel)
      vedd ki a verem tetején lévő elemet és írd ki
   else
      hiba

Egy adat verembe helyezése (push) és kivétele (pop) nagyon egyszerű művelet, de a hibafigyeléssel és hiba utáni helyreállítással már viszonylag hosszú programrészt ad, amit a programon belül többször ismételni kellene, ezért inkább önálló függvényként valósítottuk meg azokat. Szintén önálló függvény a következő bemeneti operátor vagy operandus beolvasását végző rész.Egy fontos programtervezési kérdésről még nem döntöttünk: hol legyen a verem és melyik eljárások kezelhetik közvetlenül a vermet. Egy lehetőség, hogy a verem a main függvényben van, és az adatokat a verembe író, ill. onnan kiolvasó eljárásoknak paraméterként átadjuk a vermet, ill. az aktuális veremmutatót. A main függvénynek nem kell tudni a vermet vezérlő változókról, csak a verembe írást, ill. az onnan való olvasást kell vezérelnie. Ezért úgy döntöttünk, hogy a verem és minden vele kapcsolatos információ legyen külső változó, amelyhez csak a push és pop függvény férhet hozzá, a main nem.

Ezt az elgondolást utasítások formájában egyszerűen felírhatjuk. Arra gondolva, hogy a program egyetlen forrásállományban van, a szerkezete valahogy így fog kinézni:

#include-ok
#define-ok

a main függvény-deklarációi

main ( ) {...}

külső változók a push és pop számára

void push(double f) {...}
double pop(void) {...}

int getop(char s[]) {...}

a getop függvény által hívott eljárások

A későbbiekben majd megmutatjuk, hogy a program hogyan osztható két vagy több forrásállományra.A main függvény ciklusa egy nagy switch utasítást tartalmaz, amely az operandusok és operátorok jellege szerint választja szét a feladatokat. Ez talán tipikusabb alkalmazása a switch utasításnak; mint a 3.4. pontban bemutatott példa.

#include <stdio.h>
#include <stdlib.h> /* az atof miatt */

#define MAXOP 100 /* az operandus vagy operátor
                  max. hossza */
#define SZAM '0' /* jelzi, hogy számot talált */

int getop(char[ ]);
void push (double);
double pop(void);

/*fordított lengyel jelölésmóddal működő kalkulátorprogram */
main( )
{
   int tipus;
   double op2;
   char s[MAXOP];

   while ((tipus = getop(s)) != EOF) {
      switch (tipus) {
         case SZAM:
            push(atof(s));
            break;
         case '+':
            push(pop( ) + pop ( ));
            break;
         case '*':
            push (pop( ) * pop( ));
            break;
         case '-':
            op2 = pop( );
            push(pop( ) - op2);
            break;
         case '/':
            op2 = pop( );
            if(op2 != 0.0)
               push(pop( ) / op2);
            else
               printf("Hiba: osztás nullával\n");
            break;
         case '\n':
            printf("\t%.8g\n", pop( ));
            break;
         default:
            printf ("Hiba: ismeretlen parancs %s\n", s);
            break;
      }
   }
   return 0;
}

Mivel a + és * kommutatív operátorok, ezért mindegy, hogy a veremből kivett operandusokat milyen sorrendben dolgozzuk fel, viszont a és / esetén a bal és jobb oldali operandusok nem cserélhetők fel. Ezért a

push(pop( ) - pop( )); /* HIBÁS!*/

utasításban a pop hívási sorrendek definiálatlanok lehetnek. A művelet helyes sorrendben való végrehajtása érdekében a veremből elsőnek kivett értéket egy átmeneti tárolóba kell helyezni, mint a main is teszi. A verembe írást, ill. az onnan való olvasást végző függvények:

#define MAXVAL 100
/* a val tömbbel kialakított verem max. mélysége */

int sp = 0; /* a verem következő szabad helye */
double val[MAXVAL]; /* a verem tömbje */

/* push: f értékét a verembe teszi */
void push(double f)
{
   if(sp < MAXVAL)
      val[sp++] = f;
   else
      printf("Hiba: a verem megtelt, nem írható ki %g\n", f);
}

/* pop: kiolvas egy adatot a veremből és visszatér az értékkel */
double pop(void)
{
   if (sp > 0)
      return val[--sp];
   else {
      printf ( "Hiba: a verem üres\n");
      return 0.0;
   }
}

Egy változó külső tárolási osztályú, ha bármelyik függvényen kívül definiáljuk. így a verem és a verem indexe, amelyet a push és a pop közösen használ, ezeken függvényeken kívül lett definiálva. Mivel a definiálás a push és a pop függvényekkel együtt történt, a verem változóihoz a main közvetlenül nem fér hozzá, azok a számára láthatatlanok.Most nézzük a getop függvényt, amelynek feladata a következő operátor vagy operandus előkészítése. A megoldás egyszerű: olvasni kell a bemeneti karaktersorozatot és a szóközöket, ill. tabulátorokat át kell ugrani. Ha a következő értékes karakter nem számjegy vagy tizedespont, akkor annak értékével vissza kell térni a hívó függvénybe, egyébként pedig össze kell gyűjteni a számjegyeket (amelyek között előfordulhat a tizedespont is) és vissza kell térni a SZAM értékkel, ami jelzi, hogy egy szám van összegyűjtve. A teljes getop függvény:

#include <ctype.h>

int getch(void);
void ungetch(int);

/*getop: megadja a következő operátort
vagy számot (operandust) */
int getop(char s[ ])
{
   int i, c;

   while((s[0] = c = getch( )) == ' ' || c == '\t')
      ;
   s[1] = '\0';
   if (!isdigit(c) && c != '.')
      return c; /* nem szám */
   i = 0;
   if(isdigit(c)) /*összegyűjti az egészrészt*/
      while(isdigit(s[++i] = c = getch( )))
         ;
   if (c == '.') /* összegyűjti a törtrészt */
      while(isdigit(s[++i] = c = getch()))
         ;
   s[i] ='\0';

   if(c != EOF)
      ungetch(c);
   return SZAM;
}

Mit csinál a getch és ungetch függvény? Gyakran a programban nem lehet megállapítani, hogy mikor olvastunk eleget a bemenetről, csak ha már túl sokat olvastunk. Itt egy ilyen eset, amikor a számokat gyűjtjük össze: amíg az első nem szám karaktert meg nem találtuk, addig a szám még nem teljes. De amikor a program beolvassa az első nem szám karaktert, akkor már túlfutott az olvasással, erre a karakterre még nincs felkészülve.A probléma megoldható, ha lehetőségünk van a feleslegesen beolvasott karaktert „nem beolvasottá” tenni. Így, ha a program bármikor egy karakterrel többet olvasott, akkor ezt a karaktert „visszateheti” és a fennmaradó karaktersorozat úgy viselkedik, mintha ezt a karaktert soha nem olvastuk volna be. Szerencsére a „nem beolvasottá” tétel egyszerűen szimulálható egy együttműködő függvénypárral. A getch fogja szolgáltatni a vizsgálathoz a következő bejövő karaktert, az ungetch pedig elvégzi a karakter „visszaírását” a bemenetre, így a következő getch hívás ismét ezt a karaktert veszi elő.

A két függvény együttműködése nagyon egyszerű: az ungetch a „visszaírandó” karaktert a két függvény által közösen használt pufferba (karakteres tömbbe) helyezi. A getch ebből a pufferből olvas, ha van benne valami és hívja a getchar függvényt, ha a puffer üres. Az aktuális karakter pufferbeli helye egy indexváltozóval adható meg.

Mivel a puffert és az indexváltozót a getch és az ungetch közösen használja és értéküket a két hívás között is meg kell tartani, ezért a két függvényre nézve külső változók. A getch és ungetch függvények, valamint a közös változók:

#define BUFSIZE 100

char buf[BUFSIZE]; /*az ungetch puffere */
int bufp = 0; /* a puffer következő szabad helye */

int getch(void)    /* a következő (esetleg korábban
                  visszaírt) karakter bevétele */ 
{
   return (bufp > 0) ? buf[--bufp] : getchar( ); 
}

void ungetch(int c) /* visszaír egy karaktert a bemenetre */ 
{
   if(bufp >= BUFSIZE)
      printf("ungetch: puffertúlcsordulás\n");
   else
      buf[bufp++] = c;
}

A standard könyvtárban is található egy ungetc nevű függvény, amely egyetlen karaktert „ír vissza” a bemenetre. Erre a 7. fejezetben még visszatérünk. A példánkban egy karakteres puffer helyett egy tömböt használtunk, ami sokkal általanosabb megközelítése a problémának.

4.3. gyakorlat. Adott a kalkulátorprogram váza. Bővítsük ezt ki a modulus (%) operátorral és gondoskodjunk a negatív számok (egyoperandusú ) kezeléséről!

4.4. gyakorlat. Bővítsük a programot új parancsokkal! Az egyik parancs írja ki a verem tetején lévő elemet anélkül, hogy az a veremből eltűnne, a másik cserélje meg a verem tetején lévő két elemet, a harmadik készítsen másolatot a verem tetején lévő elemről, a negyedik pedig törölje a vermet.

4.5. gyakorlat. Tegyük lehetővé, hogy a kalkulátorprogramunk hozzáférjen olyan könyvtári függvényekhez, mint sin, exp és pow. Ezek a függvények a <math.h> headerben vannak, aminek a leírása a B. Függelék 4. pontjában található.

4.6. gyakorlat. Bővítsük úgy a programot, hogy képes legyen változók kezelésére is. (Ezt könnyű megvalósítani, ha 26 változót engedünk meg, és minden változó nevéül az angol ábécé egy betűjét választjuk.) Rendeljünk egy változót a legutoljára kiírt értékhez is.

4.7. gyakorlat. Írjunk ungets(s) néven függvényt, amely egy teljes karaktersorozatot „visszaír” a bemenetre! Az ungets függvény kezelje közvetlenül a buf és bufp változókat, vagy egyszerűen csak használja az ungetch függvényt?

4.8. gyakorlat. Tegyük fel, hogy soha nem akarunk egynél több karaktert „visszaírni” a bemenetre. Módosítsuk ennek megfelelően a getch és ungetch függvényeket!

4.9. gyakorlat. A példában használt getch és ungetch függvények nem kezelik helyesen a „visszaírt” EOF karaktert. Határozzuk meg a helyes EOF kezelés módját és egészítsük ki ezzel a programtervet!

4.10. gyakorlat. Tegyük fel, hogy egy getline függvénnyel a teljes bemeneti sort egyszerre olvassuk be. Ekkor nincs szükség a getch és ungetch függvényekre. Gondoljuk át, hogy ez hogyan módosítja a kalkulátorprogramot!

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

A C nyelvű programban szereplő összes függvényt és külső változó definíciót nem szükséges egyszerre fordítani, a forrásprogram több független állományban tartható és a korábban már lefordított részek a könyvtárból betölthetők. Ezzel kapcsolatban a következő fontos kérdések merülnek fel:

  • Hogyan lehet a deklarációkat úgy megírni, hogy a változók a fordítás során megfelelően deklaráltak legyenek?
  • Hogyan kell a deklarációkat elrendezni ahhoz, hogy betöltéskor a program minden része megfelelően kapcsolódjon egymáshoz?
  • Hogyan szervezzük meg a deklarációkat, hogy mindegyik csak egyszer forduljon elő?
  • Hogyan lehet a külső változókat inicializálni?

A kérdések megválaszolásához szervezzük át a kalkulátorprogramunkat úgy, hogy az több forrásállományban legyen. Gyakorlatilag a kalkulátorprogram túl kicsi ahhoz, hogy értelmesen részekre bontsuk, de mégis jól mutatja a teendőket nagyobb programok esetén.Egy név érvényességi tartománya (hatásköre) az a programrész, amiben a nevet használhatjuk. A függvény kezdetén deklarált automatikus változó érvényességi tartománya az a függvény, amelyben deklarálták. A helyi (lokális) változók neve más függvényben ismeretlen. Ugyanez igaz a függvények paramétereire is, mivel ezek valójában helyi változók.

A külső változók vagy függvények érvényességi tartománya a deklaráció helyén kezdődik és az éppen fordított forrásállomány végéig tart. Például, ha a main, sp, val, push és pop egy állományban van definiálva az előbbi sorrendben, vagyis

main( ) {...}

int sp = 0;

double val[MAXVAL];

void push(double f) {...}

double pop(void) {...}

akkor az sp és val változók egyszerűen, a nevük megadásával használhatók a push és pop függvényekben, külön deklarációra nincs szükség. Ugyanakkor ezek a nevek (a push és pop függvénynevekkel együtt) ismeretlenek a main számára.Másrészről, ha egy külső változóra a definiálása előtt hivatkozunk, vagy más forrásállományban definiáltuk, mint ahol használjuk, akkor kötelező az extern deklaráció.

Fontos, hogy megkülönböztessük a külső változók deklarálását és definiálását. A deklaráció a változó tulajdonságait (elsősorban a típusát) írja le, a definíció viszont ezenkívül még tárterületet is rendel hozzá. Ha az

int sp;
double val[MAXVAL];

sorok bármely függvényen kívül jelennek meg, akkor definiálják az sp és val külső változókat, tárterületet rendelnek hozzájuk és a forrásállomány további része számára deklarációként is működnek. Másrészt az

extern int sp;
extern double val[ ];

sorok a forrásállomány további része számára deklarálják, hogy sp int típusú és hogy val double típusú tömb (amelynek méretét máshol adjuk meg), de nem rendelnek tárterületet ezekhez a változókhoz.A külső változókat csak a forrásállományok egyikében kell definiálni, a többi állományban csak extern deklaráció van, amelyen keresztül ezek a változók elérhetők. (A definíciót tartalmazó állományban is lehet extern deklaráció.) A tömbök méretét a definícióban kell megadni, de opcionálisan szerepelhet az extern deklarációban is.

A külső változók inicializálása csak a definícióval együtt történhet.

Bár nem nagyon valószínű, de tegyük fel, hogy a push és pop függvényeket az egyik forrásállományban definiáltuk és inicializáltuk. Ekkor az összekapcsolásukhoz az alábbi definíciók és deklarációk szükségesek:

Az 1. állományban:
   extern int sp;
   extern double val[ ];
   void push(double f) {...}
   double pop(void) {...}

A 2. állományban:
   int sp = 0;
   double val[MAXVAL];

Mivel az 1. állományban az extern deklarációk a függvénydefiníciókon kívül vannak, ezért a teljes 1. állomány számára elegendő ez a deklaráció. Ugyanez a szervezés szükséges akkor is, ha az sp és val definícióját egy állományban megelőzi a rájuk való hivatkozás.

4.5. A header állományok

Most ismét foglalkozzunk a kalkulátorprogramunk több állományba osztásával, mintha az egyes részek sokkal nagyobbak lennének. A main függvény menne a main.c nevű állományba; a push, pop és a változóik egy másik, stack.c nevű állományba; a getop a getop.c nevű állományba; végül a getch és ungetch a negyedik, getch.c nevű állományba.Azért választottuk szét az egyes részeket egymástól, mert egy tényleges programban is külön lefordított könyvtárakat használunk.

Most már csak egy nehézséget kell megoldanunk: a definíciók és deklarációk szétosztását az egyes állományok között. Amennyire csak lehetséges, igyekszünk a definíciókat és deklarációkat centralizálni, hogy csak egy példányt kelljen karbantartani és figyelemmel kísérni. Ennek megfelelően ezt a közös definíciós-deklarációs részt egy calc.h nevű header állományba helyezzük és szükség esetén include utasítással hozzáfűzzük a programhoz. (A #include utasítást a 4.11. pontban tárgyaljuk részletesen.) Az így létrejövő programszerkezet látható a 96. oldalon.

calc.h
#define SZAM ‘0’
void push(double);
double pop(void);
int getop(char[]);
int getch(void);
void ungetch(int);
main.c
#include <stdio.h>
#include <stdlib.h>
#include “calc.h”
#define MAXOP 100
main() {

}
getop.c
#include <stdio.h>
#include <ctype.h>
#include “calc.h”
getop() {

}
stack.c
#include <stdio.h>
#include “calc.h”
#define MAXVAL 100
int sp = 0;
double val[MAXVAL];
void push(double) {

}
double push(void) {

}
getch.c
#include <stdio.h>
#define BUFSIZE 100
char buf[BUFSIZE];
int bufp = 0;
int getch(void) {

}
void ungetch(int) {

}

Az a kívánság, hogy minden egyes programrész csak a feladatához szükséges információkhoz férjen hozzá, valamint a gyakorlati megvalósíthatóság között kompromisszumos döntést kell hozni, de mindenesetre több header állomány kézbentartása nagyon nehéz feladat. Közepes programméretekig valószínűleg a legjobb megoldás, ha egyetlen header állományba foglalunk mindent, ami az egyes programrészek együttműködéséhez szükséges, és csak nagyon nagy programoknál alkalmazunk bonyolultabb szervezést és több header állományt.

4.6. A statikus változók

A stack.c állomány sp és val változói, valamint a getch.c állomány buf és bufp változói az egyes forrásállományokban lévő függvények saját változói, és bárhol máshol nem hozzáférhetők. A külső változókra vagy függvényekre alkalmazott static deklaráció az objektum érvényességi tartományát az éppen fordított forrásállomány fennmaradó részére korlátozza. Így a külső változók static deklarálása jó lehetőséget nyújt az ungetch-getch függvénypár buf és bufp változóihoz hasonló változók (amelyeknek a közös használat miatt külső változóknak kell lenni) más függvények vagy programrészek előli elrejtésére (pl. a fenti példában a getch vagy ungetch felhasználói nem látják a buf vagy bufp változókat, bár azok külső változók).A statikus tárolási osztály a normális deklaráció elé írt static szóval deklarálható. Ha, mint a következő példában, a két változó és a két függvény egyetlen forrásállományból lesz lefordítva, akkor semmilyen más függvény nem férhet a buf és bufp változókhoz, és neveik ugyanezen program más forrásállományaiban szabadon használhatók.

static char buf[BUFSIZE];   /* az ungetch puffere */
static int bufp = 0;     /*a következő szabad hely a
                           pufferban */
int getch(void) { ... }
void ungetch(int c) { ... }

A push és pop veremkezelési műveletei ugyanígy elrejthetők, ha az sp és val változókat static típusúnak deklaráljuk.A külső statikus deklarációt leggyakrabban változókra alkalmazzák, de függvényekre is használható. Normális körülmények közt a függvények nevei globálisak, a teljes program számára ismertek. Ha egy függvényt static tárolási osztályúnak deklarálunk, akkor a neve a deklarációt tartalmazó forrásállományon kívül nem ismert.

A statikus tárolási osztály a belső változókra is alkalmazható. A belső statikus változók a megfelelő függvényre nézve lokálisak, csakúgy, mint az automatikus változók, de ellentétben azokkal állandóan megmaradnak (az automatikus változók a függvény hívásakor jönnek létre és a függvényből visszatérve megszűnnek). Ez azt jelenti, hogy a belső static deklarálású változók a függvény saját, állandó tárolóhelyei lehetnek.
4.11. gyakorlat. Módosítsuk a getop függvényt úgy, hogy ne kelljen használnia az ungetch függvényt! Segítség: használjunk belső statikus változót!

4.7. Regiszterváltozók

A register deklaráció azt tudatja a fordítóprogrammal, hogy az így deklarált változót nagyon gyakran fogjuk használni. Az elképzelés az, hogy a register deklarálású változót a számítógép regiszterébe helyezzük, ami kisebb méretű és gyorsabb programot eredményez. A fordítóprogramnak lehetősége van figyelmen kívül hagyni a deklarációt. A register deklaráció általános alakja:

register int x;
register char c;

A register tárolási osztály csak automatikus változókra és függvények formális paramétereire írható elő. Ez utóbbi eset

f(register unsigned m, register long n)
{
   register int i;
   ...
}

módon valósítható meg.A gyakorlatban a regiszterváltozókra az alkalmazott hardver miatt megszorítások érvényesek. Egy függvényen belül csak néhány változó lehet regiszteres és csak bizonyos típusú változók. A felesleges register deklarációk nem okoznak problémát, mivel a felesleges számú vagy nem megengedett típusú változók deklarálásából a register szó törlődik. További megszorítás, hogy nem hivatkozhatunk a regiszterváltozó címére (ezzel a kérdéssel az 5. fejezetben még foglalkozunk), függetlenül attól, hogy aktuálisan egy regiszterben helyezkedik-e el vagy sem. A regiszterváltozókra vonatkozó specialitások és korlátozások gépről gépre változnak.

4.8. Blokkstruktúra

A Pascalhoz vagy hasonló nyelvekhez viszonyítva a C nyelv nem egy blokkstrukturált nyelv, mivel a függvények nem definiálhatók más függvények belsejében. Másrészről viszont a függvények belsejében a változók blokkstrukturált módon deklarálhatók. A változó deklarációja (és vele együtt az inicializálása) bármelyik összetett utasítást kezdő bal oldali kapcsos zárójel után következhet, nem csak a függvény kezdetén. Az így deklarált változók rejtve maradnak a külső blokkok azonos nevű változói elől, és csak addig léteznek, amíg a vezérlés el nem jut a blokk záró, jobb oldali kapcsos zárójeléig. Például az

if (n > 0) {
   int i; /* itt új i változót deklarálunk */
   for (i = 0; i < n; i++)
      ...
}

programrészben az i változó érvényességi tartománya az if utasítás igaz feltételhez tartozó ága, és nincs semmiféle kapcsolata a blokkon kívüli i változóval. Egy adott blokkban a deklarációval együtt inicializált automatikus változó a blokkba való minden belépéskor újra inicializálódik. A static tárolási osztályúnak deklarált változó csak a blokkba való első belépéskor inicializálódik.Az automatikus változók (beleértve a formális paramétereket is) szintén rejtve maradnak az azonos nevű külső változók és függvények elől. Nézzük a következő deklarációkat:

int x;
int y;

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

A függvény belsejében x-re paraméterként hivatkozhatunk és double típusú, ezzel szemben a függvényen kívül külső tárolási osztályú, int típusú változó. Ugyanez igaz az y-ra is.A bemutatott példák ellenére érdemesebb elkerülni az azonos nevű, eltérő érvényességi tartományú változónevek használatát, mivel túl nagy a hibázás lehetősége.

4.9. Változók inicializálása

Az inicializálásról már többször beszéltünk, de mindig csak érintőlegesen, más téma kapcsán. Ebben a pontban összegezzük az inicializálás szabályait, figyelembe véve a tárolási osztályokról eddig elmondottakat.Explicit inicializálás hiányában a külső és statikus változók kezdeti értéke garantáltan nulla lesz, az automatikus és regiszterváltozók kezdeti értéke viszont határozatlan.

Skaláris változók a definíciójukkal együtt inicializálhatók, a nevüket követő egyenlőségjel után írt kifejezéssel. Például:

int x = 1;
char aposztrof = '\'';
long nap = 1000L * 60L * 60L * 24L;
      /* egy nap hossza ms-ban */

Külső és statikus változók esetén a kezdeti érték csak állandó kifejezéssel adható meg és az inicializálás csak egyszer, a program végrehajtásának kezdete előtt jön létre. Automatikus és regiszterváltozók esetén az inicializálás minden alkalommal megtörténik, amikor a vezérlés a függvényre vagy blokkra kerül.Automatikus vagy regiszterváltozók esetén a kezdeti érték nem csak állandó lehet, kezdeti értékként megengedett bármilyen, korábban definiált értékű változót vagy függvényhívást tartalmazó kifejezés is. Például a 3.3. pontban leírt bináris kereső program inicializáló része

int binsearch(int x, int v[ ], int n)
{
   int also = 0;
   int felso = n - 1;
   int kozep;
   ...
}

alakban is írható az ott látott

int also, felso, kozep;

also = 0;
felso = n - 1;

alak helyett. Az automatikus változók ilyen inicializálása lényegében az értékadó utasítás rövidítéseként fogható fel. Ízlés dolga, hogy az inicializálás melyik alakját részesítjük előnyben. A könyvben általában az explicit értékadást használjuk, mivel a deklarációban elhelyezett kezdeti érték nehezebben vehető észre, a program pedig nehezebben követhető.Tömbök szintén inicializálhatók a deklarációjukkal együtt, a deklarációt követő egyenlőségjel után kapcsos zárójelbe írt kezdetiérték-listával. A lista egyes elemeit vessző választja el. Például a hónapokban lévő napok számát tartalmazó napok tömb az

int napok[ ] = {31, 28, 31, 30, 31, 30, 31,
                31, 30, 31, 30, 31};

módon inicializálható. Ha a tömb mérete a deklarációból hiányzik, akkor a fordítóprogram a kezdeti értékek leszámolásával meghatározza a tömb hosszát (a fenti példában a tömb hossza 12).Ha a tömb deklarált méreténél kevesebb kezdeti értéket adunk meg, akkor a külső, statikus és automatikus tárolási osztály esetén a hiányzó elemek nulla kezdeti értéket kapnak. Hibát csak az okoz, ha a kezdeti értékek száma nagyobb, mint a tömb mérete. Más nyelvekkel ellentétben nincs mód arra, hogy egy kezdeti értéket ismétlési tényezővel több elemhez is hozzárendeljünk, vagy hogy egy tömb közbenső eleméhez kezdeti értéket rendeljünk anélkül, hogy a többi elem értéket kapna.

A karaktertömbök inicializálása speciális módon megy végbe: a hozzárendelt karaktersorozat kapcsos zárójelek és elválasztó vesszők nélkül adható meg. Például:

char minta[ ] = "dal";

adható meg, ami a vele egyenértékű

char minta[ ] = {'d', 'a', 'l', '\0'};

alak rövidebb változata. Ebben az esetben a tömbnek négy eleme van, a három karakter és a \0 végjelzés.

4.10. Rekurzió

A C nyelv függvényei rekurzívan használhatók: egy függvény közvetlenül vagy közvetetten hívhatja saját magát. Vizsgáljuk meg a számot, mint karaktersorozatot kiíró programunkat. Ahogy elmondtuk, a számjegyek rossz sorrendben keletkeznek, az alacsonyabb helyiértékű számjegy előbb áll rendelkezésünkre, mint a magasabb helyiértékű, amivel a kiírást kezdeni kellene. A probléma megoldására két lehetőség van. Az egyik, hogy a számjegyeket a keletkezésük sorrendjében egy tömbbe tároljuk, majd fordított sorrendben írjuk ki (így működött a 3.6. pontban bemutatott itoa példaprogramunk is). A másik lehetőség egy rekurzív program, amelyben a printd függvény először saját magát hívja meg, hogy feldolgozhassa a magasabb helyiértékű számjegyeket, majd utána írja csak ki az utolsó számjegyet. Ez a változat is hibás eredményt adhat a legnagyobb negatív szám esetén. A printd függvény programja:

#include <stdio.h>

/* printd: n szám kiírása decimális formában */
void printd(int n)
{
   if (n < 0) {
      putchar ('-');
      n = -n;
   }
   if (n / 10)
      printd(n/10);
   putchar (n % 10 + '0');
}

Amikor egy függvény rekurzívan hívja saját magát, minden híváskor az automatikus változók új készletével kezdi a munkát. Ez az új változókészlet teljesen független a korábbi hívásokkor keletkező készletektől. Így pl. a printd(123) esetén a printd első hívásakor az n = 123 argumentumot kapja, amiből n = 12 argumentumot ad át a második printd híváskor és n = 1 argumentumot a harmadik híváskor. A harmadik híváskor a printd kiírja az 1 értéket, visszatér a második hívási szintre, ahol kiírja a 2 értéket, végül visszatérve az első hívási szintre kiíródik a 3 érték és a folyamat befejeződik.Egy másik jó példa a rekurzióra a quicksort rendező algoritmus, amit C. A. R. Hoare 1962-ben dolgozott ki. Az algoritmus lényege, hogy adott egy tömb, amelynek egy elemét kiválasztjuk, a többi elemet pedig két részhalmazra osztjuk úgy, hogy az egyikbe a kiválasztott elemnél nagyobb vagy azzal egyenlő, a másikba pedig az annál kisebb elemek kerüljenek. Ezt az eljárást ezután rekurzívan alkalmazzuk a két részhalmazra. Amikor egy részhalmaz kettőnél kevesebb elemet tartalmaz, már nem szükséges tovább rendezni és leállítjuk a rekurziót.

Az itt ismertetett quicksort programunk nem a lehetséges leggyorsabb változat, de mindenesetre az egyik legegyszerűbb. A programban a részhalmazokra (résztömbökre) osztáshoz a középső elemet használjuk.

/* qsort: a v[bal] ... v[jobb] tömb rendezése növekvő sorrendbe */
void qsort(int v[ ], int bal, int jobb)
{
   int i, utolso;
   void swap(int v[ ], int i, int j);

   if (bal >= jobb) /* semmit nem csinál, ha */
      return; /* kettőnél kevesebb elemből áll */

   swap(v, bal, (bal + jobb)/2); /* a kiválasztott */
   utolso = bal; /* elemet a v[0] helyre rakja */

   for (i = bal + 1; i <= jobb; i++) /* felbontás */
      if (v[i] < v[bal])
         swap (v, ++utolso, i);

   swap(v, bal, utolso); /* a kiválasztott elem helyretétele */
   qsort(v, bal, utolso-1);
   qsort(v, utolso+1, jobb); 
}

A felcserélő műveletet önálló, swap nevű függvényként írtuk meg, mivel a qsort három helyen is használja.

/* swap: v[i] és v[j] felcserélése */
void swap(int v[ ], int i, int j)
{
   int temp;

   temp = v[i];
   v[i] = v[j];
   v[j] = temp;
}

A standard könyvtár tartalmazza a qsort egy általános változatát, amellyel bármilyen típusú objektumok rendezhetők.A rekurzióval tényleges tárterületet nem tudunk megtakarítani (csak a forrásprogram lesz rövidebb), mivel az éppen feldolgozott értékek számára egy veremtárat kell fenntartani. A rekurzív program nem is gyorsabb, viszont előnye, hogy a program szövege tömörebb és a rekurzív programot gyakran egyszerűbb megírni, ill. megérteni, mint a nem rekurzív változatot. A rekurzió különösen kényelmes a rekurzívan definiált adatstruktúrák (pl. fák) esetén, és ezzel a kérdéssel a 6.5. pontban még foglalkozunk.
4.12. gyakorlat. A printd függvényben alkalmazott elgondolást felhasználva írjuk meg az itoa függvény rekurzív változatát! A függvény rekurzív hívásokkal alakítson egy egész számot karaktersorozattá.

4.13. gyakorlat. Írjuk meg az s karaktersorozatot helyben megfordító reverse(s) függvény rekurzív változatát!

4.11. A C előfeldolgozó rendszer

A C nyelv a fordítás önálló első meneteként beiktatható előfeldolgozó rendszerrel bizonyos nyelvi kiterjesztéseket tesz lehetővé. A leggyakrabban használt lehetőség, hogy a fordítás során egy másik állomány tartalmát is beépíthetjük a forrásprogramunkba az #include paranccsal és hogy a #define paranccsal lehetőségünk van egy kulcsszót tetszőleges karaktersorozattal helyettesíteni. Ebben a pontban további lehetőségként még a feltételes fordítással és az argumentumot tartalmazó makrókkal fogunk foglalkozni.

4.11.1. Állományok beépítése

Az állománybeépítés egyszerű lehetőséget kínál a #define utasítással létrehozott definíciókból, deklarációkból és más elemekből összeállított részek kezelésére. A programban bárhol előforduló

#include "állománynév"

vagy

#include <állománynév>

alakú programsor a fordítás során kicserélődik a megadott nevű állomány tartalmával. Ha az állománynév idézőjelek között volt, akkor az adott állomány keresése ott kezdődik, ahol a rendszer a forrásprogramot megtalálta. Ha a keresett állomány ott nem található vagy a nevét csúcsos zárójelek között adtuk meg, akkor a keresés egy géptől és rendszertől függő szabály szerint állományról állományra folytatódik. Az így beépített állomány maga is tartalmazhat #include sorokat.Gyakran több #include sor van a forrásállomány elején, amely az egész program számára közös #define utasításokat és a külső változók extern deklarációit tartalmazó állományokat vagy a könyvtári függvények prototípus-deklarációihoz való hozzáférést lehetővé tevő header állományokat (mint pl. az <stdio.h>) építi be a programba. (Szigorúan véve a headerek nem szükségképpen állományok, a kezelésük módja géptől és rendszertől függ.)

Az #include nagy programok deklarációinak összefogására használható előnyösen. Alkalmazásával garantálható, hogy minden forrásállomány azonos definíciókat és változódeklarációkat használ, amivel kizárható néhány nagyon csúnya hiba. Természetesen, ha egy beépített állományt megváltoztatunk, akkor az összes azt felhasználó állományt újra kell fordítani.

4.11.2. Makróhelyettesítés

Egy definíció általánosan

#define név helyettesítő szöveg

alakú, és hatására a makróhelyettesítés egyik legegyszerűbb formája indul el: a névvel megadott kulcsszó minden előfordulási helyére beíródik a helyettesítő szöveg. A #define utasításban szereplő névre ugyanazok a szabályok érvényesek, mint a változók neveire, a helyettesítő szöveg pedig tetszőleges lehet. Általában a helyettesítő szöveg a sor utasítás után fennmaradó része, de hosszú definíciók több sorban is folytathatók, ha az egyes sorok végére a \ jelet írjuk. A #define utasítással definiált név érvényességi tartománya a definíció helyétől az éppen fordított állomány végéig terjed. Egy definícióban felhasználhatunk korábbi definíciókat is. A helyettesítés csak az önálló kulcsszavakra (nevekre) vonatkozik és nem terjed ki az idézőjelek közötti karaktersorozatokra sem. Például hiába egy definiált név az, hogy YES, nem jön létre a helyettesítés a printf(“YES”) utasításban vagy a YESMAN szövegben.A definícióban bármely névhez bármilyen helyettesítő szöveg hozzárendelhető. Például a

#define orokos for(;;) /* végtelen ciklus */

sor egy új szót, az orokos-t (örökös) definiálja a végtelen ciklust előidéző for utasításra.Lehetőség van argumentumot tartalmazó makrók definiálására is, így a helyettesítő szöveg a különböző makróhívásoknál más és más lesz. Példaképp definiáljuk a max nevű makrót a következő módon:

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

Ez a sor hasonlít egy függvényhíváshoz, de nem az, hanem a max makrósoron belüli kifejtése, amelyben a formális paraméter (itt A vagy B) a megfelelő aktuális argumentummal lesz helyettesítve. Így az a programsor, hogy

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

azzal a sorral helyettesítődik, hogy

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

Mindaddig, amíg az argumentumokat következetesen kezeljük, a makró bármilyen adattípus esetén helyes eredményt fog adni, tehát különböző adattípusukhoz nincs szükség különböző max makróra (szemben a függvényekkel, ahol minden adattípushoz saját függvénynek kell tartozni).Ha jól megfigyeljük a max makró kifejtését, akkor észrevehetünk benne egy csapdát. A kifejezést kétszer értékeli ki, ami az inkrementáló-dekrementáló operátorok vagy adatbevitel és adatkivitel esetén hibát (mellékhatást) okoz. Például a

max(i++, j++) /* Hibás!!! */

sorban a kifejtés hatására a nagyobbik argumentum kétszer inkrementálódik. Ügyelnünk kell a zárójelek használatára is, mert megváltozhat a végrehajtási sorrend. Nézzük meg mi történik, amikor a

#define square(x) x * x /* Hibás!!! */

makrót square(z + 1) alakban hívjuk! A kifejtés után a kifejezésben az x helyére z + 1 kerül, így a kifejezés z + l*z + 1 lesz, ami nyilvánvalóan hibás.Mindezek ellenére a makrók használata nagyon hasznos. Ennek jó gyakorlati példája, hogy az <stdio.h> headerben a getchar és putchar gyakran makróként van definiálva, amivel elkerülhető, hogy futás közben minden egyes karakter feldolgozásánál egy járulékos függvényhívás következzen be. Számos függvény a <ctype.h> headerben is makróként van definiálva.

A nevek korábbi definíciója megszüntethető az #undef paranccsal, így elérhető, hogy az

#undef getchar
int getchar(void) { ... }

esetben a getchar tényleg egy függvény legyen és ne a makró.Alapesetben a formális paramétereket nem helyettesíti az előfeldolgozó az idézőjelek közötti karaktersorozatban. Ha viszont a helyettesítő szövegben a paraméter nevét egy # jel előzi meg, akkor a makró kifejtésében az aktuális argumentummal helyettesített paramétert tartalmazó idézőjelek közötti karaktersorozat jelenik meg. Az így kapott karaktersorozatok konkatenációval kombinálhatók. Az előbbieket jól példázza a debug funkcióhoz kidolgozott kiíró makró:

#define dprint(kif) printf(#kif " = %g\n", kif)

Ha ezt a makrót a

dprint(x/y);

formában hívjuk, akkor a makró a

printf("x/y" " = %g\n", x/y);

alakban fejtődik ki, és a karaktersorozatok konkatenálódnak, aminek hatására a végső alakja

printf("x/y = %g\n", x/y);

lesz.Az aktuális argumentumon belül aza \” , és a \ a \\ karakterekkel helyettesítődik, így az eredmény egy legális karakteres állandó lesz.

A C előfeldolgozó ## operátorának hatására a makrókifejtés alatt konkatenálódnak az aktuális argumentumok. Ha a helyettesítő szövegben a paraméter mellett ## van, akkor a kifejtés során a paraméter helyettesítődik az aktuális argumentummal, eltávolítódik mellőle a ## és a körülötte lévő üres hely (szóközök), majd ezután újra megvizsgálódik a teljes szöveg. A működést a paste makrón mutatjuk be, amely konkatenálja a két argumentumát:

#define paste(elso, hatso) elso ## hatso

A makrót paste(nev, 1) formában használva a nev1 szöveg generálódik.A ## operátor beágyazott alkalmazásának szabályai elég bonyolultak, a részleteket az A. Függelékben találhatjuk.
4.14. gyakorlat. Definiáljunk egy swap(t, x, y) makrót, amely felcseréli a két t típusú argumentumát! (A megoldásban segítségünkre lesz a blokkstruktúra.)

4.11.3. Feltételes fordítás

Az előfeldolgozási folyamat közben kiértékelt feltételes utasításokkal lehetőségünk van magának az előfeldolgozásnak feltételektől függő vezérlésére is. Ennek hatására szelektíven iktathatunk be sorokat a programba, a fordítás (előfeldolgozás) során kiértékelt feltételek értékétől függően.Az #if sor hatására az utána álló állandó egész kifejezés (amely nem tartalmaz sizeof, enum vagy kényszerített típusú [cast] állandókat) kiértékelődik, és ha ennek értéke nem nulla, akkor a következő sorok az első #endif, #elif vagy #else utasításig beépülnek a programba. (Az előfeldolgozó #elif utasítása hasonló a C else if utasításához.) A defined (név) kifejezés értéke 1 az #if utasításban, ha a név már definiálva volt, és 0 különben.

Például, ha biztosak szeretnénk lenni abban, hogy a hdr.h állomány tartalma csak egyszer, de egyszer legalább beépül a programba, akkor a hdr.h állomány beépítési helyének környezetébe az alábbi utasításokat kell elhelyezni:

#if !defined(HDR)
#define HDR

/* ide épül be a hdr.h tartalma */

#endif

A hdr.h első beépülése a programba definiálja a HDR nevet, ezért a következő beépülési kísérletnél a név már definiált, így az előfeldolgozó átugorja az #endif-ig terjedő részt. Hasonló módon lehet megakadályozni más állományok többszöri beépítését is. Ha ezt a szerkezetet következetesen használjuk, akkor az egyes header állományok saját maguk beépíthetik a számukra szükséges további header állományokat anélkül, hogy a felhasználónak bármit is tudnia kellene a headerek kapcsolatáról. Az alábbi vizsgálatsorozatban a SYSTEM név dönti el, hogy melyik headerváltozatot kell a programba beépíteni:

#if SYSTEM == SYSV
   #define HDR "sysv.h"
#elif SYSTEM == BSD
   #define HDR "bsd.h"
#elif SYSTEM == MSDOS
   #define HDR "msdos.h"
#else
   #define HDR "default.h"
#endif
#include HDR

Az #ifdef és #ifndef sorok speciális vizsgálatot végeznek: azt ellenőrzik, hogy az adott név definiált-e vagy sem. Ezek felhasználásával az első példát úgy is írhattuk volna hogy

#ifndef HDR
#define HDR

/* ide épül be a hdr.h tartalma */

#endif
3. FEJEZET Tartalom 5. FEJEZET



megosztom