Alapismeretek
Előszó Tartalom 2. FEJEZET

1. FEJEZET:

Alapismeretek


Kezdjük a C nyelv tanulását az alapfogalmakkal! Az a célunk, hogy a nyelv elemeit működőképes programokon keresztül mutassuk be, anélkül, hogy belemennénk a részletekbe, formális szabályokba és a kivételek tárgyalásába. Ezért nem törekszünk a teljességre vagy pontosságra, de természetesen ettől függetlenül a leírt példák helyesek. El szeretnénk érni, hogy az olvasó a lehető leggyorsabban hasznos kis programokat írjon, emiatt ebben a fejezetben csak az alapfogalmakra (változók, állandók, aritmetika, vezérlési szerkezetek, függvények, ill. az egyszerű adatbevitel és -kivitel) koncentrálunk. Szándékosan nem foglalkozunk a C nyelv olyan lehetőségeivel, amelyek elsősorban a nagyobb programok írásánál szükségesek. Ezek közé tartozik a mutatók és struktúrák használata, a C nyelv gazdag operátorkészletének jelentős része, néhány vezérlési szerkezet és a standard könyvtár.

Ennek a megközelítésnek természetesen hátrányai is vannak: a legsúlyosabb, hogy a nyelv egy elemét leíró összes információt a fejezet rövidsége miatt itt nem adhatjuk meg és ebből félreértések keletkezhetnek. A másik gond, hogy a példaprogramok nem használhatják ki a C nyelv összes lehetőségét, így nem olyan tömörek és elegánsak, mint ahogy szeretnénk. Mindent elkövettünk, hogy ezeket a hátrányokat csökkentsük, de kérjük az olvasót, hogy az itt elmondottakat vegye figyelembe a fejezet tanulmányozása során. Az előzőek miatt a későbbi fejezetekben kénytelenek leszünk ismétlésekbe bocsátkozni, de reméljük, hogy ez inkább segíti az olvasót a megértésben, mintsem bosszantaná.

A tapasztalt programozók természetesen már ebből a fejezetből is kikövetkeztethetik a számukra szükséges további tudnivalókat. A kezdőknek javasoljuk, hogy az itteni példákhoz hasonló kis programokat írjanak.

Az 1. fejezetet a kezdő és tapasztalt programozók egyaránt keretként használhatják a 2. fejezettel kezdődő részletes leíráshoz.

1.1. Indulás


Egy új programozási nyelv elsajátításának egyetlen útja, hogy az adott nyelven programokat írunk. Az első példaprogram minden nyelv tanulásának kezdetén előfordul. A feladat, hogy nyomtassuk ki a következő szöveget:
Halló mindenki!
A feladat megoldása számos problémát vet fel: képesnek kell lennünk egy program létrehozására, annak sikeres lefordítására, betöltésére, futtatására, és ki kell találnunk, hogy a kiírt szöveg hol jelenik meg. Ezeken a rutin jellegű részleteken túljutva a többi már viszonylag egyszerű.

A C nyelvben a „Halló mindenki!” szöveget kiíró program a következő módon néz ki:

#include <stdio.h>

main()
{
   printf("Halló mindenki!\n");
}
A program futtatásának módja az általunk használt rendszertől függ. Például a UNIX operációs rendszer alatt a programot egy olyan forrásállományban kell létrehozni, amelynek neve .c-re végződik. Ilyen név lehet pl. az, hogy hallo.c. Az így elkészített forrásprogramot a
cc hallo.c
paranccsal le kell fordítani.

Ha a program beírásakor nem hibáztunk (pl. nem hagytunk ki betűket vagy nem írtunk valamit hibásan), akkor a fordítás rendben megtörténik és egy a.out nevű végrehajtható állomány keletkezik. Ezt az

a.out
paranccsal futtathatjuk. Ennek hatására a kimeneten megjelenik a
Halló mindenki!
szöveg.

Más operációs rendszer alatt dolgozva természetesen más szabályok érvényesek. Ha gondunk támad, forduljunk a helyi viszonyokat ismerő szakemberhez.

Most némi magyarázatot fűzünk a mintaprogramunkhoz. Egy C nyelvű program, bármilyen méretű is legyen, függvényekből és változókból áll. A függvény utasításokat tartalmaz, amelyek meghatározzák, hogy a számítás menetét hogyan kell végrehajtani, és a változók azokat az értékeket tárolják, amelyekkel a számolást végre kell hajtani. A C nyelv függvényei hasonlóak a FORTRAN szubrutinjaihoz, ill. függvényeihez vagy a Pascal eljárásaihoz, ill. függvényeihez. A példaprogramunk egy main nevű függvényből áll. Általában a függvény neve tetszőleges lehet, de a main egy speciális név és a program végrehajtása mindig a main elején kezdődik. Ebből következik, hogy minden programban valahol kell hogy legyen egy main.

A main a feladat végrehajtása érdekében általában további függvényeket hív, és ezeket vagy a programmal együtt mi írjuk, vagy a függvénykönyvtárban találhatók. A példaprogram első

#include <stdio.h>
sora éppen azt mondja meg a fordítóprogramnak, hogy a fordítás során a programba foglalja bele a standard bemeneti/kimeneti könyvtárra vonatkozó információkat. Ez a sor a legtöbb C nyelvű forrásprogram elején megtalálható. A standard könyvtárat a 7. fejezetben és a B. Függelékben írjuk le.

A függvények közti adatcsere egyik módszere, hogy a hívó függvény adatokból álló listát, az ún. argumentumokat adja át a hívott függvénynek. A függvény neve utáni () zárójelek ezt az argumentumlistát határolják. A mintapéldánkban a main-t olyan függvényként definiáltuk, amelynek nincs argumentuma és ezt a () üres lista jelöli.

Az első C nyelvű program
#include <stdio.h> beépíti a standard könyvtárra vonatkozó információkat;
main() definiál egy függvényt main() névvel, argumentumok nélkül;
{ kapcsos zárójelek határolják a main()-t alkotó utasításokat;
printf("Halló mindenki!\n"); main a printf könyvtári függvényt hívja a kívánt szöveg kiíratásához; a \n egy újsor-karaktert jelöl
}  


A függvényt alkotó utasításokat a {} kapcsos zárójelek határolják. A main függvény csak egyetlen utasítást tartalmaz:
printf("Halló mindenki!\n");
Egy függvényt a nevével és az azt követő, zárójelben elhelyezett argumentumlistával hívunk, így ez az utasítás a printf függvény hívása a "Halló mindenki!\n" argumentummal. A printf egy könyvtári függvény, amely az idézőjelek közti szöveget (karakterláncot) a kimeneti egységre írja ki.

A két felső idézőjel közt elhelyezkedő tetszőleges számú karakterből álló szöveget karakterláncnak (stringnek) vagy karakterlánc-állandónak (stringkonstansnak) nevezzük. Ilyen karakterlánc a mi esetünkben a "Halló mindenki!\n". A könyv ezen bevezető részében a karakterláncokat csak a printf és más függvények argumentumaként használjuk.
A karakterláncban lévő \n jelsorozat a C nyelvben az újsor-karaktert jelöli, amelynek kiírása után a további szöveg a következő sor elején (bal margójánál) kezdve következik. Ha a \n jelsorozatot elhagyjuk (javasoljuk ennek kipróbálását), akkor a kiírás után a kocsi vissza és a soremelés kiírása elmarad. A printf függvény argumentumában az újsor-karaktert csakis a \n jelsorozat beiktatásával kérhetjük, és ha megpróbálkoznánk az argumentum

printf("Halló mindenki!
");
típusú beírásával, akkor a C fordítóprogram hibát jelezne.

A printf soha nem hoz létre automatikusan soremelést, így egyetlen sornyi szöveg printf hívások sorozataként is kiíratható. Ennek megfelelően az első példaprogramunkat így is írhattuk volna:

#include <stdio.h>

main( )
{
   printf("Halló ");
   printf("mindenki!");
   printf("\n");
}
Mindkét változat azonos módon nyomtatja ki a kívánt szöveget.
Megjegyezzük, hogy a \n csak egyetlen karaktert jelent. A \n jelsorozathoz hasonló, ún. escape jelsorozatok általános és jól bővíthető lehetőséget nyújtanak a nehezen nyomtatható vagy nyomtatási képpel nem rendelkező karakterek előállítására. A C nyelvben ilyen escape jelsorozat még a \t a tabulátor, \b a kocsi-visszaléptetés (back-space), \" az idézőjel vagy \\ a fordított törtvonal (backslash) előállítása. Az escape jelsorozatok teljes listája a 2.3. pontban található.

1.1. gyakorlat. Futtassa le a "Halló mindenki!" szöveget kiíró programot a saját rendszerén! Próbálja meg a program egyes részeit elhagyni és figyelje meg milyen hibajelzést ad a rendszer!

1.2. gyakorlat. Próbálja ki, hogy mi történik, ha a printf argumentumában a \x jelsorozat szerepel (ahol \x a fenti listában nem szereplő escape jelsorozat)!

1.2. Változók és aritmetikai kifejezések


A következő példaprogrammal a °C = (5/9) (°F-32) képlet alapján, táblázatos formában ki akarjuk íratni az összetartozó, Fahrenheit- (°F) és Celsius-fokban (°C) mért hőmérsékletértékeket a következő formában:
0        -17
20       -6
40       4
60       15
80       26
100      37
120      48
140      60
160      71
180      82
200      93
220      104
240      115
260      126
280      137
300      148
A program most is egyetlen, main nevű függvény definiálásából áll, hosszabb az előző példaprogramnál, de nem bonyolultabb annál. Ebben a programban már néhány új fogalmat (megjegyzés, deklaráció, változók, aritmetikai kifejezések, ciklus, formátumozott adatkivitel) is bevezetünk. Maga a példaprogram a következő:
#include <stdio.h>

/* Fahrenheit-fok-Celsius-fok táblázat kiírása
F = 0, 20, ..., 300 Fahrenheit-fokra */
main()
{
   int fahr, celsius;
   int also, felso, lepes;
   also = 0;      /* a táblázat alsó határa */
   felso = 300;   /* a táblázat felső határa */
   lepes = 20;    /* a táblázat lépésköze */

   fahr = also;
   while (fahr <= felso) {
      celsius = 5 * (fahr-32) / 9;
      printf("%d\t%d\n", fahr, celsius);
      fahr = fahr + lepes; 
   }
}
A program első két sora, a
/* Fahrenheit-fok-Celsius-fok táblázat kiírása
F = 0, 20, ..., 300 Fahrenheit-fokra */
sorok egy megjegyzést alkotnak (comment), amely röviden leírja a program működését és feladatát. Bármilyen, /* és */ közt elhelyezkedő szöveget a C fordítóprogram figyelmen kívül hagy, ezért ide tetszőleges, a program megértését és használatát segítő szöveget írhatunk. A programban bárhol lehet megjegyzés, ahol betűköz, tabulátor vagy új sor előfordulhat.

A C nyelvben minden változót a használata előtt deklarálni kell, ami általában a függvény kezdetén, a végrehajtható utasítások előtt történik. A deklaráció változók tulajdonságait írja le és egy típus megadásából, valamint az adott típusú változók felsorolásából áll, mint pl.:

int fahr, celsius;
int also, felso, lepes;
Az int típus azt jelenti, hogy a felsorolt változók egész (integer) értéket vehetnek fel, ellentétben a float típus megadásával, amely lebegőpontos értékű változót – azaz olyan változót, amelynek értéke törtrészt is tartalmaz – jelöl. Az int és float típusú változók pontossága és lehetséges nagysága a használt számítógéptől függ. Gyakran 16 bites int típusú változókat használnak, amelyek értéke -32 768 és +32 767 közé eshet, de előfordul 32 bites int típusú változó is. A float típusú számokat általában 32 biten ábrázolják, legalább hat értékes számjegy pontossággal és az abszolút értékük 10-38-tól 10+38-ig terjedhet.

Az int és float típuson kívül a C nyelv még további adattípusokat is értelmez. Ilyen a

char      karakter, egy bájton ábrázolva;
short     rövid egész típusú szám;
long      hosszú egész típusú szám;
double    kétszeres pontosságú lebegőpontos (valós) szám.
Ezen adattípusok méretei szintén a felhasznált számítógéptől függenek. Ezekből az elemi (alap)adattípusokból épülnek fel az összetett adattípusok: tömbök, struktúrák és unionok. Az adott típusú adatokra mutatók (pointerek) mutathatnak és a függvények adott típusú értékkel térnek vissza az őket hívó függvényhez. Mindezekről a későbbiekben még szó lesz. A hőmérséklet-átszámító program számítási műveletei az
also = 0;
felso = 300;
lepes = 20;
fahr = also;
értékadó utasításokkal kezdődnek, amelyek a felhasznált változók kezdeti értékeit állítják be. Az egyes utasításokat a pontosvessző zárja.

A táblázat minden sorát azonos módon kell kiszámítani, így a soronként egyszer ismétlődő számítások megvalósítására ciklust használunk, amelyet a while utasítással alakítunk ki a következő módon:

while (fahr <= felso) {
   ...
}
A while utasítással szervezett ciklus működése a következő: futás közben a számítógép megvizsgálja a zárójelben elhelyezett kifejezést, és ha az igaz (fahr kisebb vagy egyenlő, mint felso), akkor végrehajtja a kapcsos zárójelek közti ciklusmagot (esetünkben ez három utasításból áll). Ezután a gép a feltételt újra megvizsgálja, és ha ismét igaz az értéke, akkor a ciklusmag újra végrehajtódik. Ha egyszer a vizsgálat eredménye hamis lesz (fahr értéke nagyobb lesz, mint felso), a ciklus befejeződik, és a program a ciklust követő utasítással folytatódik. A példaprogramunkban a ciklus után már nincs újabb utasítás, így a program befejeződik.

A while utasítás ciklusmagja egy vagy több, kapcsos zárójelek közt elhelyezett utasításból (mint a hőmérséklet-átalakító programban) vagy egyetlen, kapcsos zárójelek nélkül elhelyezett utasításból állhat. Ez utóbbit szemlélteti a következő példa:

while (i < j)
   i = 2 * i;
A while hatáskörébe tartozó utasításokat mindkét esetben beljebb írtuk, hogy világosan kitűnjön, mely utasítások tartoznak a ciklusmaghoz. Ez a beljebb kezdés a program logikai szerkezetét hangsúlyozza. A C fordítóprogramok eléggé kötetlenül kezelik az utasítások elhelyezését, a bekezdések és üres helyek csak a program olvasását és megértését segítik. Célszerű, ha soronként csak egy utasítást írunk és az operátorok előtt, ill. után írt szóközzel tesszük egyértelművé a tagolást. A zárójelek elhelyezkedése kevésbé kritikus, erre egyéni stílust alakíthatunk ki, vagy átvehetjük valamelyik, éppen divatos stílust. Bármilyen, nekünk tetsző formát választhatunk, de célszerű, ha a későbbiekben ehhez következetesen ragaszkodunk.

A program a munka zömét a ciklusmagban végzi. A Celsius-fokban mért hőmérsékletet kiszámító és a Celsius nevű változónak értékül adó utasítás

celsius = 5 * (fahr-32) / 9;
Az ok, ami miatt először 5-tel szorzunk, majd 9-cel osztunk az 5/9-del való szorzás helyett az, hogy a C nyelv – több más programozási nyelvhez hasonlóan – az egész számok osztásánál csonkít, az eredmény törtrészét elhagyja. Mivel 5 és 9 egész számok, 5/9-ed csonkított eredménye nulla, így minden Celsius-fok értéke nulla lenne.

Ez a példaprogram egy kicsivel többet is bemutat a printf függvény működéséből. A printf általános célú, formátumozott kimenetet előállító függvény, amelyet részletesen a 7. fejezetben fogunk ismertetni. A függvény első argumentuma a kinyomtatandó karakterlánc, amelyben a % jelek mutatják, hova kell az első (második, harmadik stb.) argumentum(ok) értékét behelyettesíteni és milyen formában kell azokat kiírni. Például a %d egy egész típusú argumentumot jelöl ki, így a

printf("%d\t%d\n", fahr, celsius);
utasítás két egész típusú változó (fahr és celsius) értékét fogja kiírni, köztük egy tabulátort (\t) elhelyezve. Az első argumentum minden egyes % jeles konstrukciójához egy megfelelő második, harmadik stb. argumentum párosul. A % konstrukciókból és a további argumentumokból álló pároknak szám és típus szerint meg kell egyeznie, különben hibás eredményt kapunk.

Egyébként a printf nem része a C nyelvnek, a nyelvben magában nincs definiálva az adatbevitel és -kivitel. A printf csak egy hasznos függvény, ami a C programok által hozzáférhető standard könyvtárban található. A printf viselkedését az ANSI szabvány definiálja, így a szabványon keresztül a függvény tulajdonságai minden fordítóprogram és könyvtár számára azonosak.

Azért, hogy a figyelmünket főleg a C nyelvnek szentelhessük, az adatok beviteléről és kiviteléről a 7. fejezetig nem sokat beszélünk. Elsősorban a formátumozott adatátvitel tárgyalását halasztjuk későbbre. Ha numerikus adatokat akarunk a programmal beolvastatni, akkor a 7.4. pontban olvassuk el a scanf függvényre vonatkozó részeket. A scanf hasonló a printf függvényhez, csak adatkiírás helyett adatot olvas.

A hőmérséklet-átalakító programunknak számos baja van. Az egyik legegyszerűbben megszüntethető hiba, hogy a kiírás nem túl szép, mivel a számok nincsenek jobbra igazítva. Ezen könnyű segíteni: ha a printf utasításban lévő %d konstrukciót a szélességet megadó résszel egészítjük ki, akkor a számok a rendelkezésükre álló mezőben jobbra igazodva jelennek meg. Például azt írhatjuk, hogy

printf("%3d %6d\n", fahr, celsius);
akkor az egyes sorokban az első szám három számjegy széles, a második szám pedig hat számjegy széles mezőbe íródik az alábbiak szerint:
0     -17
20     -6
40      4
60     15
80     26
100    37
...   ...
A legkomolyabb probléma az egész aritmetika használatából adódik, mivel a kapott Celsius-fok értékek nem túl pontosak. Pl. a 0 °F-nek a -17,8 °C felel meg és nem pedig a táblázatban szereplő -17 °C. Pontosabb eredményt kapunk, ha az egészaritmetika helyett lebegőpontos aritmetikát használunk. Ez a program kis változtatását igényli. Ennek megfelelően a program második változata:
#include <stdio.h>

/* Fahrenheit-fok-Celsius-fok táblázat kiírása F = 0, 20,
..., 300 Fahrenheit-fokra; a program lebegőpontos
változata */
main ( ) {
   float fahr, celsius;
   int also, felso, lepes;
   also = 0;      /* a táblázat alsó határa */
   felso = 300;  /* a táblázat felső határa */
   lepes = 20;   /* a táblázat lépésköze */
   fahr = also;
   while (fahr <= felso) {
      celsius = (5.0/9.0) * (fahr-32.0);
      printf("%3.0f %6.1f\n", fahr, celsius);
      fahr = fahr + lepes;
   }
}
A program lényegében azonos az előző változattal, csak a fahr és celsius változók float-ként lettek deklarálva és az átalakítást megadó képletet sokkal inkább a megszokott formában írtuk. Az előző programban nem írhattunk 5/9-det, mert az egészosztás okozta csonkítás miatt az eredmény nulla lett volna. Az új képlet állandóiban a tizedespont jelzi, hogy lebegőpontos számokról van szó, így az 5.0 / 9.0 nem csonkul, hiszen két lebegőpontos szám hányadosa is lebegőpontos szám.

Ha egy aritmetikai operátornak egész típusú operandusai vannak, akkor a gép az egész számokra érvényes műveletet fogja elvégezni. Ha egy aritmetikai operátor egyik operandusa lebegőpontos, a másik pedig egész típusú, a művelet végrehajtása előtt az egész típusú operandus automatikusan lebegőpontossá konvertálódik. Amennyiben a képletben fahr-32 szerepelne, a 32 automatikusan lebegőpontos számmá alakulna. Ennek ellenére célszerű a lebegőpontos állandókban a tizedespontot akkor is kiírni, ha a szám éppen egész értékű, mivel ez az olvasó számára jobban kihangsúlyozza a szám lebegőpontos jellegét.

Az egész típusú adatok lebegőpontossá alakításának részletes szabályaival a 2. fejezetben foglalkozunk. Pillanatnyilag csak azt jegyezzük meg, hogy a

fahr = also;
értékadás, valamint a
while (fahr <= felso)
vizsgálat az előbb elmondottak szerint működik, azaz az int típusú adatok a végrehajtás előtt float típusúvá alakulnak.

A printf függvényben szereplő %3.0f konverziós előírás azt jelenti, hogy a lebegőpontos szám (a mi esetünkben a fahr) legalább három karakter széles mezőbe lesz kinyomtatva, tizedespont és törtrész nélkül. A %6.1f egy másik szám (a celsius) kiírását specifikálja: ez legalább hat karakter széles mezőben lesz kinyomtatva, amiből egy számjegy a tizedespont után van. Az így kiírt táblázat a következő:

0     -17.8
20     -6.4
40      4.4
...     ...
A szélesség vagy pontosság hiányozhat is a specifikációból: a %6f azt írja elő, hogy a szám legalább hat karakter széles mezőbe nyomtatódik; a %.2f azt, hogy a számnak a tizedespont után még két karaktere lehet és a teljes szélességére nincs előírás; a %f pedig pusztán csak azt jelzi, hogy a számot lebegőpontos formában kell kiírni. A következőben bemutatunk néhány formátumspecifikációt:
%d       a számot decimális egészként írja ki;
%6d      a számot decimális egészként, legalább hat karakter széles mezőbe írja ki;
%f       a számot lebegőpontosként írja ki;
%6f      a számot lebegőpontosként, legalább 6 karakter széles mezőbe írja ki;
%.2f     a számot lebegőpontosként, két tizedessel írja ki;
%6.2f    a számot lebegőpontosként, legalább 6 karakter széles mezőbe, két tizedessel írja ki.
Többek közt a printf függvény a %o specifikációt az oktális, a %x specifikációt a hexadecimális, a %s specifikációt a karakterlánc típusú kiíráshoz, és a %% specifikációt pedig a % jel kiírására használja.

1.3. gyakorlat. Módosítsuk a hőmérséklet-átalakító programot úgy, hogy a táblázat fölé fejlécet is nyomtasson!

1.4. gyakorlat. Írjunk programot, amely a Celsius-fokban adott értékeket alakítja Fahrenheit-fokká!

1.3. A for utasítás

Az egyes feladatok megoldására többféle módon írhatunk programot. Próbáljuk meg a hőmérséklet-átalakító programunk következő változatát:
#include <stdio.h>

/* Fahrenheit-fok-Celsius-fok átszámítási táblázat */
main ( )
{
   int fahr;

   for (fahr = 0; fahr <= 300; fahr = fahr+20)
      printf("%3d %6.1f\n", fahr, (5.0/9.0)*(fahr-32));
}

Ez a program ugyanazt csinálja, mint az előző, de attól szemlátomást különbözik. Az egyik legjelentősebb változás, hogy eltűnt a változók többségének deklarálása, csak a fahr maradt meg, int típusúként. Az alsó és felső határt, ill. a lépésközt csak állandóként szerepeltetjük a for utasításban, ami maga is új a számunkra. A Celsius-fokot kiszámító kifejezés sem önálló utasítás, hanem a printf függvény harmadik argumentumaként szerepel.

Ez az utóbbi változtatás egy teljesen általános szabályra mutat példát: minden olyan összefüggésben, ahol valamilyen típusú változó értékét használjuk, megengedett egy ugyanolyan típusú összetett kifejezés használata is. Mivel a printf harmadik argumentumának a %6.1f specifikációhoz illeszkedően egy lebegőpontos számnak kell lennie, ezen a helyen bármilyen lebegőpontos kifejezés megadható.

A for utasítás szintén egy ciklusszervező utasítás, a while utasítás általánosítása. Ha összehasonlítjuk a korábban használt while utasítással, a for működése teljesen világos. A zárójelek között három, egymástól pontosvesszővel elválasztott rész található. Az első, kezdeti értékadó rész

fahr = 0
amit csak egyszer hajt végre a program, a ciklusba való belépés előtt. A második rész a ciklust vezérlő ellenőrzés vagy feltétel
fahr <= 300
alakú. Működés közben a gép megvizsgálja ezt a feltételt, és ha igaz, akkor végrehajtja a ciklusmagot (ami most csak egyetlen printf utasítás). Ezután történik a lépésközzel való növelés
fahr = fahr + 20
amit a feltétel újbóli ellenőrzése követ. A ciklus akkor fejeződik be, ha a feltétel értéke hamis lesz. Csakhogy, mint a while utasításnál, a ciklusmag itt is lehet egyetlen utasítás vagy kapcsos zárójelek között elhelyezett utasításcsoport. A kezdeti értékadás, feltételvizsgálat és a lépésközzel való növelés tetszőleges kifejezéssel adható meg.

A while és a for között szabadon választhatunk aszerint, hogy számunkra melyik tűnik világosabbnak. A for utasítás általában akkor előnyös, ha a kezdeti értékadás és lépésközzel való növelés egy-egy logikailag összefüggő utasítás, mivel ekkor a for utasítással szervezett ciklus sokkal tömörebb a while utasítással szervezett ciklusnál és a ciklust vezérlő utasítások egy helyen vannak.

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

1.4. Szimbolikus állandók

Mielőtt elbúcsúznánk a hőmérséklet-átalakító programunktól, még egy észrevételt teszünk: nagyon rossz gyakorlat a 300-hoz vagy a 20-hoz hasonló „bűvös számokat” beépíteni a programba. Ezek később, a program olvasásakor nem sokat mondanak és az esetleges megváltoztatásuk nagyon nehézkes. Az ilyen bűvös számok kiküszöbölésének egyik módja, hogy „beszélő” neveket rendelünk hozzájuk. A #define szerkezet lehetővé teszi, hogy egy megadott karakterlánchoz szimbolikus nevet vagy szimbolikus állandót rendeljünk, az alábbiak szerint:
#define név helyettesítő szöveg
Ezután a fordítóprogram a név minden önálló előfordulásakor (amikor a név nincs idézőjelek közt vagy nem része egy másik névnek) a név helyett a megadott helyettesítő szöveget írja be. A névre ugyanazok a szabályok vonatkoznak, mint a változók nevére: betűkből, ill. számjegyekből állhat, és betűvel kell kezdődnie. A helyettesítő szöveg tetszőleges karaktersorozat lehet, nem csak szám. A #define használatát a következő példán mutatjuk be.
#include <stdio.h>

#define ALSO  0    /* a táblázat alsó határa */
#define FELSO 300  /* a táblázat felső határa */
#define LEPES 20   /* a táblázat lépésköze */

/* a Fahrenheit-fok-Celsius-fok táblázat kiírása */
main( )
{
   int fahr;

   for (fahr = ALSO; fahr <= FELSO; fahr = fahr + LEPES)
   printf("%3d %6.1f\n", fahr, (5.0/9.0)*(fahr-32));
}
Az ALSO, FELSO, LEPES szimbolikus állandók és nem változók, így nem szerepelnek a deklarációkban. A szimbolikus neveket általában nagybetűkkel írjuk (de ez nem kötelező), hogy megkülönböztethetők legyenek a kisbetűvel írt változónevektől. Megjegyezzük, hogy a define szerkezet végén nincs pontosvessző.

1.5. Karakteres adatok bevitele és kivitele

A következőkben néhány egymással összefüggő, karakteres adatok feldolgozására alkalmas programot ismertetünk. A későbbiekben látni fogjuk, hogy számos bonyolult program ezeknek a példaprogramoknak a kibővített változata.
A karakteres adatok be- és kivitelének standard könyvtárral támogatott megvalósítása nagyon egyszerű. Szövegek be- és kivitelét - függetlenül attól, hogy honnan erednek vagy hová irányulnak - karakterek áramaként fogjuk fel. A szövegáram legalább két sorból álló karakteráram (karaktersorozat), amelynek mindegyik sora nulla vagy annál több karakterből áll és a végén egy újsor-karakter helyezkedik el. A standard könyvtár feladata, hogy az adatáramok be- és kivitelét a fenti modell alapján kezelje. A C nyelvet használó programozó ezeket a könyvtári függvényeket használja, és nem törődik azzal, hogy az egyes sorok a programon kívül mit jelentenek.

A standard könyvtárban számos olyan függvény van, amelyekkel egy időben egy karakter olvasható vagy írható, és ezen függvények közül a legegyszerűbb a getchar és putchar függvény. Minden egyes hívásakor a getchar függvény a szövegáramból beolvassa a következő karaktert és annak értékét adja vissza a hívó függvénynek. Ennek megfelelően a

c = getchar( )
végrehajtása után a c változó a bemenő szöveg következő karakterét fogja tartalmazni. A karakterek általában a terminálról (billentyűzetről) érkeznek, az adatállományból történő beolvasással a 7. fejezetben fogunk foglalkozni.

A putchar függvény minden egyes hívásakor kiír egy karaktert. A

putchar(c)
végrehajtása során a c egész típusú változó tartalma mint egy karakter íródik ki, általában a képernyőre. A putchar és a printf hívások felváltva is történhetnek, ilyenkor a kimenet a hívások sorrendjében fog megjelenni.

1.5.1. Állománymásolás

A getchar és putchar felhasználásával nagyon sok programot írhatunk a bemenet és a kimenet pontos ismerete nélkül. A legegyszerűbb ilyen mintaprogram a bemenetet karakterenként átmásolja a kimenetre. A program szerkezete:
egy karakter beolvasása
while (a karakter nem az állományvége-jel)
az éppen beolvasott karakter kimenetre írása
egy új karakter beolvasása
Mindez C nyelvű programként megfogalmazva:
#include <stdio.h>

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

A while utasításban szereplő != operátor jelentése „nem egyenlő”.
A billentyűzeten vagy képernyőn megjelenő karakter a számítógépen belül bitmintaként tárolódik. A char típus egy ilyen karakteres adat tárolására alkalmas tárolóhelyet specifikál, de erre a célra bármilyen egész típusú adat is alkalmas. A programban a karakter tárolására int típusú változót használtunk egy bonyolult, de lényeges okból.

A program működése során a fő probléma az érvényes bemeneti adatok végének érzékelése. A probléma úgy oldható meg, ha a getchar függvény egy olyan értékkel tér vissza a bemeneti adatok elfogyása esetén, ami semmilyen más, létező karakterrel nem téveszthető össze. Ezt az értéket EOF-nak, (end of file), állományvége-jelnek nevezik. A programban a c változót úgy kellett deklarálni, hogy elegendően nagy legyen bármilyen, a getchar által visszaadott érték tárolására. Ezért a c változó nem lehet char típusú, mivel az nem elegendően nagy az EOF befogadására, így int típusú változót használunk.

Az EOF az <stdio.h> headerben definiált egész érték, amelynek konkrét értéke mindaddig nem lényeges, amíg különbözik bármely char típusú értéktől. Az EOF szimbolikus állandó használatával garantálható, hogy egyetlen program működése sem függ az EOF tényleges számértékétől.

A gyakorlottabb C programozók a fenti másolóprogramot sokkal tömörebben írnák meg. A C nyelvben a

c = getchar()
jellegű értékadások kifejezésekbe is beépíthetők és értékük megegyezik az értékadás bal oldalának értékével. Ez azt jelenti, hogy egy értékadás egy nagyobb kifejezés része lehet. Ha az egy karaktert a c változóhoz rendelő értékadást a while ciklus ellenőrző részébe építjük be, akkor a másoló program a következő módon fog kinézni:
#include <stdio.h>

/* a bemenet átmásolása a kimenetre - 2. változat */
main( )
{
int c; while ((c = getchar()) != EOF) putchar(c); }
A program a while ciklusban beolvas egy karaktert, hozzárendeli a c változóhoz, majd megvizsgálja, hogy a beolvasott karakter megegyezik-e az állományvége-jellel. Ha nem, akkor a while ciklusmagja végrehajtódik és a karakter kiíródik, majd a ciklusmag ismétlődik. Amikor a bemeneti karaktersorozat véget ér, a while ciklus befejeződik és ezzel együtt a main is.

A programnak ez a változata egy helyre koncentrálja az adatbeolvasást - csak egyszer szerepel benne a getchar függvény hívása -, így rövidebbé, ill. olvashatóbbá válik a program. Ezzel a programozási stílussal gyakran találkozunk. Bár a túlzott tömörítésnek megvan az a veszélye, hogy a program áttekinthetetlenné válik, ezt a továbbiakban is igyekszünk elkerülni.

A while utasításban az értékadás körüli zárójelek feltétlenül szükségesek, mivel a != operátor precedenciája nagyobb, mint az = operátoré és ezért a zárójelek hiányában először a reláció kiértékelése történne meg, és csak ezt követné az értékadás. A zárójelet elhagyva a

c = getchar() !=EOF
utasítást kapjuk, ami egyenértékű a
c = (getchar() != EOF)
utasítással, aminek nem kívánt következménye, hogy a c változóhoz a 0 vagy 1 értéket rendeli attól függően, hogy a getchar az állományvége-jelet olvasta-e vagy sem. A kérdéskörrel a 2. fejezetben még részletesen foglalkozunk.

1.6. gyakorlat. Igazoljuk, hogy a getchar( ) != EOF kifejezés értéke valóban 0 vagy 1!

1.7. gyakorlat. Írjunk programot, ami kiírja az EOF értékét!

1.5.2. Karakterek számlálása

A következő példaprogram a másolóprogramhoz hasonlóan működik és megszámlálja a beolvasott karaktereket.
#include <stdio.h>

/* a beolvasott karaktereket számláló program */
/* 1. változat */
 main( )
{
   long nc;

   nc = 0;
   while (getchar( ) != EOF)
      ++nc; 
   printf("%ld\n", nc); 
}
A programban szereplő
++nc;
utasításban egy új operátor, a ++ található, amelynek jelentése: növelj eggyel (inkrementálás). Ehelyett természetesen azt is írhatnánk, hogy nc = nc+1, de a ++nc sokkal tömörebb és gyakran hatékonyabb is. Létezik a -- operátor is, ami az eggyel való csökkentést (dekrementálás) valósítja meg. A ++ és -- operátor egyaránt lehet előtag (prefix) és utótag (postfix) operátor (++nc, ill. nc++ vagy --nc, ill. nc--). A kétféle forma a kifejezésben különböző értéket ad, ennek pontos leírásával a 2. fejezetben találkozunk, de a lényeg az, hogy mind a ++nc, mind az nc++ növeli az nc értékét. Egyelőre mi a prefix formát használjuk.

A karaktereket számláló program a kapott számot int helyett long típusú változóban tárolja. Az int típusú változók max. értéke 32 767 lehet, ami viszonylag kicsi, és számlálóként int típusú változót használva hamar túlcsordulás jelentkezne. A long típusú egész számot a legtöbb számítógép legalább 32 biten ábrázolja (bár néhány számítógépen az int és a long típusú változók egyaránt 16 bitesek). A %ld konverziós specifikáció azt jelzi a printf függvénynek, hogy a megfelelő argumentum long típusú egész szám.

Sokkal nagyobb számokig is elszámlálhatnánk, ha double (kétszeres pontosságú lebegőpontos) változót használnánk.

A ciklusszervezés másik módjának szemléltetésére a while helyett használjuk a for utasítást.

#include <stdio.h>

/* a beolvasott karaktereket számláló program */
/* 2. változat */
main ( )
{
   double nc;
   for (nc = 0; getchar() != EOF; ++nc)
      ;
   printf("%.0f\n", nc);
}

A kiíratásban a float és double típusú adatokhoz egyaránt használhatjuk a %f specifikációt, és a %.0f specifikáció elnyomja a tizedespont és a törtrész kiírását (a törtrész hossza nulla jegy).

A ciklusmag üres, hiszen minden műveletet az ellenőrző és növelő részben végzünk el. A C nyelv szintaktikai (nyelvtani) szabályai viszont megkövetelik, hogy a for utasítással szervezett ciklusnak legyen magja. Az önmagában álló pontosvessző, azaz nulla (vagy üres) utasítás ezt a követelményt kielégíti. Az üres utasítást külön sorba írtuk, hogy kihangsúlyozzuk a fontosságát.

Mielőtt befejeznénk a karaktereket számláló program tárgyalását, felhívjuk a figyelmet arra, hogy ha a bemeneten egyáltalán nincs adat, akkor a getchar első hívása után a while vagy a for vizsgáló része hamis eredményt ad, a program nulla számú karaktert számol, azaz helyesen működik, ami nagyon fontos. A dolog a while és a for azon kedvező tulajdonságával kapcsolatos, hogy mindkettő a ciklus elején, a ciklusmag végrehajtása előtt ellenőrzi a feltételt (előtesztelő ciklus). Ha tehát semmit sem kell csinálni, akkor a ciklus valóban nem csinál semmit, még akkor sem, ha emiatt soha nem hajtja végre a ciklusmagot. A programjainknak határesetben (ha a bemeneten nulla hosszúságú adatsor van) is helyesen kell működniük.

1.5.3. Sorok számlálása

A következő példaprogramunk megszámolja a bemenetre adott adatsorokat. Mint korábban már említettük, a standard könyvtár a bemeneti szövegáramot egymást követő sorok sorozataként értelmezi és minden sort az újsor-jel zár. Ebből következik, hogy a sorok számlálása lényegében az újsor-karakterek számlálásának felel meg. Így a program:
#include <stdio.h>

/* a bemenő szöveg sorainak számlálása */
main ( )
{
   int c, nl;

   nl = 0;
   while ((c = getchar( )) != EOF) 
      if (c == '\n')
         ++nl; 
   printf("%d\n", nl); 
}
A while ciklusmagja most egy if utasítást tartalmaz, amely a ++nl inkrementáló utasítás végrehajtását vezérli. Az if utasítás ellenőrzi a zárójelben lévő feltételt, és ha az igaz, akkor végrehajtja a következő utasítást (vagy a kapcsos zárójelek közt elhelyezett utasításcsoportot). A program elrendezése most is világossá teszi, hogy mi mit vezérel.

Az == kettős egyenlőségjel jelentése a C nyelvben az „egyenlő valamivel” (hasonlóan a Pascal = jeléhez vagy a FORTRAN .EQ. operátorához). Az == szimbólumot azért vezették be, hogy az egyenlőség vizsgálatát megkülönböztessék az = jellel jelölt értékadástól. Még egy figyelmeztető megjegyzés: a kezdő C programozók gyakran írnak = jelet ott, ahol == kellene. Amint ezt a 2. fejezetben látni fogjuk, az eredmény általában egy érvényes (de az adott helyen értelmetlen) kifejezés, így a fordítóprogram nem ad hibajelzést.

Két aposztróf között elhelyezett karakter egy egész számot jelent, amelynek értéke a karakter gépi karakterkészletben kódjával egyezik meg. Az aposztrófok között lévő karaktert karakterállandónak nevezzük és helyette használhatjuk a neki megfelelő kis egész számot (kódot) is. Például az 'A' egy karakterállandó, amelynek értéke az ASCII karakterkészletben 65, vagyis ez a szám az A belső, gépi ábrázolása. Természetesen 'A' helyett 65-öt is írhatnánk, de az 'A' jelentése sokkal világosabb és független az éppen használt karakterkészlettől.

Az escape sorozatok karakterállandókénti megadása szintén lehetséges, így a '\n' az újsor-karakter értékét jelenti, ami az ASCII karakterkészletben 10. Ne feledjük, hogy a '\n' egyetlen karakter és a kifejezésekben egyetlen számnak felel meg, az "\n" pedig egy karaktersorozat (string), ami adott esetben csak egy karaktert tartalmaz. A karakterek és karaktersorozatok témájával szintén a 2. fejezetben foglalkozunk majd részletesebben.

1.8. gyakorlat. Írjunk programot, ami megszámolja a bemenetre adott szövegben lévő szóközöket, tabulátorokat és újsor-karaktereket!

1.9. gyakorlat. Írjunk programot, ami a bemenetre adott szöveget úgy másolja át a kimenetre, hogy közben az egy vagy több szóközből álló karaktersorozatokat egyetlen szóközzel helyettesíti!

1.10. gyakorlat. Írjunk programot, ami a bemenetre adott szöveget úgy másolja át a kimenetre, hogy közben a tabulátorkaraktereket \t, a visszaléptetés- (backspace) karaktereket \b és a fordított törtvonal- (backslash) karaktereket \\ karakterekkel helyettesíti! Ezzel az átírással a tabulátor- és visszaléptetés-karakterek a nyomtatásban is láthatóvá válnak.

1.5.4. Szavak számlálása

A sorozat negyedik programja a sorokon és a karaktereken kívül megszámolja a bemenetre adott szövegben lévő szavakat is. A szó számunkra olyan tetszőleges karaktersorozatot jelent, amelyben nem fordul elő a szóköz-, tabulátor- vagy újsor-karakter. A szó fenti, elég laza definíciója alapján működő program (ami a UNIX wc segédprogramjának váza) a következő:
#include <stdio.h>

#define BENN 1 /* a szó belseje */
#define KINT 0 /* a szón kivül */

/* a bemenetre adott szövegben lévő sorok,
szavak és karakterek számolása */ 
main( ) 
{
   int c, nc, nl, nw, allapot;

   allapot = KINT;
   nl = nw = nc = 0;
   while ((c = getchar( )) != EOF) {
      ++nc;
      if (c == '\n')
         ++nl;
      if (c == ' ' || c == '\n' || c == '\t')
         allapot = KINT; 
      else if (allapot == KINT) {
         allapot = BENN;
         ++nw; 
      } 
   }
   printf("%d %d %d\n", nl, nw, nc); 
}
Amint a program megtalálja egy szó első karakterét, azonnal növeli a szószámlálót (nw). Az allapot változó azt jelzi, hogy pillanatnyilag a szó belsejében vagyunk-e vagy sem. Kezdetben „nincs a szóban”, amit a hozzárendelt KINT érték jelez. A programban a BENN és a KINT szimbolikus állandók használata előnyösebb az 1 és 0 értékeknél, mivel a program olvashatóbbá válik. Az olvashatóság ebben a kis példában nem okoz nehézséget, de nagy programoknál az áttekinthetőség növekedése lényegesen több hasznot jelent. Az olyan program, amiben a „bűvös számok” helyett szimbolikus állandókat használunk, könnyebben is módosítható.

A program

nl = nw = nc = 0;
sorában mindhárom változóhoz nulla értéket rendelünk. Ez nem egy speciális utasításfajta, hanem abból következik, hogy az értékadásban egy kifejezés szerepel valamilyen értékkel és az értékadások balról jobbra haladva hajtódnak végre. Az előzővel teljesen egyenértékű az
nl = (nw = (nc = 0));
értékadás. A || operátor a logikai VAGY műveletet jelenti, így a
if (c == ' ' || c == '\n' || == '\t'
utasítás azt jelenti, hogy „ha c szóköz vagy c újsor vagy c tabulátor, akkor...”. (Mint korábban már említettük, a \t escape sorozat a tabulátorkaraktert jelzi.) Van egy másik logikai operátor is, az &&, ami a logikai ÉS műveletet jelenti és ennek precedenciája nagyobb, mint a logikai VAGY műveleté. Az && és || operátorokkal összekapcsolt kifejezések kiértékelése balról jobbra történik, és a kiértékelés azonnal félbeszakad, ha a kifejezés igaz vagy hamis volta egyértelművé válik. Ha a c szóköz volt, akkor nincs értelme azt vizsgálni, hogy c tartalma újsor- vagy tabulátorkarakter-e, így ezek a vizsgálatok már nem mennek végbe. Ez itt most nem különösen fontos, de bonyolultabb esetekben, amint azt hamarosan látni fogjuk, nagyon lényeges lehet.

A példában előfordul az else utasítás, ami meghatározza a program működését abban az esetben, ha az if utasítás feltétele hamis volt. Az utasítás általános formája:

if (kifejezés)
   1. utasítás
else
   2. utasítás
Az if-else szerkezetnek mindig csak az egyik utasítása hatásos. Ha a kifejezés igaz, akkor az 1. utasítást, ha nem, akkor pedig a 2. utasítást hajtja végre a program. Az 1. és 2. utasítások önálló utasítások vagy kapcsos zárójelben elhelyezett utasításcsoportok lehetnek. A szavakat számláló programban az else után egy if utasítás áll, ami két másik, kapcsos zárójelbe foglalt utasítást vezérel.

1.11. gyakorlat. Hogyan lehet ellenőrizni a szavakat számláló programot? Milyen bemeneti adatsort kell használni, hogy a legnagyobb valószínűséggel érzékeljük a program esetleges hibáit?

1.12. gyakorlat. Írjunk programot, ami a bemenetére adott szöveg minden szavát új sorba írja ki!

1.6. Tömbök

Írjunk programot, amely megszámlálja, hogy a bemenetre adott szövegben hányszor fordulnak elő az egyes számjegyek, az üres helyet jelentő karakterek (szóköz, tabulátor, új sor), valamint az összes többi karakter! Ez egy elég mesterkélt feladat, de lehetővé teszi, hogy egyetlen programban jól szemléltessük a C nyelv számos lehetőségét.

Mivel a bemeneti adatokat 12 kategóriába kell sorolni, kézenfekvőnek látszik az egyes számjegyek előfordulásainak számát egy tömbben tárolni, tíz külön változó helyett. Ennek megfelelően a program egyik változata a következő:

#include <stdio.h>

/* számjegyeket, üres helyeket és más
karaktereket számláló program*/ 
main ( )
{
   int c, i, nures, nmas; 
   int ndigit[10];

   nures = nmas = 0;
   for (i = 0; i < 10; ++i) 
      ndigit[i] = 0;

   while ((c = getchar()) != EOF)
      if (c >= '0' && c <= '9')
         ++ndigit[c-'0']; 
      else if (c == ' ' || c == '\n' || c == '\t')
         ++nures; 
      else
         ++nmas;

   printf("számok =") ;
   for (i = 0; i < 10; ++i)
      printf(" %d", ndigit[i]);
   printf (", üres = %d, más = %d\n", nures, nmas);
}
A program az eredményt pl. a következő módon írja ki:
számok = 9 3 0 0 0 0 0 0 0 1, üres = 123, más = 345
Az
int ndigit[10];
deklaráció a 10 egész számot tartalmazó ndigit tömböt deklarálja. A C nyelvben a tömbök indexe mindig nullától indul, így az ndigit-nek ndigit[0], ndigit[1], ..., ndigit[9] elemei vannak. Ez tükröződik a tömböt kezdő értékkel feltöltő (inicializáló) és kinyomtató for ciklusokban.

Az index tetszőleges egész típusú kifejezés lehet, ami megadható egy egész típusú változóval (pl. i) vagy egész típusú állandóval.

A példaprogram nagymértékben kihasználja a számjegyek karakteres ábrázolásának tulajdonságait. Így pl. az

if (c >= '0' && c <= '9')
vizsgálat eldönti, hogy a c változóban lévő karakter számjegy-e. Ha az, akkor ennek a számjegynek a numerikus értéke
c - '0'
Ez a módszer természetesen csak akkor alkalmazható, ha a '0', '1', ..., '9' egymást követő, növekvő sorrendű, pozitív egész számok. Szerencsére ez minden karakterkészlet esetén teljesül.

Definíció szerint a char típusú adatok kis egész számok, ezért az aritmetikai kifejezésekben a char és int típusú változók és állandók egyenértékűek. Ez elég természetes és kényelmes megoldás, pl. a c-'0' egész kifejezés értéke 0 és 9 közt van, attól függően, hogy a '0' ... '9' karakterek közül melyik tárolódott a c változóban. Az így kapott érték felhasználható az ndigit tömb indexeként.
Annak eldöntése, hogy a karakter számjegy, üres hely vagy valami más-e, a következő vizsgálatsorozattal lehetséges:

if (c >= '0' && c <= '9')
   ++ndigit[c-'0']; 
else if (c == ' ' || c == '\n' || c == '\t')
   ++nures; 
else
   ++nmas;
Az
if (1.feltétel)
   1. utasítás
else if (2.feltétel)
   2. utasítás
...
   ...
else
   n. utasítás
felépítésű szerkezetek gyakran előfordulnak a programokban, mint a többutas elágazások megvalósításai. A gép a feltételeket felülről kezdi kiértékelni és ha az igaz, akkor végrehajtja a megfelelő utasítást, majd befejezi a szerkezetet. (Természetesen bármelyik utasítás helyett kapcsos zárójelben elhelyezett utasításcsoport is állhat.) Ha egyik feltétel sem teljesül, akkor az utolsó else utáni utasítást hajtja végre. Ha az utolsó else és a hozzá tartozó utasítás hiányzik (mint a szavakat számláló programban), akkor semmi nem történik. Egy programban a kezdeti if és a végső else között tetszőleges számú
else if (feltétel)
   utasítás
felépítésű utasításcsoport lehet.

Stilisztikai szempontból célszerű a példaprogramban bemutatott formát követni, mert így a programsorok nem lesznek túl hosszúak, a hosszú döntési láncok nem nyúlnak a jobb margón túlra.

A 3. fejezetben fogjuk ismertetni a switch utasítást, ami szintén a többutas elágazások leírására alkalmas. Ez főleg akkor használható előnyösen, ha azt vizsgáljuk, hogy egy egész vagy karakteres típusú kifejezés egyezik-e egy állandókból álló halmaz valamelyik elemével. Összehasonlítás céljából a 3.4. pontban bemutatjuk a példaprogramunk switch utasítással megvalósított változatát.

1.13. gyakorlat. Írjunk programot, ami kinyomtatja a bemenetre adott szavak hosszának hisztogramját! A legcélszerűbb, ha a hisztogramot vízszintesen ábrázoljuk, mert a függőleges ábrázolás túl bonyolult lenne.

1.14. gyakorlat. Írjunk programot, ami kinyomtatja a bemenetre adott különböző karakterek előfordulási gyakoriságának hisztogramját!

1.7. Függvények

A C nyelv függvényei megfelelnek a FORTRAN szubrutinjainak vagy függvényeinek, vagy a Pascal eljárásainak vagy függvényeinek. A függvény kényelmes lehetőséget nyújt a programozónak, hogy egy számítási részt önállóan kezelhető, zárt egységbe foglaljon. Ezek a zárt egységek ezután szabadon felhasználhatók anélkül, hogy a konkrét felépítésükkel, megvalósításukkal törődnünk kellene. Megfelelően tervezett és megvalósított függvények esetén teljesen figyelmen kívül hagyhatjuk, hogy hogyan keletkezett a függvény értéke (eredménye), elegendő csak az eredményt tudni. A C nyelvben a függvények használata egyszerű, kényelmes és hatékony. Gyakran találkozunk majd rövid, néhány sorban definiált és csak egyszer meghívott függvényekkel, amelyeknek pusztán csak az a szerepe, hogy a programot áttekinthetővé tegyék.

Ez idáig csak kész, könyvtári függvényekkel (printf, getchar, putchar) találkoztunk, így itt az ideje, hogy magunk is írjunk függvényeket. Mivel a C nyelvnek nincs a FORTRAN-ban értelmezett **-hoz hasonló hatványozó operátora, ezért a függvénydefiniálás bemutatására írjuk meg a power(m, n) függvényt, amely előállítja egy m egész szám n-edik hatványát (n pozitív egész szám). Például a power(2, 5) értéke 32 lesz. A példaként választott függvény nem egy valódi hatványozó eljárás, mivel csak kis egész számok pozitív, egész kitevős hatványait képes kiszámítani. (A standard könyvtár pow(x, y) függvénye egy általános, xy alakú kifejezés értékét határozza meg.)

A következőkben bemutatjuk a power függvényt és az azt hívó main főprogramot (ami maga is függvény), ami alapján a teljes szerkezet elemezhető.

#include <stdio.h>

int power(int m, int n);

/* a hatványozó függvény ellenőrzése */
main()
{
   int i;

   for (i = 0; i < 10; ++i)
      printf("%d %d %d\n", i, power(2, i) , power(-3, i));
   return 0; 
}

/* a power(m, n) függvény az m alapot az n-edik hatványra
emeli, ahol n >= 0 */
int power(int alap, int n) 
{
   int i, p;

   p = 1;
   for (i = 1; i <= n; ++i)
      p = p * alap; 
   return p; 
}
A függvénydefiníció általános alakja:
visszatérési típus   függvénynév (paraméter-deklarációk, ha vannak)
{
      deklarációk
      utasítások
}

A függvénydefiníciók tetszőleges sorrendben szerepelhetnek, egy vagy több forrásállományban (bár természetesen egy függvény nem osztható szét két forrásállományba). Ha a forrásprogram több állományban helyezkedik el, bonyolultabb a fordítás és a betöltés, mintha egyetlen állományban lenne, de ez az operációs rendszer problémája és nem a nyelv jellegzetessége. Pillanatnyilag feltételezzük, hogy mindkét függvény azonos állományban van, így a C programok futtatásáról elmondottak továbbra is érvényesek.
A main a power függvényt kétszer hívja a

printf("%d %d %d\n", i, power (2, i), power(-3, i));
sorban. Mindegyik híváskor két argumentumot adunk át a power függvénynek és az egy egész értékkel tér vissza, amit a hívó program formátumozott formában kiír. Egy aritmetikai kifejezésben a power(2, i) éppen olyan egész mennyiség, mint 2 és i. (Nem minden függvény ad egész értéket, erről majd a 4. fejezetben mondunk többet.) A power függvény első sorában az
int power(int alap, int n)
a paraméterek (argumentumok) típusát és nevét, valamint a függvény visszatéréskor szolgáltatott értékének típusát deklarálja. A power függvény által használt paraméterek nevei a power függvényre nézve lokálisak, azaz egyetlen más függvényben sem „láthatók”, bármely eljárás ugyanezeket a neveket minden gond nélkül saját célra használhatja. Ez szintén igaz a power-ben deklarált i és p változókra is, és a power-ben használt i változónak semmi köze a main-ben használt i változóhoz.

A fogalmak egyértelművé tétele érdekében a továbbiakban paraméternek nevezzük a függvénydefiníció során zárójelben megadott változóneveket, és argumentumnak a függvény hívása során átadott értékeket. Ezekre a fogalmakra néha a formális és aktuális argumentum vagy formális és aktuális paraméter fogalmakat is használják.

A power függvényben kiszámított értéket a return utasítás adja vissza a main függvénynek. A return utasítás után tetszőleges kifejezés állhat a

return kifejezés;
szintaktika szerint. Egy függvénynek nem feltétlenül szükséges értékkel visszatérni a hívó programba. Egy kifejezés nélküli return utasítás visszaadja a vezérlést a hívó programnak, de nem ad át hasznos információt. Ez történik akkor is, ha a vezérlés átlépi a függvény végét jelző jobb oldali kapcsos zárójelet. Természetesen a hívó függvénynek jogában áll nem figyelembe venni a hívott függvény által visszaadott értéket.

Vegyük észre, hogy a main végén is van egy return utasítás. Mivel a main ugyanolyan függvény, mint bármelyik más, ezért visszaadhat egy értéket a hívó programnak. A main hívó programja az a környezet, amiben a program lefut, és a visszaadott értéket ez a környezet használja fel. Tipikusan a nulla visszaadott érték a program normális lefutását jelzi, a nullától különböző érték pedig a program abnormális vagy hibás lefutására utal. Az egyszerűség kedvéért eddig elhagytuk a return utasítást a main függvényeinkből, de ezután használni fogjuk, hogy a programunk egy állapotjelzést adhasson a környezetének.

Az

int power(int m, int n);
deklaráció a main elején azt mondja meg, hogy a power függvény két egész típusú argumentumot vár és egész típusú eredménnyel tér vissza. Ezt a deklarációi függvény-prototípusnak nevezik, és meg kell hogy egyezzen a power függvény definíciójával és használatával. Programozási hiba, ha a függvény definíciója vagy bármilyen használata nem egyezik a függvényprototípussal.

Természetesen a paraméterek neveinek nem kell egyezni, a függvény prototípusaiban a paraméternevek opcionálisak és a power prototípusát így is írhatnánk:

int power(int, int);
Mindenesetre a jól választott nevek jó programdokumentálást tesznek lehetővé, ezért a továbbiakban is gyakran használni fogjuk a neveket a prototípusban.

Végezetül egy történelmi megjegyzést szeretnénk tenni: az ANSI C és a korábbi C változatok közti legnagyobb eltérés éppen a függvények definiálásában és deklarálásában tapasztalható. A C nyelv eredeti definícója szerint a power függvényt a következő módon írtuk volna meg:

/* a power(m, n) függvény az m alapot az n-edik hatványra
emeli, ahol n >= 0 - régi tipusú változat */
power(alap, n)
int alap, n;
{
   int i, p;

   p = 1;
   for (i = 1; i <= n; ++i)
      p = p * alap; 
   return p; 
}
Itt a paraméterek nevét zárójelek között adtuk meg és a típusaikat a nyitó kapcsos zárójel előtt deklaráltuk. A deklarálatlan paraméterek int típusúak lesznek. (A függvény törzse mindkét változatban megegyezik.) A main kezdetén a power függvényt az
int power();
utasítással deklaráltuk volna. Itt nincs megengedve a paraméterlista, így a fordítóprogram nem képes egyszerű módon ellenőrizni, hogy a power függvényt helyesen hívták-e. Valójában kiindulhatnánk az alapfeltételezésből is, és mivel a power int típusú értékkel tér vissza, így az egész deklarációt elhagyhatnánk.

A függvényprototípusok új szintaktikája sokkal egyszerűbbé teszi a fordítóprogram számára az argumentumok számában és típusában elkövetett hibák észlelését. A függvények régi típusú definiálási és deklarálási módja átmenetileg még érvényben van az ANSI C-ben, de javasoljuk, hogy mindenki az új változatot használja, ha a fordítóprogramja támogatja azt.

1.15. gyakorlat. Írjuk át az 1.2. pontban ismertetett hőmérséklet-átalakító programot úgy, hogy az átalakításhoz függvényt használunk!

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

A C nyelv függvényeinek egyik tulajdonságát a más nyelvekben - különösen FORTRAN nyelvben - gyakorlatot szerzett programozók szokatlannak fogják találni: a C nyelvben minden függvényargumentumot érték szerint adunk át. Ez azt jelenti, hogy a hívott függvény mindig az argumentumok értékét kapja meg (átmeneti változókban), azok címe helyett. Ez sok esetben eltérést okoz az olyan nyelvekhez képest, ahol a hívott eljárás az eredeti argumentum címét kapja meg egy helyi másolat helyett (mint pl. a FORTRAN-ban, ahol név szerinti a paraméterátadás, vagy mint pl. a Pascalban, ahol a var deklaráción keresztüli paraméterátadás van).

A legfontosabb különbség ezekhez a nyelvekhez képest, hogy a C nyelvben a hívott függvény közvetlenül nem férhet hozzá és nem változtathatja meg a hívó függvény változóit, csak a saját, átmeneti másolatával dolgozhat.

Az érték szerinti hívás mindenképpen előny. Felhasználásával sokkal tömörebb, kevesebb segédváltozót tartalmazó program írható, mivel a hívott függvényben a paraméterek ugyanúgy kezelhetők, mint az inicializált lokális változók. Az elmondottak illusztrálására nézzük a power függvény egy újabb változatát!

/*a power(m, n) függvény az m alapot az n-edik hatványra
emeli, ahol n >= 0 - 2. változat */ 
int power(int alap, int n)
{
   int p;

   for (p = 1; n > 0; --n)
      p = p*alap;
   return p;
}
A programban az n paramétert átmeneti változóként használtuk és lefelé számláltattuk (a for ciklus csökkenő irányú), amíg csak nulla nem lett. Ennek következtében nincs szükség az i segédváltozóra. Az n értékének power függvényen belüli módosítása semmilyen hatással sincs a híváskor használt eredeti értékre.

Szükség esetén természetesen elérhető, hogy a hívott függvény megváltoztassa a hívó függvény valamelyik változóját. Ehhez a hívott függvénynek meg kell kapnia a kérdéses változó címét a hívó függvénytől (ami a változót megcímző mutató átadásával lehetséges) és a hívott függvényben a paramétert mutatóként kell deklarálni, amelyen keresztül indirekten érhető el a hívó függvény változója. A kérdéssel az 5. fejezetben fogunk részletesebben foglalkozni.

A leírtak nem érvényesek a tömbökre. Ha argumentumként egy tömb nevét adjuk meg, akkor a függvénynek átadott érték a tömb kezdetének helye (címe) lesz, a tömbelemek átmásolása nem történik meg. Ezt az értéket indexelve a hívott függvény a hívó függvény bármelyik tömbeleméhez hozzáférhet. Ezzel a kérdéssel a következő pontban foglalkozunk.

1.9. Karaktertömbök

A C nyelvben valószínűleg a leggyakrabban használt tömbtípus a karaktertömb. Annak bemutatására, hogy hogyan használjuk a karaktertömböket, ill. hogyan manipuláljuk azokat a megfelelő függvényekkel, írjunk egy programot, ami szövegsorokat olvas be és kiírja közülük a leghosszabbat. A program váza viszonylag egyszerű:
while (van további sor)
   if (a sor hosszabb az eddigi leghosszabbnál)
      tárold a sort
      tárold a sor hosszát
nyomtasd ki a leghosszabb sort
A programváz alapján látszik, hogy a program több, jól elkülönülő részre osztható: az első beolvassa, és megvizsgálja, a második pedig eltárolja az új sort, a fennmaradó rész pedig a folyamatot vezérli.
Mivel az egyes részek ilyen jól elkülönülnek, célszerű, ha a programot is ennek megfelelően írjuk meg. Ezért először írjunk egy önálló getline függvényt, aminek feladata, hogy előkészítse a következő sort. Megpróbáljuk a getline függvényt úgy megírni, hogy az más programokban is jól használható, kellően általános legyen. A minimális igény, hogy a getline visszatérésekor jelezze, ha elértük az állomány végét. Sokkal általánosabbá tehetjük a függvényt, ha az visszatéréskor a sor hosszát adja meg, vagy nullát, ha elértük az állomány végét. A nulla hossz biztosan megfelel az állományvégének jelzésére, mivel nem lehet tényleges sorhossz (minden szövegsor legalább egy karaktert, az újsor-karaktert tartalmazza, így a hossza minimálisan 1).

Ha találunk egy sort, ami hosszabb az eddigi leghosszabb sornál, akkor azt valahová el kell tennünk. Erre a célra egy másik, copy nevű függvényt használunk, amelynek feladata az új sor biztos helyre mentése.

Végül szükségünk van egy main-re a getline és a copy vezérléséhez. Az eredmény a következő:

#include <stdio.h>
#define MAXSOR 1000 /* a beolvasott sor max. mérete */

int getline(char sor[ ], int maxsor); 
void copy(char ba[ ], char bol [ ]);

/* a leghosszabb sor kiíratása */
main()
{
   int hossz;         /* az aktuális sor hossza */
   int max;           /* az eddigi maximális hossz */
   char sor[MAXSOR];  /* az aktuális sor */
   char leghosszabb[MAXSOR]; 
   /* ide teszi a leghosszabb sort */
   
   max = 0;
   while ((hossz = getline(sor, MAXSOR)) > 0) 
      if (hossz > max) {
         max = hossz; 
         copy(leghosszabb, sor);
      } 
   if (max > 0) /* volt sor, nem EOF */
      printf("%s", leghosszabb); 
   return 0;
} /* getline: egy sort beolvas az s-be */ /* és visszaadja a hosszát */ int getline(char s[ ], int lim) { int c, i; for (i = 0; i < lim-1 && (c = getchar()) != EOF && c != '\n'; ++i) s[i] = c; if (c == '\n') { s[i] = c; ++i; } s[i] = '\0'; return i; } /* copy: a "ba" helyre másol a "bol" helyről */ void copy(char ba[ ], char bol[ ]) { int i; i = 0; while ((ba[i] = bol[i]) != '\0') ++i; }
A getline és copy függvényeket a program elején deklaráljuk és a programról feltételezzük, hogy egyetlen állományban van.

A main és a getline egy argumentumpáron keresztül tartja a kapcsolatot és a getline egy értékkel tér vissza. A getline argumentumait az

int getline(char s[], int lim)
sorban deklaráltuk, ami azt mondja, hogy az első argumentum (s) egy tömb, a második (lim) pedig egy egész változó. A deklarációban a tömb méretének megadásától eltekinthetünk. Az s tömb méretét a getline függvényben sem kell megadni, mivel azt a main-ben már beállítottuk. A getline függvény a power-hez hasonlóan tartalmaz egy return utasítást, amelyen keresztül egy értéket ad vissza a hívó programnak. A deklaráló sor jelzi, hogy a getline egész típusú értéket ad vissza. Mivel az alapfeltételezés szerint a visszatérési érték int típusú, így a deklarációból ez el is hagyható.

Néhány függvény a hívó programban felhasználható értékkel tér vissza, mások (mint pl. a copy) csak végrehajtanak egy feladatot és nem adnak vissza értéket. A copy függvény visszatérési típusa void, ami explicit módon azt jelzi, hogy nincs visszatérési érték.
A getline a '\0' karaktert (nulla karaktert, amelynek értéke nulla) helyezi a tömb végére, amivel a karaktersorozat (a beolvasott sor) végét jelzi. A C nyelv is ezt a módszert használja a szöveg végének jelzésére. Például a

"halló\n"
karakterlánc-állandó egy tömbként tárolódik el, amelynek egyes elemei a karakterlánc egyes karakterei és a végét a '\0' végjel mutatja az alábbiak szerint:

h a l l ó \n \0

A printf függvényben szereplő %s formátummegadás egy ilyen formában megadott karaktersorozat kiírását jelzi. A copy függvény is kihasználja, hogy a kapott argumentumát a '\0' karakter zárja és ezt a karaktert át is másolja a kimenő argumentumába. (Mindez azt jelenti, hogy a '\0' karakter nem része a beolvasott szövegnek.)

Egyértelmű, hogy még egy ilyen kis programnál is adódnak tervezési problémák. Például felmerül a kérdés: mit csináljon a main, ha a beolvasott sor hosszabb a megadott korlátnál? A getline jól működik: ha megtelt a tömb, leáll, még akkor is, ha nem olvasott újsor-karaktert. A getline-tól kapott hossz és az utolsó karakter alapján a main eldöntheti, hogy a sor túl hosszú volt-e, és ezután tetszése szerint cselekedhet. A program rövidsége miatt ezt az esetet nem vettük figyelembe.
A getline függvény felhasználója nem tudhatja, hogy milyen hosszú a beolvasandó sor, ezért a getline ellenőrzi a túlcsordulást. A copy felhasználója viszont már tudja (vagy megtudhatja) a karaktersorozat hosszát, ezért ott nem alkalmaztunk hibaellenőrzést.

1.16. gyakorlat. Módosítsuk a leghosszabb sort kiíró program main függvényét úgy, hogy helyesen adja meg a tetszőlegesen hosszú bemeneti sor méretét és annak szövegéből a lehető legtöbbet írja ki!

1.17. gyakorlat. Írjunk programot, ami kiírja az összes, 80 karakternél hosszabb bemeneti sort!

1.18. gyakorlat. Írjunk programot, ami eltávolítja a beolvasott sorok végéről a szóközöket és tabulátorokat, valamint törli a teljesen üres sorokat!

1.19. gyakorlat. Írjunk egy reverse(s) függvényt, ami megfordítja az s karaktersorozat karaktereit! Használjuk fel ezt a függvényt egy olyan programban, ami soronként megfordítja a beolvasott szöveget.

1.10. A változók érvényességi tartománya és a külső változók

A main-ben használt változók (pl. leghosszabb, sor stb.) a main saját, lokális változói. Mivel ezeket a main-ben deklaráltuk, így közvetlenül egyetlen más függvény sem férhet hozzájuk. Ez ugyanígy igaz a többi függvényre is, pl. a getline i változójának semmi köze a copy i változójához. A függvény lokális változói csak a függvény hívásakor jönnek létre és megsemmisülnek, amikor a függvény visszaadja a vezérlést a hívó programnak. Ezért az ilyen változókat (más nyelvek szóhasználatához igazodva) automatikus változóknak nevezzük. Ezentúl a lokális változókra való hivatkozáskor az automatikus megnevezést fogjuk használni. (A 4. fejezetben tárgyaljuk majd a static tárolási osztályú változókat, amelyek két függvényhívás közt is megtartják értéküket.)
Mivel az automatikus változók csak a függvény hívásakor léteznek, így a következő hívásig nem őrzik meg az értéküket és minden függvényhíváskor explicit módon értéket kell nekik adni. Ha erről megfeledkeznénk, akkor a tartalmuk határozatlan lesz.

Az automatikus változók mellett olyan változók is definiálhatók, amelyek az összes függvényre nézve külsők, azaz amelyekhez a nevükre hivatkozva bármely függvény hozzáférhet. (Ezek a változók nagyon hasonlítanak a FORTRAN nyelv COMMON változóihoz vagy a Pascal programok legkülső blokkjában deklarált változókhoz.) Mivel ezek a külső (external) változók az egész programra nézve globálisak, felhasználhatók a függvények közti adatcseréhez az argumentumlista helyett. A külső változók állandóan érvényben vannak, és függetlenül a függvények hívásától vagy a függvényből való visszatéréstől, megtartják az értéküket.

A külső változókat csak egyszer, az összes függvényen kívül kell definiálni, és ennek hatására tárolóhely rendelődik hozzájuk. A külső változókat minden olyan függvényben deklarálni kell, amely hozzájuk akar férni. Ez a deklaráció megadja ezen változók típusát a függvényben. A deklaráció történhet explicit módon, az extern utasítással vagy implicit módon, a programkörnyezet alapján. A külső változók használatának megvilágítására írjuk újra a leghosszabb sort kiíró programot úgy, hogy a sor, a leghosszabb és a max változók külső változók legyenek. Ez a függvényhívások, a deklarációk és mindhárom függvénytörzs módosítását igényli.

Az új program:

#include <stdio.h>

#define MAXSOR 1000   /* a beolvasott sor max. mérete */

int max;              /* az eddigi maximális hossz */
char sor[MAXSOR];     /* az aktuális sor*/
char leghosszabb[MAXSOR];
/* ide teszi a leghosszabb sort */

int getline(void); 
void copy(void);

/* a leghosszabb sor kiíratása - speciális változat */
main( )
{
   int hossz; /* az aktuális sor hossza */
   extern int max;
   extern char leghosszabb[MAXSOR];

   max = 0;
   while ((hossz = getline( )) > 0) 
      if (hossz > max) {
         max = hossz; 
         copy( ); 
      } 
      if (max > 0) /* volt sor, nem EOF */
         printf("%s", leghosszabb); 
   return 0;
} /* getline: speciális változat */ int getline(void) { int c, i; extern char sor[ ]; for (i = 0; i < MAXSOR-1 && (c = getchar ( )) != EOF && c != '\n'; ++i) sor[i] = c; if (c == '\n') { sor[i] = c; ++i; } sor[i] = '\0'; return i; } /* copy: speciális változat */ void copy(void) { int i; extern char sor[ ], leghosszabb [ ]; i = 0; while ((leghosszabb[i] = sor[i]) != '\0') ++i; }
A main, getline és copy függvények külső változóit a példaprogram első soraiban definiáltuk, amivel meghatároztuk a típusukat és lefoglaltuk számukra a tárolóhelyet. Szintaktikailag ezek a külső változódefiníciók ugyanolyanok, mint a lokális változók definíciói, de mivel a függvényeken kívül helyezkednek el, ezért külső változót írnak le. Egy függvény csak akkor használhat külső változókat, ha azok nevei már ismertek a számára. Ennek egyik módja, hogy egy extern deklarációt írunk a függvénybe. A deklaráció ugyanolyan, mint a korábbiak, csak az extern alapszó van előtte.

Bizonyos esetekben az extern deklaráció elmaradhat. Ha a külső változó definíciója a forrásállományban megelőzi a változó használatát valamelyik függvényben, akkor ebben a függvényben nem szükséges az extern deklaráció. Példaprogramunkban a main, getline és copy függvények extern deklarációi emiatt feleslegesek. Általános gyakorlat, hogy az összes külső változó definícióját a forrásállomány elejére teszik, és így az összes extern deklaráció elhagyható a programból.

Ha a program több forrásállományban van, és a külső változókat pl. a file1 állományban definiáljuk, és a file2, ill. file3 állományokban használjuk, akkor az extern deklarációra a változók előfordulásának megfelelően szükség van a file2 és file3 állományokban. Szokásos gyakorlat, hogy az összes külső változó extern deklarációját és a függvényeket egy önálló állományba (amit történelmi okokból fejnek vagy header-nek neveznek) gyűjtik és ezt az #include paranccsal az egyes forrásállományokhoz kapcsolják. Az állománynév utáni .h kiterjesztés ilyen header állományt jelöl. A standard könyvtár függvényei pl. a <stdio.h>-hoz hasonló header állományokban vannak deklarálva. A deklarációk részleteit a 4. fejezetben, a könyvtárral kapcsolatos ismereteket a 7. fejezetben és a B. Függelékben tekintjük át.

Mivel a getline és copy függvényeknek az új programváltozatban nincs argumentumuk, arra gondolhatnánk, hogy a forrásállomány elején a prototípusuk getline() és copy() alakú. De a régebbi C programokkal való kompatibilitás érdekében a szabvány az üres paraméterlistát régi stílusú függvénydeklarációnak tekinti és leállítja a paraméterlista ellenőrzését, ezért a ténylegesen üres paraméterlistában a void kulcsszónak kell szerepelnie. A kérdéssel a 4. fejezetben még foglalkozunk.

Megjegyezzük, hogy a külső változókkal foglalkozó részben nagy gonddal használtuk a definiálás és deklarálás fogalmakat. A definíció a program azon helye, ahol a változót (vagy függvényt) létrehoztuk vagy tárterületet rendeltünk hozzá. A deklaráció viszont olyan programrész, ahol csak leírjuk a változó tulajdonságait, de nem rendelünk hozzá tárterületet.

Hajlamosak vagyunk a program összes változóját külső változóként definiálni, mert így egyszerűsíthető a függvények közötti információcsere, rövidebbé válnak az argumentumlisták és a változók mindig a rendelkezésünkre állnak, amikor szükséges. De sajnos, a külső változók akkor is jelen vannak, ha nem akarjuk! Elég veszélyes dolog túlzottan a külső változókra támaszkodni, mert ez olyan programot eredményez, amelyben az adatkapcsolatok áttekinthetetlenek és a változók nem várt módon, sőt sokszor szándékunk ellenére megváltoznak, valamint a programot később nehéz módosítani. A leghosszabb sort kiíró program második változata ilyen szempontból rosszabb az elsőnél és tovább rontja a helyzetet, hogy a változók nevének rögzítésével a getline és a copy függvények elvesztették általános jellegüket.

Ebben a fejezetben áttekintettük a C nyelv legfontosabb elemeit. Ezekből az elemekből jelentős méretű, jól használható programok írhatók. Ennek érdekében javasoljuk az olvasónak, hogy most tartson egy kis szünetet a könyv olvasásában, és mielőtt tovább haladna, gondolja át az itt leírtakat, tanulmányozza a példaprogramokat és oldja meg a gyakorlatok feladatait. A következő gyakorlatokkal olyan programozási feladatokat kínálunk, amelyek a korábbiaknál bonyolultabbak.

1.20. gyakorlat. Írjunk detab néven programot, amely a beolvasott szövegben talált tabulátorkaraktereket annyi szóközzel helyettesíti, amennyi a következő tabulátorpozícióig hátravan! Tételezzük fel, hogy a tabulátorpozíciók adottak, pl. minden n-edik oszlopban. Az n értékét változóként vagy szimbolikus állandóként célszerű megadni?

1.21. gyakorlat. Írjunk programot entab néven, amely a beolvasott szövegben talált, szóközökből álló karaktersorozatot a minimális számú tabulátorkarakterrel és szóközökkel helyettesíti úgy, hogy a szövegben a távolság ne változzon! Használjuk ugyanazokat a tabulátorpozíciókat, mint a detab programban! Ha a következő tabulátorpozíció eléréséhez egyetlen szóköz vagy egyetlen tabulátor karakter is elegendő, akkor melyiket részesíti előnyben?

1.22. gyakorlat. Írjunk olyan programot, amely a hosszú bemeneti sorokat az n-edik oszlop előtt előforduló utolsó szóközkarakter után egy vagy több rövidebb sorba tördeli! Győződjünk meg arról, hogy a program nagyon hosszú sorok és az n-edik oszlop előtt sem szóközt, sem tabulátort nem tartalmazó sorok esetén egyaránt helyesen működik!

1.23. gyakorlat. Írjunk programot, ami egy C program szövegéből eltávolít minden megjegyzés szövegrészt! Ne feledkezzünk meg az idézőjelek közti karaktersorozatok és karakterállandók helyes kezeléséről! A C nyelvben a megjegyzés szövegek nem ágyazhatók egymásba.

1.24. gyakorlat. Írjunk programot, ami egy C program szövegét olyan alapvető szintaktikai hibák szempontjából ellenőrzi, mint a nem azonos számú kerek, szögletes és kapcsos kezdő és végzárójelek! Ne feledkezzünk meg az idézőjelekről, aposztrófokról, escape jelsorozatokról és megjegyzés szövegekről sem! Ezt a programot teljesen általános formában elég nehéz elkészíteni.



Előszó Tartalom 2. FEJEZET