Advertisement
4. FEJEZETTartalom6. FEJEZET

5. FEJEZET:

Mutatók és tömbök

A mutató vagy pointer olyan változó, amely egy másik változó címét tartalmazza. A C nyelvű programokban gyakran használják a mutatókat, egyrészt mert bizonyos feladatokat csak velük lehet megoldani, másrészt mert alkalmazásukkal sokkal tömörebb és hatékonyabb program hozható létre. A mutatók és a tömbök szoros kapcsolatban vannak egymással és ebben a fejezetben ezt a kapcsolatot vizsgáljuk, ill. megmutatjuk, hogy ez a kapcsolat hogyan használható ki.Gyakran a mutatót összekapcsolják agotoutasítással, mondván, hogy mindkettő csodálatos lehetőséget teremt az érthetetlen programok írásához. Ez biztosan így is van, ha nem kellő gondossággal használjuk, hiszen könnyű olyan mutatót létrehozni, amely valamilyen nem várt helyre mutat. Kellő fegyelemmel viszont elérhető, hogy a mutatókat használó program világos és áttekinthető legyen. A következőkben ezt próbáljuk meg bemutatni.

Az ANSI C egyik legfontosabb új eleme, hogy explicit szabályokat tartalmaz a mutatók használatára, amelyeket a jó programozók a gyakorlatban kihasználnak és a jó fordítóprogramok érvényre juttatnak. A korábbi C változathoz képest változás még, hogy achar*általános mutató helyett bevezették avoid*mutatótípust.

5.1. Mutatók és címek

A vizsgálatainkat kezdjük a számítógép tárolójának szervezését leíró egyszerű képpel! A tipikus számítógép tárolója egymást követő, folyamatosan számozott vagy címezett tárhelyekből áll, amelyekkel egyenként vagy folytonos csoportokban végezhetünk műveleteket. Elég gyakori, hogy a tároló bármelyik bájtja egy karakter, bájt-párja egyshorttípusú egész szám és négy szomszédos bájtja egylongtípusú egész szám tárolására alkalmas. Egy mutató a tárolóelemek egy csoportja (gyakran két vagy négy bájt), amely egy címet tartalmazhat. Ezért, hacegychartípusú változó éspegy rá mutató (azt címző) mutató, akkor a helyzet a következő vázlatnak megfelelően alakul.

img/01.png

Az&unáris (egyoperandusú) operátor megadja egy operandus címét, ezért a

p = &c;

utasításccímét hozzárendeli apváltozóhoz és ilyenkor azt mondjuk, hogypc-re „mutat”. Az&operátor csak változókra és tömbelemekre alkalmazható és nem használhatjuk kifejezésekre, állandókra, vagy regiszterváltozókra.A*unáris operátor neveindirekció, és ha egy mutatóra alkalmazzuk, akkor a mutató által megcímzett változóhoz férhetünk hozzá. Tegyük fel, hogyxésyegésztípusúak ésipegyinttípushoz tartozó mutató. A következő, elég mesterkélt példán bemutatjuk a mutatók deklarálását, valamint az&és*operátorok használatát.

int x = 1, y = 2, z[10];
int *ip;       /* ip az int tipushoz tartozó mutató */

ip = &x;       /* ip x-re mutat */
y = *ip;       /* y most 1 lesz */
*ip = 0;       /* most x nulla lesz */
ip = &z[0];    /* ip most z[0]-ra mutat */

x,yészdeklarációja az eddig látott módon történik, azipmutatót

int *ip;

módon deklaráljuk, és azt mondjuk*ipegyint. A változók deklarációjának szintaxisa annak a kifejezésnek a szintaxisát követi, amelyben a változót használjuk. Ezt a meggondolást már eddig is alkalmaztuk a függvények deklarációjánál. Ennek mintájára pl.

double *dp, atof(char *);

azt jelenti, hogy egy kifejezésben*dp(adpmutatóval kijelölt változó értéke) ésatofértékedoubletípusú, ill. azatofargumentumachartípushoz tartozó mutató.Az indirekció alapján látható, hogy egy mutató mindig meghatározott objektumra mutat, azaz minden mutató meghatározott adattípust jelöl ki. (Ez alól csak egy kivétel van, avoidtípushoz tartozó mutató, ami egy olyan adat, amely bármilyen mutatót tartalmazhat. Erre az a megszorítás érvényes, hogy önmagára nem alkalmazhatja az indirekciót. A kérdésre az5.11. pontbanmég visszatérünk.)

Haipegyxegészre mutat, akkor*ipminden olyan programkörnyezetben előfordulhat, aholxhasználata megengedett. Így pl. megengedett a

*ip = *ip + 10;

értékadás is, amely*ip-et tízzel növeli.Az&és*unáris operátorok szorosabban kötnek, mint az aritmetikai operátorok, ezért az

y = *ip + 1

kifejezés kiértékelésekor a gép először veszi azt az adatot, amireipmutat, hozzáad egyet, majd az eredményt hozzárendeliy-hoz. Az

*ip += 1

inkrementálja azt a változót, amireipmutat, csakúgy, mint a

++*ip

vagy az

(*ip)++

Az utolsó esetben a zárójelre szükség van, mert hiányában azipinkrementálódna azipáltal kijelölt adat helyett, mivel a*-hoz és++-hoz hasonló unáris operátorok jobbról balra hajtódnak végre.Mivel a mutatók is változók, ezért indirekció nélkül is használhatók kifejezésekben. Ha pl.iqegyintadatot címző mutató, akkor

iq = ip

értékadás hatásáraiptartalmaiq-ba másolódik, függetlenül attól, hogyipmire mutat.

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

Mivel a C nyelv a függvényeknek érték szerint adja át az argumentumokat, így a hívott függvény nem tudja megváltoztatni a hívó függvény változóit. Például a rendező programban használtuk a swap függvényt a rossz sorrendben lévő adatok felcserélésére. Itt nem elegendő, ha azt írjuk, hogy

swap(a, b);

ahol aswapfüggvényt úgy definiálnánk, hogy

void swap(int x, int y) /* Hibás!!! */
{
   int temp;

   temp = x;
   x = y;
   y = temp;
}

Mivel a függvényt érték szerint hívjuk, aswapnem képes a hívásában szereplőaésbargumentumokat befolyásolni (azoknak csak egy helyi másolatával dolgozik, ezek cseréje pedig nem befolyásolná az eredeti argumentumok sorrendjét).A kívánt hatás csak úgy érhető el, ha a hívó függvény mutatókat ad át a hívott függvénynek.

swap(&a, &b);

Mivel az&operátor a változó címét állítja elő, ezért az&aaza-hoz tartozó mutató. Ha aswapfüggvényben a paramétereket mutatóként deklaráljuk, akkor indirekt módon hozzáfér az operandusokhoz és így közvetlenül felcserélheti azokat. Az így megírtswapfüggvény:

void swap(int *px, int *py) /* *px és *py cseréje */
{
   int temp;

   temp=*px;
   *px = *py;
   *py = temp;
}
img/02.png

A folyamatot egyszerű ábrával is szemléltethetjük. Egy függvény a mutató típusú argumentumokon keresztül képes hozzáférni és megváltoztatni a hívó eljárás objektumait. Példaként írjuk meg agetintfüggvényt, amely egy szabad formátumú bemeneti konvertáló eljárás. Agetinta bemeneti karakteráramból hívásonként egy egész típusú értéket emel ki és visszaadja azt a hívó függvénynek, ill. jelzi a bemeneti állomány végét. A kapott egész számot és azEOFjelzését két külön csatornán kell megoldani, mivel azEOFjelzése maga is egy egész szám lehet, ami a beolvasott értékkel keveredve zavart okozhatna.Azt a megoldást választottuk, hogy azEOF-ra utaló állapotjelzést a függvény visszatérési értékével adjuk át a hívó programnak, a beolvasott egész számot pedig mutató típusú argumentumon keresztül. (A7.4. pontbanismertetésre kerülőscanffüggvény ugyanilyen módon működik.) Az

int n, tomb[MERET], getint(int *);
for (n = 0; n < MERET && getint(&tomb[n]) != EOF; n++)
   ;

ciklusgetinthívásokkal feltölti a tömböt. Agetintminden hívásakor a beolvasott egész számot elhelyezi atomb[n]pozícióra, majd a hívó ciklus inkrementáljanértékét. A helyes működéshez a híváskor atomb[n]tömbelem címét kell agetintfüggvénynek átadni, mert az csak ennek felhasználásával tudja a talált számot visszaadni a hívó eljárásnak.Agetintfüggvény itt közölt változatának visszatérési függvényértékeEOF, ha elérte az állomány végét, nulla, ha a következő bemeneti adat nem szám és pozitív érték, ha az eredmény érvényes egész szám (ami atomb[n]helyen van). A program:

#include <ctype.h>

int getch(void);
void ungetch(int);
/* getint: a bemenetről beolvas egy egész számot és a *pn
helyre teszi */
int getint(int *pn){
   int c, sign;

   while(isspace(c = getch())) /* átlépi az üres helyeket */
      ;
   if(!isdigit(c) && c != EOF && c != '+' && c!= '-') {
      ungetch (c); /* ez nem szám */
      return 0;
   }
   sign = (c == '-') ? -1 : 1;
   if(c == '+' || c == '-')
      c = getch();
   for(*pn = 0; isdigit(c); c = getch())
      *pn = 10 * *pn + (c - '0');
   *pn *= sign;
   if(c != EOF)
      ungetch(c);
   return c;
}

A*pn-t a teljesgetintfüggvényben úgy használjuk, mint egy közönségesinttípusú változót. Agetintszintén használja a4.3. pontbanleírtgetchésungetchfüggvényeket, mivel a feleslegesen beolvasott karaktert most is vissza kell írni a bemenetre.

5.1.gyakorlat. Ahogy ezt a példaprogramban láttuk, agetintaz olyan+vagyelőjelet, ami után nem következik számjegy, érvényes, nulla értékű adatként kezeli. Szüntessük meg ezt a problémát úgy, hogy egy nullát visszaírunk a bemenetre!

5.2.gyakorlat. Írjuk meg agetfloatfüggvényt, ami agetintlebegőpontos megfelelője! Milyen típusú függvényértékekkel tér vissza a getfloat?

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

A C nyelvben a mutatók és a tömbök között szoros kapcsolat van, ami indokolja, hogy a két dolgot közösen tárgyaljuk. Bármilyen művelet, amit egy tömb indexelésével elvégezhetünk, megoldható mutatókkal is. A mutatót használó programváltozat általában gyorsabb, de legalábbis a kezdők számára nehezebben érthető. Az

int a[10];

deklaráció egy tízelemű tömböt jelöl ki, azaz tíz egymást követő,a[0]…a[9]névvel ellátott objektumot.

img/03.png

Aza[i]jelölés a tömbi-edik elemére hivatkozik. Hapaegy egész típushoz tartozó mutató, amit

int *pa;

módon deklaráltunk, akkor a

pa = &a[0];

értékadás hatásárapaazatömb nulladik elemére fog mutatni, vagyispaaza[0]címét fogja tartalmazni.

img/04.png

Most nézzük az

x=*pa;

értékadást! Ez aza[0]tartalmát fogja azx-be másolni.Hapaegy tömb adott elemére mutat, akkor definíció szerintpa+1a következő elemre,pa+iapautánii-edik elemre éspa-iapaelőttii-edik elemre fog mutatni. Így hapaaza[0]elemre mutat, akkor

*(pa + 1)

a tömba[1]elemének tartalmára hivatkozik, éspa+iaza[i]címét adja, így*(pa+i)aza[i]tartalmát jelenti.

img/05.png

Ezek a megállapítások a tömböt alkotó változók típusától vagy méretétől függetlenül igazak. Az a kijelentés, hogy „adj 1-et a mutatóhoz” azt jelenti, hogypa+1a következő objektumra, hasonlóanpa+ipedig apautánii-edik objektumra mutat. A teljes mutatóaritmetikára igaz, hogy a növekmény mértékegysége annak az objektumnak a térbeli mérete, amire a mutató mutat.Az indexelés és a mutatóaritmetikai műveletek közötti összefüggés nagyon szoros. Definíció szerint a tömbre való hivatkozás a tömb első (nulladik indexű) elemét kijelölő mutató létrehozását jelenti. Vagyis a

pa = &a[0];

értékadás hatásárapaésa(mint a tömbre való hivatkozás) azonos értékű lesz. Mivel a tömb neve és a nulladik indexű elemének címe szinonimák, ezért apa=&a[0]értékadás úgy is írható, hogy

pa = a;

A fenti megállapítás első látásra nagyon meglepő és azt jelenti, hogy aza[i]-re való hivatkozás*(a+i)formában is írható. Aza[i]hivatkozás kiértékelésekor a C fordító azonnal átalakítja a hivatkozást*(a+i)alakra, és a két alak egymással teljesen egyenértékű. A fenti egyenlőség mindkét oldalára alkalmazva az&operátort az következik, hogy&a[i]ésa+iszintén azonosak, vagyisa+iazautánii-edik elem címe. Más oldalról nézve viszont hapaegy mutató, akkor az a kifejezésekben indexelhető, vagyispa[i]megfelel a*(pa+i)-nek. Röviden összefoglalva: bármely tömböt és indexet tartalmazó kifejezés egyenértékű egy mutatót és egy eltolást (ofszetet) tartalmazó kifejezéssel. A kétféle írásmód egyetlen utasításon belül is megengedett.A tömb neve és a mutató között csak egyetlen különbség van, amiről nem szabad elfelejtkeznünk: a mutató egy változó, tehátpa = avagypa++érvényes kifejezések, a tömb neve viszont nem változó, ezért aza = pavagya++alakú konstrukciók nem megengedettek!

Amikor egy tömb nevét átadjuk egy függvénynek, akkor valójában a tömb kezdetének címét adjuk át. A hívott függvényben ez az argumentum egy helyi változó lesz, így egy paraméterként megadott tömbnév lényegében egy mutató, vagyis egy címet tartalmazó változó. Ezt a tényt kihasználva írjuk meg egy tetszőleges karaktersorozat hosszát meghatározóstrlenfüggvény egy másik változatát.

/* strlen: megadja a karaktersorozat hosszát */
int strlen(char *s)
{
   int n;
   for (n = 0; *s != '\0'; s++)
      n++;
   return n;
}

Mivelsegy mutató, az inkrementálása megengedett, de azs++nincs semmilyen hatással sem azstrlenfüggvényt hívó függvényben a karaktersorozatra, mivel csak a mutató helyi másolata inkrementálódik. Ez azt jelenti, hogy azstrlenfüggvényt a

strlen("Halló mindenki!");    /* karakteres állandó */
strlen(tomb);                 /* char tomb[100]; */
strlen(ptr);                  /* char *ptr; */

formában és argumentumokkal híva jól működik.Egy függvény definíciójában a formális paraméterek között szereplő

char s[ ];
char *s;

megadási formák egyenértékűek, de a továbbiakban a másodikat részesítjük előnyben, mert sokkal egyértelműbben mutatja, hogy a paraméter egy mutató. Amikor egy tömb nevét adjuk át a függvénynek, a függvény szabadon dönthet, hogy tömbként vagy mutatóként kezeli (akár mindkét értelmezést is használhatja, ha az célszerű és világos).Arra is van lehetőség, hogy a tömbnek csak egy részét adjuk át a függvény hívásakor, ha a résztömb kezdetének mutatóját adjuk át. Például, haaegy tömb, akkor

f(&a[2])

vagy

f(a+2)

átadja azffüggvénynek azt a résztömböt, amely aza[2]elemmel kezdődik. Azffüggvényen belül a paraméterdeklaráció

f(int rtomb[]) {...}

vagy

f(int *rtomb) {...}

alakú lehet. Ami azffüggvényt illeti, arra semmiféle következménnyel nem jár, hogy a paraméter egy nagyobb tömb része.Ha biztosak vagyunk benne, hogy a megfelelő tömbelem létezik, akkor megengedett a tömb visszafele indexelése is. Ap[-1],p[-2]stb. szintaktikailag helyes és a tömbp[0]elemét közvetlenül megelőző elemekre való hivatkozást jelent. Természetesen nem hivatkozhatunk olyan objektumra, ami nincs a tömb határain belül.

5.4. A címaritmetika

Hapegy tömb valamelyik elemének mutatója, akkorp++inkrementálja apmutatót, hogy az a tömb következő elemére mutasson ésp+=ipedig úgy növelip-t, hogy az az aktuális elem utánii-edik elemre mutasson. Ezek, ill. az ehhez hasonló konstrukciók a mutató- vagy címaritmetika legegyszerűbb esetei.A C nyelv következetesen és szisztematikusan közelít a címaritmetikához: a mutatók, tömbök és a címaritmetika egységes kezelése a nyelv egyik pozitívuma. Ennek szemléltetésére írjunk egy primitív tárolóhely-kiosztó eljárást. Az eljárás két függvényből fog állni. Az első,alloc(n)függvény azndarab egymást követő karakterpozícióhoz tartozó mutatóval tér vissza, és ezt azallocfüggvény hívója a karakterek eltárolásához fogja felhasználni. A második,afree(p)függvény felszabadítja a tárterületet, ami így később újra felhasználható lesz. Az eljárást azért neveztük primitívnek, mert azafreehívásai azallochívásaival ellentétes sorrendben kell hogy történjenek. Így azallocés azafreeáltal kezelt tárterület lényegében egy veremtár, vagyis egy „utolsó be, első ki” típusú lista. A standard könyvtár hasonló feladatot ellátómallocésfreefüggvényeire nincs ilyen megszorítás (ezekről részletesebben majd a8.7. pontbanolvashatunk).

A legegyszerűbb megoldás, ha azallocegy nagy karakteres tömb, azallocbufrészeit szolgáltatja. Ez a tömb azallocésafreefüggvényekre nézve saját és közös. Mivel a függvények a feladatot mutatókkal és nem indexekkel oldják meg, ezért egyetlen más eljárásnak sem kell ismerni a tömb nevét, amit azallocésafreefüggvényeket tartalmazó forrásállománybanstatictárolási osztályúnak deklaráltunk (ezért más függvényekből nem látható). A gyakorlati megvalósításban nem is fontos, hogy a tömbnek neve legyen, megoldható a feladat úgy is, hogy amallocfüggvénnyel vagy más módon az operációs rendszertől kérünk egy név nélküli tárterület elejét kijelölő mutatót.

Azallocbufhasználatához további információk kellenek. A programban egyallocpmutatót fogunk használni, ami kijelöli azallocbufkövetkező szabad helyét. Ha azalloc-tólnkarakternyi helyet kérünk, akkor az ellenőrzi, hogy van-e még ennyi szabad hely azallocbuf-ban. Ha igen, akkor azallocvisszatér azallocpaktuális értékével (azaz a szabad terület kezdetével), majd ezutánnértékével megnöveli, hogy a következő szabad helyre mutasson. Ha azallocbuf-ban nincs elegendő hely, akkor azallocnulla értékkel tér vissza. Azafreefüggvény egyszerűenpértékre állítja azallocpmutatót, hapazallocbuff-ba mutat. A puffer kezelését a következő egyszerű ábra mutatja.

img/06.png

A program:

#define ALLOCSIZE 10000 /* a rendelkezésre álló hely */

/* az alloc tárolója */
static char allocbuf[ALLOCSIZE];
/* a következő szabad hely */
static char *allocp = allocbuf;

/* visszatér az n karakterhez tartozó mutatóval */ 
char *alloc(int n)
{
   if (allocbuf + ALLOCSIZE - allocp >= n) {
   /* van elég hely */
      allocp += n;
      return allocp - n; /* a régi mutató */
   } else /* nincs elég hely */
      return 0;
}

/* a p-ig terjedő részt felszabadítja */
void afree(char *p)  {
   if (p >= allocbuf && p < allocbuf + ALLOCSIZE)
      allocp = p;
}

A mutatót ugyanúgy lehet inicializálni, mint bármelyik más változót, de normális esetben csak a nullát vagy korábban definiált megfelelő típusú adatok címeit tartalmazó kifejezést rendelhetünk hozzá kezdeti értékül. A

static char *allocp = allocbuf;

deklaráció definiálja azallocpkarakteres adathoz tartozó mutatót és egyben inicializálja is, hogy az azallocbufkezdetére mutasson, ami a program indulásakor az első szabad hely. Helyette azt is írhattuk volna, hogy

static char *allocp = &allocbuf[0];

mivela tömb neveegybena nulladik indexű elemének a címe. A program vizsgáló része

if(allocbuf + ALLOCSIZE - allocp >= n)

ellenőrzi, hogy a tömbben van-e elegendő helynkarakter számára. Ha igen, akkor azallocplegfeljebb eggyel mutat túl azallocbufvégén. Ha a helyigény kielégíthető, akkor azallocaznkarakteres blokk kezdetét kijelölő mutatóval tér vissza (figyeljük meg a függvény deklarációját). Ha nem, akkor azallocegy jelzést ad vissza. A C nyelv garantálja, hogy egy adat címe soha nem nulla, így a visszatéréskor érzékelt nulla függvényérték a normálistól eltérő működést, azaz a szükséges hely hiányát jelzi.A mutatók és az egész számok nem felcserélhetők. Ez alól egyetlen kivétel van, a nulla: a nulla, mint állandó, hozzárendelhető egy mutatóhoz és egy mutató összehasonlítható a nulla állandóval. A nulla számkonstans helyett gyakran használják aNULLszimbolikus állandót, amivel a mutató speciális értékét jelzik. Ezek után a mutatókkal kapcsolatban mi is aNULLszimbolikus állandót fogjuk használni. ANULLaz<stdio.h>headerben van definiálva.

Az

if(allocbuf + ALLOCSIZE - allocp >= n)

és

if(p >= allocbuf && p < allocbuf + ALLOCSIZE)

formájú vizsgálatok a címaritmetika számos fontos tulajdonságára mutatnak rá. Hapésqugyanazon tömb elemeire mutatnak, akkor az==,!=,<,>,<=stb. relációk helyesen működnek. Például a

p < q

reláció igaz, hapa tömb egy korábbi (kisebb indexű) elemére mutat, mintq. Bármely mutató értelmes módon egyenlőségre vagy nem egyenlőségre összehasonlítható nullával. Ha viszont nem ugyanazon tömb mutatóit használjuk aritmetikai kifejezésekben vagy relációkban, akkor az eredmény értelmetlen (amit vagy azonnal észreveszünk, vagy nem). A szabály alól csak egyetlen kivétel van: egy tömb vége utáni első elem címét a címaritmetika még képes feldolgozni.Mint már láttuk, egy mutatót és egy egész számot szabad összeadni vagy kivonni. A

p + n

konstrukció apmutatóval aktuálisan kijelölt utánin-edik objektumot jelenti. Ez attól függetlenül igaz, hogypmilyen típusú objektumot címez meg, mivel a fordítóprogramn-t olyan egységekben számolja, mint ap-vel megcímzett objektum mérete (és ezt apdeklarációja határozza meg). Ha pl. az adott számítógépen azinttípusú adat négy bájtos, akkorintadatok esetén a mérték négy.A mutatók kivonása szintén megengedett: hapésqugyanazon tömb elemeit címzik, ésp < q, akkorq – p + 1apésqközötti elemek száma (a határokat is beleértve). Ezt a tényt kihasználva írjuk meg a karaktersorozat hosszát megadóstrlenfüggvény egy újabb változatát!

/* strlen: az s karaktersorozat hossza */
int strlen (char *s)
{
   char *p = s;

   while (*p != '\0')
      p++;
   return p - s;
}

A deklarációban apkezdeti értékekénts-t adtuk meg, ígypa karaktersorozat első elemére mutat. Awhileciklusban egyenként vizsgáljuk a karaktereket, amíg el nem érünk a\0végértékig. Mivelpkarakterekre mutat,p++mindig a következő karakterre léptetip-t, és a vizsgálat végénp – sa megvizsgált karakterek számát adja. (A karaktersorozatban lévő karakterek száma túl nagy lehet ahhoz, hogyinttípusú adatként kezeljük. Az<stddef.h>headerben definiálva van egyptrdiff_ttípus, ami elegendően nagy ahhoz, hogy két mutató előjeles különbségét tárolja. Ha nagyon gondosak akarunk lenni, akkor azstrlenvisszatérési típusát a standard könyvtári változatnak megfelelőensize_ttípusnak választjuk. Asize_tegy előjel nélküli egész adattípus, amelyet asizeofoperátor ad visszatérési értékként.)A címaritmetika működése következetes: hafloattípusú adatokkal dolgozunk, amelyek több helyet igényelnek, mint achartípusúak, éspegyfloattípusú adatot címző mutató, akkorp++a következőfloattípusú adatot jelöli ki. Így minden további nélkül megírhatjuk azallocfüggvény egy másik változatát, amelyben acharadattípus helyettfloatadattípust tárolunk. Ehhez mindössze azallocés afreefüggvényekben acharszótfloat-ra kell átírni. Minden mutatókkal végzett művelet automatikusan figyelembe veszi a megcímzett objektum méretét.

A mutatók esetén megengedett műveletek az azonos típusú mutatók közötti értékadás, mutatók és egészek közötti összeadás vagy kivonás, két azonos tömbre értelmezett mutató kivonása vagy összehasonlítása, valamint mutató értékadása vagy összehasonlítása nullával. Minden más mutatókra vonatkozó aritmetikai művelet tilos. Nem lehet két mutatót összeadni, szorozni, osztani, léptetni vagy maszkolni, és ugyancsak tilos mutatóhozfloatvagydoubletípusú értéket hozzáadni. A szabályok alól csak avoid* a kivétel, amely rákényszerített típusmegadás (cast) nélkül egy adott típusú mutatóhoz egy másik típusú mutatót rendel.

5.5. Karaktermutatók és függvények

Az

"Ez egy karaktersorozat"

alakban írt karaktersorozat állandók valójában karakterekből álló tömbök, amelyeket a belső ábrázolásban egy null-karakter (\0) zár. A program a karaktersorozat végét a null-karakter keresésével találja meg. Ezt a belső ábrázolást használva a karaktersorozat tárolásához szükséges hely csak egy karakterrel több, mint az idézőjelek közötti karakterek száma. A karaktersorozat állandók a leggyakrabban függvények argumentumaként fordulnak elő, mint pl. a

printf("Halló mindenki!\n");

függvényben. Az ehhez hasonló karaktersorozatokhoz a program karakteres mutatón keresztül fér hozzá, azaz aprintffüggvény megkapja a karaktersorozat kezdetének mutatóját. Általánosan igaz, hogy a program a karaktersorozathoz az első elemét kijelölő mutatón keresztül fér hozzá.A karaktersorozat természetesen nem csak függvények argumentumaként fordulhat elő. Ha apuzenetváltozót úgy deklaráltuk, hogy

char *puzenet;

akkor a

puzenet = "Itt az idő!"

utasítás apuzenet-hez a karaktersorozatot tartalmazó tömb mutatóját rendeli. Ez valójában nem karaktersorozat-másolás, csak a mutatók rendelődnek egymáshoz. A C nyelvben nincs olyan operátor, amellyel egy karaktersorozat, mint egyetlen objektum, feldolgozható lenne.A következő két definíció között lényeges különbség van:

char auzenet = "Itt az idő!";  /* ez egy tömb */
char *puzenet = "Itt az idő!"; /* ez egy mutató */

Azauzenetegy tömb, ami elegendően nagy ahhoz, hogy a karaktersorozatot és a\0végjelzést tárolni tudja, és a tömbnek az„Itt az idő!”karaktersorozatot adjuk kezdeti értékül. Másrészt apuzenetegy mutató, amelyhez egy karaktersorozat állandót megcímző kezdőértéket rendelünk. Ez a mutató természetesen módosítható, és akkor más objektumot fog megcímezni, viszont a karaktersorozat módosítása definiálatlan eredményt ad. A kétféle definíció térbeli elhelyezkedését a következő vázlat mutatja:

img/07.png

A mutatók és tömbök használatának néhány kérdését két hasznos függvényen keresztül mutatjuk be. A függvények a standard könyvtárban megtalálhatók. Az első,strcpy(s, t)függvény atkaraktersorozatot azskaraktersorozatba másolja át. Jó lenne, ha azt írhatnánk, hogys = t, de ez csak a mutatót másolja át, magát a karaktersorozatot nem. A karaktersorozat átmásolásához egy ciklus szükséges. A karaktersorozatot átmásolóstrcpyfüggvény karakteres tömbökkel megvalósított változata:

/* strcpy: a t karaktersorozatot s-be másolja – tömb
indexelésével megvalósított változat */
void strcpy(char *s, char *t)
{
   int i;

   i = 0;
   while ((s[i] = t[i]) != '\0')
      i++;
}

A következőkben bemutatjuk azstrcpyfüggvény mutatókkal megvalósított változatát:

/* strcpy: a t karaktersorozatot s-be másolja
1. mutatókkal megvalósított változat */
void strcpy(char *s, char *t)
{
   while ((*s = *t) != '\0') {
      s++;
      t++;
   }
}

Mivel a C nyelv az argumentumokat érték szerint adja át, így azstrcpytetszés szerint használhatja azséstparamétereket (ill. azok helyi másolatait). A program a szokásos módon inicializálja a mutatókat, majd ezek karakterenként végighaladnak a tömbökön, mindaddig, amíg atkaraktersorozatot lezáró\0át nem másolódiks-be.A gyakorlatban azstrcpyfüggvényt nem az előző módon írnánk meg. Egy gyakorlott C programozó inkább a következő változatot részesítené előnyben:

/* strcpy: a t karaktersorozat s-be másolja
2. mutatókkal megvalósított változat */
void strcpy(char *s, char *t)
{
   while((*s++ = *t++) != '\0')
      ;
}

Ez a változat azséstmutatók inkrementálását a ciklus vizsgáló részébe építi be. A*t++értéke az a karakter, amelyretaz inkrementálás előtt mutat. A postfix inkrementálás a karakter feldolgozásának befejeztéig nem változtatja megt-t. Ugyanígy a karakter a régisáltal kijelölt pozícióba tárolódik, mégsinkrementálása előtt. Az átmásolt karaktereket a program a\0végjelzéssel hasonlítja össze és ez vezérli a ciklust. Mindezek hatásáratösszes karaktere, a lezáró\0végjelet is beleértve átkerüls-be.A programot vizsgálva megfigyelhető, hogy a\0végjelzéssel való összehasonlítás redundáns, ezért a függvény még tovább rövidített változata úgy írható be, hogy

/* strcpy: a t karaktersorozatot s-be másolja
3. mutatókkal megvalósított változat */
void strcpy(char *s, char *t)
{
   while (*s++ = *t++)
      ;
}

Ez a változat első ránézésre nagyon titokzatosnak tűnik, de a jelölés nagyon kényelmes, ezért célszerű elsajátítani. Különböző programokban gyakran találkozhatunk vele.
Azstrcpyfüggvény a standard könyvtárban (a<string.h>headerben) található, és visszatérési értéke az átmásolt karaktersorozat.A példaként megírt második függvény azstrcmp(s, t), amely azséstkaraktersorozatokat hasonlítja össze, és visszatérési értéke negatív, nulla vagy pozitív attól függően, hogy azslexikografikusan kisebb, egyenlő vagy nagyobb t-nél. (A lexikografikus sorrendet úgy kapjuk, hogy a karaktersorozatokat a gép belső karakterkészletének megfelelően – tágabb értelemben, a különböző jeleket is beleértve – ábécésorrendbe rendezzük. A kisebb, egyenlő vagy nagyobb reláció ekkor az ábécében elfoglalt helyek viszonyát jelzi.) Ezt az értéket úgy kapjuk meg, hogy az első olyan helyen, aholséstkülönbözik, kivonjuk egymásból a két karaktert. A program tömbindexeléssel megvalósított változata:

/* strcmp: visszatérési érték <0, ha s<t, 
=0, ha s=t és >0, ha s>t */
int strcmp(char*s, char *t)
{
   int i;

   for (i = 0; s[i] == t[i]; i++)
      if (s[i] == '\0') 
         return 0; 
   return s[i] - t[i];
}

A függvény mutatókkal megvalósított változata:

/* strcmp: visszatérési érték <0, ha s<t, 
=0, ha s=t és >0, s>t */
int strcmp(char *s, char *t)
{
   for ( ; *s == *t; s++, t++)
      if (*s == '\0')
         return 0;
   return *s - *t;
}

Mivel a++ésprefix és postfix formában egyaránt használhatók, ezért a*, ill. a++ésoperátorok, ritkán, de más kombinációban is előfordulhatnak. Például a

*--p

ap-vel megcímzett karakter elővétele előtt dekrementáljap-t. Még néhány fontosabb kombináció:

*p++ = val; /* val értékét a verembe teszi */
val = *--p; /* a verem tetején lévő elemet val-ba teszi */

Az előző két példát érdemes megjegyezni, mivel a verem kezelésének alapműveletei.Azstrcpyésstrcmpfüggvények deklarációit a standard könyvtár tartalmazza, és több más karaktersorozat-kezelő függvény deklarációjával együtt a<string.h>headerben találhatók.
5.3.gyakorlat. Írja meg a2. fejezetbenbemutatottstrcat(s, t)függvény mutatóval megvalósított változatát! Azstrcat(s, t)függvény atkaraktersorozatot azskaraktersorozat végéhez másolja.

5.4.gyakorlat. Írjonstrend(s, t)néven függvényt, amely1értékkel tér vissza, ha atkaraktersorozat megtalálható azskaraktersorozat végén, és0értékkel, ha nem!

5.5.gyakorlat. Írja meg azstrncpy,strncatésstrncmpkönyvtári függvények saját változatát! Ezek a függvények az argumentumként megadott karaktersorozat legfeljebb elsőnkarakterével végeznek műveletet, pl. azstrncpy(s, t, n)atkaraktersorozat legfeljebb elsőnkarakterét másoljas-be. (A könyvtári függvények leírása aB. Függelékbentalálható.)

5.6.gyakorlat. Írjuk át a korábbi fejezetek erre alkalmas példaprogramjait úgy, hogy indexelt tömbök helyett mutatókat használjunk! Erre kiválóan alkalmas az1.és4. fejezetbenmegírtgetline, a2.,3.és4. fejezetbenmegírtatoi,itoaés minden változata, a3. fejezetbenhasználtreverse, valamint a4. fejezetbenhasználtstrindexésgetopfüggvény.

5.6. Mutatótömbök és mutatókat megcímző mutatók

Mivel a mutatók maguk is változók, ezért minden további nélkül tárolhatók tömbökben, csakúgy, mint bármely más változó. Ennek bemutatása céljából írjunk programot, amely szövegsorokat ábécésorrendbe rendez. (Ez a UNIX sort rendezőprogramjának egyszerűsített változata lesz.)A3. fejezetbenmár bemutattuk a Shell-féle algoritmus alapján működő rendezőprogramot és a4. fejezetbenannak javított változatát, aquicksortprogramot. A példában ugyanezeket az algoritmusokat fogjuk használni, azzal az eltéréssel, hogy most változó hosszúságú szövegsorokat rendezünk az egész számok helyett. Ez lényeges különbség, mivel a szövegsorokat nem hasonlíthatjuk össze vagy mozgathatjuk egyetlen művelettel. Olyan adatábrázolásra van szükség, ami lehetővé teszi a változó hosszúságú szövegsorok hatékony és kényelmes kezelését.

Ez a mutatókból álló tömbökkel valósítható meg legegyszerűbben. Ha a rendezendő sorokat egy hosszú karakteres tömbben, egymáshoz illesztve tároljuk, akkor az egyes sorok az első karakterüket megcímző mutatón keresztül érhetők el. Ezek a mutatók egy tömbben tárolhatók. Két sor úgy hasonlítható össze, hogy átadjuk a mutatóikat azstrcmpfüggvénynek. Ha két sor rossz sorrendben van és fel kell cserélni őket, akkor csak a mutatóikat cseréljük a mutatótömbben, és nem pedig magukat a sorokat. Az elvet a következő ábra szemlélteti:

img/08.png

Ezzel a szervezéssel elkerülhetjük azt a kettős problémát, amit a bonyolult tárolókezelés és a szövegsorok tényleges mozgatásából adódó nagy műveletigény jelent. A rendezési folyamat három lépésből áll:

az összes sor beolvasása
a sorok rendezése
a rendezett sorok kiíratása

A szokásoknak megfelelően a programot a feladat természetes felosztása szerint tagoljuk, és amaincsak az egyes programrészeket vezérli. Pillanatnyilag ne foglalkozzunk magával a rendezéssel, hanem koncentráljunk az adatszerkezetre, valamint az adatok bevitelére és kiírására.Az adatbeviteli programrész beolvassa és eltárolja az egyes sorok karaktereit, valamint ezzel egy időben létrehozza a sorok mutatóit tartalmazó tömböt. Ugyancsak ennek a programrésznek a feladata a sorok számlálása, mivel a sorok számára szükség lesz a rendezés és a kinyomtatás során. Mivel az adatbeviteli programrész csak véges sok sort képes beolvasni, ezért egy adott korlátnál több sor beérkezése esetén-1értékkel tér vissza (illegális számú sor jelzése).

A sorokat kiíró programrész olyan sorrendben fogja kiírni a szövegsorokat, amilyen sorrendben a mutatóik előfordulnak a mutatótömbben. A program eddig tárgyalt részei:

#include <stdio.h>
#include <string.h>

#define MAXSOR 5000
/* max. ennyi sor rendezhető */
char *sorptr[MAXSOR]; /* mutatótömb a sorokhoz */

int readlines(char *sorptr[], int nsor);
void writelines(char *sorptr[], int nsor);
void qsort(char *sorptr[], int bal, int jobb);
/* beolvasott sorok rendezése */
main()
{
   int nsor; /* a beolvasott sorok száma */

   if ((nsor = readlines(sorptr, MAXSOR)) >=0) {
      qsort (sorptr, 0, nsor-1);
      writelines(sorptr, nsor);
      return 0;
   } else {
      printf ("Hiba: túl sok rendezendő sor\n");
      return 1;
   }
}

#define MAXHOSSZ 1000 /* a sor max. hossza */
int getline(char *, int);
char *alloc(int);

/* readlines: sorokat beolvas */
int readlines(char *sorptr[], int maxsor)
{
   int hossz, nsor;
   char *p, sor[MAXHOSSZ];

   nsor = 0;
   while ((hossz = getline(sor, MAXHOSSZ)) > 0)
      if (nsor >= maxsor || (p = alloc(hossz)) == NULL)
         return -1;
      else {
         sor[hossz-1] = '\0'; /* törli az újsor-karaktert */
         strcpy(p, sor);
         sorptr[nsor++] = p;
      }
   return nsor;
}

/* writelines: kiírja a rendezett sorokat */
void writelines(char *sorptr[], int nsor)
{
   int i;
   for (i =0; i < nsor; i++)
      printf ("%s\n", sorptr[i]);
}

A program használja az1.9. pontbanleírtgetlinefüggvényt.A legfontosabb újdonsággal a sorptr deklarációjában találkozunk. A

char *sorptr[MAXSOR]

azt mondja, hogy asorptregy tömb, amelynekMAXSORszámú eleme van és minden elem egychartípushoz tartozó mutató. Így asorptr[i]egy karakterhez tartozó mutató és*sorptr[i]pedig azi-ediknek eltárolt szövegsor első karakterének mutatója.Mivelsorptrmaga is egy tömb neve, mutatóként kezelhető ugyanúgy, mint a korábbi példákban a tömbnevek, ezért awritelinesfüggvényt úgy is megírhatjuk, hogy

/* writelines: kiírja a rendezett sorokat */
void writelines(char *sorptr[], int nsor)
{
   while (nsor-- > 0)
      printf ("%s\n", *sorptr++);
}

Kezdetben*sorptraz első sorra mutat, majd az inkrementálás hatására a következő sorra lép, amíg csaknsor le nem számlálódik.Miután a sorok beolvasását, ill. kiíratását elintéztük, rátérhetünk a rendezésre. A4. fejezetbenbemutatottquicksortprogramot kissé meg kell változtatnunk: módosítani kell a deklarációkat és az összehasonlítást azstrcmpfüggvény hívásával kell elvégeznünk. Az algoritmus változatlan marad, ezért bízhatunk benne, hogy továbbra is működni fog.

/* qsort: a v[bal] ... v[jobb] tömb rendezése növekvő
sorrendbe */
void qsort(char *v[], int bal, int jobb)
{
   int i, utolso;
   void
   swap(char *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);
   utolso = bal;
   for (i = bal + 1; i <= jobb; i++)
      if (strcmp(v[i], v[bal]) < 0)
         swap(v, ++utolso, i);
   swap(v, bal, utolso);
   qsort(v, bal, utolso-1);
   qsort(v, utolso+1, jobb);
}

Aswapfüggvényt is csak triviális módon kell megváltoztatni, a deklaráció értelemszerű módosításával:

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

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

Mivelv(asorptr) bármelyik egyedi eleme egy karakteres mutató, ígytempis az kell, hogy legyen az értékadás miatt.

5.7.gyakorlat. Módosítsuk areadlinesfüggvényt úgy, hogy a beolvasott sorokat amaináltal létrehozott tömbben tárolja, és ne azallocfüggvényen keresztül kérjen mindig helyet a sor számára! A program mennyivel lesz gyorsabb, ha elmarad azallochívása?

5.7. Többdimenziós tömbök

A C nyelv lehetővé teszi a derékszögű többdimenziós tömbök alkalmazását, bár ezeket sokkal ritkábban használják, mint a mutatótömböket. Ebben a pontban bemutatjuk a többdimenziós tömbök tulajdonságait.Vizsgáljuk meg a hónap napjairól az év napjaira vagy fordítva történő adatátalakítás feladatát! Például március 1. egy nem szökőév 60. napja, szökőévben pedig a 61. nap. Az átalakításhoz definiáljunk két függvényt: aday_of_yearfüggvény a hónap napjait az év napjaivá alakítja, amonth_dayfüggvény pedig az év napjait a hónap napjaivá. Mivel amonth_dayfüggvény két értéket (hónap és nap) számol, így a hónap és nap argumentum mutató lesz. A

month day(1988, &h, &n)

függvényhíváshértékét 2-re,nértékét 29-re állítja be (1988. február 29.).Mindkét függvénynek azonos információra van szüksége: egy táblázatra, ami tartalmazza az egyes hónapokban lévő napok számát. Mivel a hónapok napjainak száma más és más szökőévben és nem szökőévben, ezért egyszerűbb a szökőév és nem szökőév adatait egy kétdimenziós tömb két sorában tárolni, mint mindig vizsgálni, hogy februárban milyen adattal kell dolgozni. Az adatátalakítást végző függvények és az átalakításhoz szükséges tömb:

static char naptab[2][13] = {
{0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31},
{0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
};

/* day_of_year: a hónap és nap értékéből kiszámítja
az év napját */
int day_of_year(int ev, int ho, int nap)
{
   int i, szoko;

   szoko = ev%4 == 0 && ev%100 != 0 ||
               ev%400 == 0;
   for (i = 1; i < ho; i++)
      nap += naptab[szoko][i];
   return nap;
}

/* month_day: az éven belüli napból megadja a hónapot
és a napot*/
void month_day(int ev, int evnap, int *pho, int *pnap)
{
   int i, szoko;
   szoko = ev%4 == 0 && ev%100 != 0 ||
               ev%400 == 0;
   for (i = 1; evnap >naptab[szoko][i]; i++)
      evnap -= naptab[szoko][i];
   *pho = i;
   *pnap = evnap;
}

Emlékezzünk vissza a logikai kifejezések számértékéről mondottakra: az nulla, ha a kifejezés hamis, és egy, ha igaz. Ezt használtuk ki aszokomeghatározásánál, és az így kapott 0 vagy 1 érték felhasználható anaptabindexelésére.Anaptabtömb aday_of_yearésmonth_dayfüggvények számára külső változó, így mindkettő használhatja. Anaptabtömb elemeitchar-ként adtuk meg, hogy ezzel is megmutassuk achartípusú adat nemcsak karakterek, hanem kis egész számok tárolására is alkalmas.

Anaptabaz első kétdimenziós tömb, amivel eddig találkoztunk. A C nyelvben a kétdimenziós tömb valójában egy egydimenziós tömb, amelynek mindegyik eleme szintén egy tömb. Ezért kell az indexeket

naptab[i][j] /* [sor][oszlop] */

alakba írni a más nyelvekben megszokott

naptab[i, j] /* HIBÁS!!! */

alak helyett. A kétdimenziós tömbben az elemek sorfolytonosan tárolódnak, ezért a jobbra az utolsó index (oszlopindex) változik a leggyorsabban, ha az elemeket a tárolás sorrendjében címezzük.A tömböt kapcsos zárójelek között elhelyezett kezdeti értékekkel inicializáljuk, és a kétdimenziós tömb egyes sorait a megfelelő allisták inicializálják. Anaptabtömböt egy nulla tartalmú oszloppal kezdtük, hogy az 1-től 12-ig terjedő hónapszámmal természetes módon indexelhessük a 0…11 indexek helyett. Mivel a példában a felesleges oszlop nem növeli számottevően a program helyfoglalását, ezért inkább ezt a megoldást választottuk, mint az indexek átkódolását.

Ha egy kétdimenziós tömböt átadunk egy függvénynek, akkor a függvényben lévő paraméterdeklarációban meg kell mondani az oszlopok számát. A sorok száma közömbös, mivel (az egydimenziós tömbökhöz hasonlóan) itt is a sorok alkotta tömb mutatóját adjuk át (ami jelen esetben 13inttípusú adatot tartalmazó tömbre mutat). Ezért ha anaptabtömböt pl. egyffüggvénynek adnánk át, akkor azfdeklarációja

f(int naptab[2][13]) {...}

alakú, amit úgy is írhatnánk, hogy

f(int naptab[][13]) {...}

mivel a sorok száma közömbös, vagy

f(int (*naptab)[13]){...}

alakban, ami azt fejezi ki, hogy a paraméter egy mutató, ami 13 egész adatból álló tömbre mutat. A kerek zárójel szükséges, mivel a[ ]szögletes zárójelek nagyobb precedenciájúak, mint a*. A zárójel nélküli

int *naptab[13]

deklaráció 13 mutatóból álló tömböt deklarálna, amelynek mindegyik eleme egy egész típusú adatra mutat. Az ehhez hasonló összetett deklarációk kérdésével az5.12. pontbanmég foglalkozunk.

5.8.gyakorlat. Aday_of_yearésmonth_dayfüggvényekben nincs hibaellenőrzés. Küszöböljük ki ezt a hiányosságot!

5.8. Mutatótömbök inicializálása

Írjunk egymonth_name(n)függvényt, amely egy, azn-edik hónap nevét tartalmazó karaktersorozatot címző mutatót ad visszatérési értékként. Ez a belsőstatictárolási osztályú tömb ideális alkalmazási lehetősége! Amonth_namefüggvény karaktersorozatokból álló saját tömböt tartalmaz, és hívásakor a megfelelő mutatóval tér vissza. A feladat megoldása kapcsán megmutatjuk, hogy hogyan inicializálható a nevekből álló tömb.A szintaxis hasonló a korábban használt inicializálások szintaxisához:

/* month_name: visszatér az n-edik hónap nevével */
char *month_name(int n)
{
   static char *nev[] = {
      "Illegális hónapnév",
      "Január", "Február", "Március",
      "Április", "Május", "Június",
      "Július", "Augusztus", "Szeptember",
      "Október", "November", "December"
   };

   return (n < 1 || n > 12) ? nev[0] : nev[n];
}

A karakteres mutatók tömbjét alkotó nev deklarációja ugyanolyan, mint asorptrdeklarációja volt a rendezőprogramban. Az eltérés csak az, hogy a kezdeti értékek most karaktersorozatok, amelyek hozzá vannak rendelve a tömb megfelelő eleméhez. Azi-edik karaktersorozat valahol a tárolóban helyezkedik el (nem tudjuk, hogy pontosan hol, de ez nem is érdekes) és a mutatója van anev[i]helyen. Mivel anevtömb méretét nem specifikáltuk, a fordítóprogram megszámolja a kezdeti értékeket és a kívánt helyre beírja a helyes számot.

5.9. Mutatók és többdimenziós tömbök

A kezdő C programozókat gyakran megzavarja a kétdimenziós tömb és a mutatókból álló tömb (mint pl. anevaz előző példában) közötti különbség. Ha pl. adott a következő két definíció:

int a[10][20];
int *b[10];

akkor aza[3][4]ésb[3][4]mindegyike szintaktikailag helyes hivatkozás egyinttípusú adatra. De a valójában egy kétdimenziós tömb: a definíció szerint 200intméretű tárolóhelyet foglalunk le a számára, és egy elemének tényleges helyét mátrixos indexeléssel, mint 20*sor+oszlophatározzuk meg aza[sor][oszlop]logikai indexelés alapján. Ab-re vonatkozó definíció csak 10 mutató számára foglal helyet és nem rendel hozzájuk kezdeti értéket. Az inicializálást explicit módon, statikusan vagy a programban kell elvégeznünk. Feltételezve, hogybminden eleme egy 20 elemű tömbre mutat, akkor ez a tárolóban 200intváltozónyi helyet igényel, amihez még hozzájön a 10 mutató helyigénye. A mutatótömbnek van egy lényeges előnye: az általa címzett tömb sorai különböző hosszúságúak lehetnek. Így abegyes elemei nem szükségképpen mutatnak egy 20 elemű vektorra, lehet olyan, amelyik 2 elemre, a másik 5 elemre, sőt olyan is, amelyik 0 elemre mutat.Bár az előbbi fejtegetésben mi mindig egész típusú adatokról beszéltünk, a mutatótömbök leggyakoribb alkalmazása mégis az, amikor elemeik különböző hosszúságú karaktersorozatokra mutatnak (mint pl. amonth_namefüggvényben). Az elmondottak jól láthatók az alábbi deklarációk és a hozzájuk tartozó, tárbeli elhelyezkedést szemléltető ábrák alapján. A mutatótömb:

char *nev[] = { "Illegális hónap", "Jan",
"Febr", "Márc" };
img/09.png

A kétdimenziós, karaktersorozatokat tároló tömb:

char anev[][15] = { "Illegális hónap", "Jan",
"Febr", "Márc" } ;
img/10.png

5.9.gyakorlat. Módosítsuk aday_of_yearésmonth_dayfüggvényeket úgy, hogy indexelés helyett mutatókat használjanak!

5.10. Parancssor-argumentumok

A C nyelvet támogató környezetben lehetőségünk van a programnak parancssor-argumentumokat vagy paramétereket átadni a végrehajtás megkezdésekor. Amikor a végrehajtás kezdetekor a rendszer amain-t hívja, akkor a hívásban két argumentum szerepel. Az első (amitargc-nek szokás nevezni) megadja a parancssor-argumentumok számát, amellyel a programot hívtuk. A második (amitargv-nek szokás nevezni) egy karaktersorozatokat tartalmazó tömböt címző mutató. A tömb tartalmazza a program hívásakor átadandó parancssor-argumentumokat (minden argumentum egy karaktersorozat). Ezeket a karaktersorozatokat általában többszintű mutatóhasználattal kezeljük.Az elmondottakat a legegyszerűbben azechoprogram mutatja, amely egyszerűen visszaírja az egy sorban megjelenő, egymástól szóközzel elválasztott parancssor-argumentumokat. Az a parancs, hogy

echo Halló mindenki!

egyszerűen kiírja a kimenetre, hogy

Halló mindenki!

Megállapodás szerint azargv[0]az a név, amellyel a programot hívták, ígyargclegalább 1. Haargcegy, akkor a program neve után nincs parancssor-argumentum. A mi példánkbanargcértéke három, és azargv[0],argc[1], ill.argv[2]rendre az„echo”,„Halló”, ill.„mindenki!”karaktersorozatokat tartalmazza. A sorban az első opcionális argumentum azargv[1]és az utolsó azargv[argc-1]. Mindezeken kívül a szabvány megköveteli, hogyargv[argv]NULLértékű mutató legyen. Az elmondottakat az alábbi ábra szemlélteti.

img/11.png

Azechoprogram első változata azargv-t karakteres mutatók tömbjeként kezeli.

#include <stdio.h>
/* parancssor-argumentumok visszaírása – 1. változat */
main (int argc, char *argv[])
{
   int i;
   for (i = 1; i < argc; i++)
      printf("%s%s", argv[i], (i < argc-1) ? " " : "");
   printf("\n");
   return 0;
}

Mivelargvegy mutatótömb mutatója, ezért célszerűbb minden műveletet mutatókkal és nem indexelt tömbökkel végezni. A program következő változata ezértchartípusú adatokat címző mutatók mutatóját, azargv-t inkrementálja, aargc-t pedig lefelé számlálja.

#include <stdio.h>
/* parancssor-argumentumok visszaírása – 2. változat */
main (int argc, char *argv [])
{
   while(--argc > 0)
      printf("%s%s", *++argv, (argc > 1) ? " " : "");
   printf("\n");
   return 0;
}

Mivelargvaz argumentumok karaktersorozataiból álló tömb kezdetét kijelölő mutatók mutatója, inkrementálása (++argv) után az eredetiargv[0]helyett azargv[1]-re fog mutatni,argvaz egymást követő inkrementálások hatására mindig a következő argumentumot fogja megcímezni és*argvekkor az argumentum mutatója lesz. Ugyanakkorargcdekrementálódik, és ha értéke nulla lesz, akkor már nincs további kiírandó argumentum.Aprintfutasítást úgy is írhattuk volna, hogy

printf((argc > 1) ? "%s " : "%s", *++argv);

Ez szintén azt példázza, hogy aprintfargumentuma kifejezés is lehet.A második példánk a4.1. pontbanismertetett mintakereső program bővített változata. Ha visszagondolunk a programra, akkor emlékezhetünk rá, hogy a keresett minta mélyen a programba van beágyazva (karakteres állandóként), ami nem túl szerencsés megoldás. A következőkben a UNIX rendszergrepsegédprogramjának elvét követve a programot úgy változtattuk meg, hogy a keresendő mintát a parancssor első argumentumaként adhassuk meg. A módosítottfindprogram:

#include <stdio.h>
#include <string.h>
#define MAXSOR 1000
int getline(char *sor, int max);

/* find: az 1. argumentumában megadott mintát tartalmazó
sorokat megkeresi és kiírja */
main(int argc, char *argv[])
{
   char sor[MAXSOR];
   int talalt = 0;

   if (argc != 2)
      printf("Mintakeresés\n");
   else
      while (getline(sor, MAXSOR) > 0)
         if (strstr(sor, argv[1])!= NULL) {
            printf("%s", sor);
            talalt++;
         }
}

A standard könyvtárstrstr(s, t)függvénye egy mutatóval tér vissza, amely atkaraktersorozats-beli első előfordulásának helyére mutat, vagy aNULLértékkel, hatnem fordul elős-ben. Azstrstrfüggvény a<string.h>headerben van deklarálva.Most a programot finomítsuk tovább, hogy újabb mutatókkal kapcsolatos példákat készíthessünk. Tegyük fel, hogy a program hívásakor két argumentumot engedünk meg. Az egyik jelentse azt, hogy „írj ki minden sort, kivéve azokat, amelyekben a keresett minta megtalálható”, a másik pedig azt, hogy „írd ki minden sor elé a sorszámot”.

A UNIX rendszer alatt futó C nyelvű programok esetén a programnév után opcionálisan megadható paraméterek vagy jelzők (flagek) megállapodás szerint a mínusz jellel kezdődnek. Ha a-xparamétert választjuk az inverz kiírási feltétel jelzésére (ne írja ki azokat a sorokat, amiben megtalálható a minta) és-nparamétert a sorszámozás jelzésére, akkor a teljes parancs:

find -x -n minta

alakú lesz, és hatására kiíródik minden olyan sor, amelyben a keresett minta nem található meg és a sorok előtt megjelenik a sorszám.Az opcionális argumentumok sorrendje tetszőleges kell legyen, és a program további (nem a paramétereket feldolgozó) részének működése nem függhet a megadott argumentumok számától. A felhasználók számára kényelmes lehet, hogy az argumentumok kombinálhatók a

find -xn minta

alakban. A továbbfejlesztett program:

#include <stdio.h>
#include <string.h>
#define MAXSOR 1000

int getline(char *sor, int max);

/* find: kiírja azokat a sorokat, amelyekben
az 1. argumentumban megadott minta megtalálható */
main(int argc, char *argv[]) 
{
   char sor[MAXSOR];
   long sorszam = 0;
   int c, kiveve = 0, szam = 0, talalt = 0;

   while (--argc > 0 && (*++argv)[0] == '-')
      while (c = *++argv[0])
         switch (c) {
            case 'x':
               kiveve = 1;
               break;
            case 'n':
               szam = 1;
               break;
            default:
               printf("find: illegális opció %c\n", c);
               argc = 0; talalt = -1;
               break;
         }
   if (argc != 1)
      printf("find -x -n minta \n");
   else
      while (getline(sor, MAXSOR) > 0) {
         sorszam++;
         if ((strstr(sor, *argv) != NULL)!= kiveve) {
            if(szam)
               printf("%1d:", sorszam);
            printf("%s", sor);
            talalt++;
         } 
      }
   return talalt;
}

Minden egyes opcionális argumentum elővétele előtt azargcdekrementálódik és azargvinkrementálódik. A ciklus végén – ha nem volt hiba – azargctartalma megmondja, hogy hány argumentum maradt feldolgozatlanul ésargvezek közül az elsőre mutat, ígyargcakár1is lehet, és ekkor*argva keresendő mintára fog mutatni. Mivel*++argva karaktersorozatként megadott argumentum mutatója, ezért a(*++argv)[0]a karaktersorozat első karaktere. (Egy másik, szintaktikailag helyes forma az első karakter kijelölésére a**++argv.) Mint már említettük, a[]szorosabban kötődik, mint a*és a++, ezért a zárójelekre szükség van. Elhagyva azokat a kifejezés*++(argv[0])formában értékelődne ki, ami mást jelent. Más a helyzet, amikor a belső ciklust használjuk, aminek az a feladata, hogy végighaladjon egy kiválasztott argumentum karaktersorozatán! Itt a*++argv[0]azargv[0]mutatót inkrementálja!Az itt bemutatottaknál ritkán használunk bonyolultabb mutatós kifejezéseket. Ha mégis szükség lenne ilyenekre, akkor célszerű azokat két vagy három egyszerűbb lépésre bontani.
5.10.gyakorlat. Írjuk meg azexprprogramot, amely kiértékeli a parancssor-argumentumban megadott fordított lengyel jelölésmódú kifejezést! A parancssorban az egyes operátorokat és operandusokat szóköz választja el egymástól, pl. az

expr 2 3 4 + *

formában, ami a2*(3+4)kifejezésnek felel meg.5.11.gyakorlat. Módosítsuk az1. fejezetbenmegírtentabésdetabprogramot úgy, hogy a tabulátorbeállítási pozíciók listáját a parancssor-argumentumból vegye! Használjuk az alapesetnek megfelelő működést, ha nincs argumentum!

5.12.gyakorlat. Bővítsük azentabésdetabprogramokat úgy, hogy értelmezni tudják az

entab -m +n

rövidített jelölést! A bővített forma jelentése, hogy azm-edik oszloptól kezdve iktasson be tabulátorokat mindenn-edik oszlophoz. A program a felhasználó szempontjából kényelmes módon működjön, ha nem adunk meg argumentumot!5.13.gyakorlat. Írjuk meg atailprogramot, amely kinyomtatja az utolsónbemeneti sort! Alapfeltételezés szerint legyenn=10, de tegyük lehetővénváltoztatását egy opcionális argumentummal pl. a

tail -n

formában. (Ennek hatására az utolsón sor íródjon ki.) A program viselkedjen ésszerűen akkor is, ha a bemenet vagy aznértéke ésszerűtlen. A programot úgy írjuk meg, hogy a lehető legjobban használja a rendelkezésére álló tárterületet: a szövegsorokat a rendezőprogramnál leírt módon tároljuk és ne rögzített méretű kétdimenziós tömbként.

5.11. Függvényeket megcímző mutatók

A C nyelvben a függvények ugyan nem változók, de azért lehetséges hozzájuk mutatót definiálni, amivel minden, a mutatókra megengedett művelet elvégezhető (szerepelhet értékadásban, elhelyezhető egy tömbben, átadható egy függvénynek argumentumként, lehet a függvény visszatérési értéke stb.). A függvényekhez rendelt mutatók használatát a korábban megírt rendezőprogram módosított változatán mutatjuk be. A rendezőprogramot alakítsuk át úgy, hogy a-nopcionális argumentum hatására a bemeneti sorokat ne lexikografikusan, hanem numerikusan rendezze.A rendezés általában három részből áll: összehasonlításból, ami meghatározza bármely objektumpár sorrendjét; cseréből, ami megfordítja az objektumok sorrendjét, valamint a rendező algoritmusból, ami mindaddig végzi az összehasonlítást és cserét, amíg minden objektum nem kerül a megfelelő sorrendbe. A rendező algoritmus független az összehasonlítás és a cserélés működésétől, így különböző összehasonlító és cserélő függvényeket használva különböző kritériumok szerint rendezhetünk. Ezt a lehetőséget használjuk ki az új rendezőprogram kialakításánál.

Két szövegsor lexikografikus összehasonlítását eredetileg astrcmpvégezte. Most szükségünk lesz anumcmpfüggvényre, amely két sort a numerikus értéke alapján hasonlít össze és ugyanolyan módon tér vissza, mint astrcmp. Az összehasonlító függvényeket amainelőtt deklaráljuk, és a megfelelő függvény mutatóját adjuk át aqsortfüggvénynek (ami a rendező algoritmust valósítja meg). Nem foglalkozunk a hibás argumentumok kezelésével, csak a fő feladatra, a mutatók átadására koncentrálunk.

#include <stdio.h>
#include <string.h>
#define MAXSOR 5000 /* a rendezhető sorok max. száma */
char *sorptr[MAXSOR]; /* a szövegsorok mutatói */

int readlines(char *sorptr[], int nsor);
void writelines(char *sorptr[], int nsor);

void qsort(void *sorptr[], int bal, int jobb,
            int (*comp) (void *, void *));
int numcmp(char *, char *);

/* a bevitt sorok rendezése */
main(int argc, char *argv[])
{
   int nsor; /* a beolvasott sorok száma */
   int numeric = 0; /* 1, ha numerikus a rendezés */

   if (argc > 1 && strcmp(argv[1], "-n") == 0)
      numeric = 1;
   if ((nsor = readlines(sorptr, MAXSOR)) >= 0) {
      qsort ((void **) sorptr, 0, nsor-1,
         (int (*)(void*, void*)) (numeric ? numcmp : strcmp));
      writelines(sorptr, nsor);
      return 0;
   } else {
      printf("Túl sok rendezendő sor\n");
      return 1;
   }
}

Aqsorthívásában astrcmpésnumcmpfüggvények címei szerepelnek. Mivel ezek a nevek biztosan függvények nevei, ezért nincs szükség az&operátorra, ugyanúgy, ahogy a tömbök nevei előtt sem.Úgy módosítottuk aqsortprogramot, hogy képes legyen bármilyen adattípus feldolgozására, ne csak karaktersorozatokéra. Amint ezt a függvényprototípusban jeleztük, aqsortegy mutatókból álló tömböt, két egész típusú adatot és egy függvényt (két mutató típusú argumentummal) vár. A mutató típusú argumentumok megadásához avoid*általános (generikus) mutatótípust használtuk. Bármely mutató átalakíthatóvoid*típusúvá, ill. abból visszaalakítható bármiféle információvesztés nélkül, ezért használtunk aqsorthívásánálvoid*típusú argumentumokat. A megfelelően kialakított típusmegadások garantálják a mutatók típusegyeztetését. Ez a program tényleges megvalósítására semmilyen hatással sincs, de biztosítja a fordítóprogram helyes működését.

Ezután nézzük aqsortfüggvény módosított változatát!

/* qsort: a v[bal] ... v[jobb] rendezése növekvő sorrendbe */
void qsort(void *v[], int bal, int jobb,
            int (*comp)(void *, void *))
{
   int i, utolso;
   void swap(void *v[], int, int);
   if (bal >= jobb) /* nem csinál semmit, ha a tömb */
      return; /* kettőnél kevesebb elemből áll */
   swap(v, bal, (bal + jobb)/2);
   utolso = bal;
   for (i = bal+1; i <= jobb; i++)
      if ((*comp) (v[i], v[bal]) < 0)
         swap(v, ++utolso, i);
   swap(v, bal, utolso);
   qsort(v, bal, utolso-1, comp);
   qsort(v, utolso+1, jobb, comp);
}

A deklarációkat különös gonddal kell tanulmányozni! Aqsortnegyedik paramétere, az

int (*comp)(void *, void *)

azt mondja ki, hogy acompegy függvényt címző mutató, amelynek kétvoid* típusú argumentuma van ésinttípusú értékkel tér vissza. Acompfüggvény használata az

if ((*comp) (v[i], v[bal] < 0)

sorban összhangban van a deklarációval:compegy függvényhez tartozó mutató, így*compmaga a függvény, és

(*comp)(v[i], v[bal])

pedig annak hívása. A zárójelek feltétlenül szükségesek a helyes végrehajtási sorrend céljából. Ha elhagynánk őket, akkor az

int *comp(void *, void *) /* HIBÁS!!! */

definícióhoz jutnánk, ami azt mondja ki, hogycompegy függvény, amely egyinttípusú adatot megcímző mutatót ad vissza. Ez nyilvánvalóan mást jelent, mint az eredeti értelmezés.Korábban már bemutattuk a két karaktersorozatot összehasonlítóstrcmpfüggvényt, így most csak anumcmpfüggvénnyel foglalkozunk. Anumcmpaz első jegytől indulva számérték szerint hasonlít össze két karaktersorozatot. Anumcmpaz összehasonlításhoz a számokat tartalmazó karaktersorozatot azatoffüggvénnyel alakítja numerikus változóvá.

#include <stdlib.h>

/* numcmp: s1 és s2 karaktersorozat összehasonlitása
numerikusan */
int numcmp(char *s1, char *s2)
{
   double v1, v2;

   v1 = atof(s1);
   v2 = atof(s2);
   if (v1 < v2)
      return -1;
   else if (v1 > v2)
      return 1;
   else
      return 0;
}

A két adatot a mutatóik felcserésével megcserélőswapfüggvény azonos a fejezet elején leírttal, kivéve, hogy a deklarációkvoid* típusra változtak.

void swap(void *v[], int i, int j)
{
   void *temp;

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

Az itt bemutatotton kívül még számtalan más opció is illeszthető a rendezőprogramhoz, ezek közül néhány jó gyakorló feladat lesz.

5.14.gyakorlat. Módosítsuk a rendezőprogramot úgy, hogy kezelni tudja a-rjelzést, amivel a fordított (csökkenő) irányú rendezést írjuk elő! Biztosítsuk, hogy a-rműködjön a-nopcióval együtt is!

5.15.gyakorlat. A rendezőprogramot egészítsük ki a-fopcióval, ami egyesíti a nagy- és kisbetűket úgy, hogy a rendezésnél nem tesz különbséget közöttük! (PéldáulAésaösszehasonlítva legyen egyenlő.)

5.16.gyakorlat. A rendezőprogramot egészítsük ki a-dopcióval, aminek hatására csak a betűk, számjegyek és szóközök kerülnek összehasonlításra! Gondoskodjunk róla, hogy a-dopció működjön a-fopcióval együtt is!

5.17.gyakorlat. Egészítsük ki a rendezőprogramot mezőkezelési funkcióval, ami lehetővé teszi, hogy a rendezést sorokon belül kijelölt mezőkön hajtsuk végre! Engedjünk meg az egyes mezőkhöz egymástól független opciókészletet. (A könyv eredeti kiadásának tárgymutatóját kulcsszavakra a-df, oldalszámokra a-nopcióval rendezte egy hasonló rendezőprogram.)

5.12. Bonyolultabb deklarációk

A C nyelvet gyakran bírálják a deklarációinak szintaxisa miatt, különösen a függvényekhez tartozó mutatók használata esetén. A szintaxis megkísérli összeegyeztetni a deklarációt a gyakorlati alkalmazással, ami az egyszerűbb esetekben jól megoldható, viszont a bonyolultabb esetekben zavarokhoz vezethet. Ennek főleg az az oka, hogy egy deklaráció nem olvasható egyszerűen balról jobbra és túl sok a zárójel is. Az

int *f();    /* egy int típusú adatot címző mutatóval
             visszatérő függvény */

és

int (*pf)(); /* pf: egy int típusú értékkel visszatérő
             függvény mutatója */

deklarációk közötti különbség jól mutatja a problémát. A*egy prefix operátor, ami alacsonyabb precedenciájú, mint a(), ezért zárójeleket kell alkalmazni a megfelelő végrehajtási sorrend érdekében. Bár a valóban bonyolult deklarációk csak ritkán fordulnak elő a gyakorlatban, fontos, hogy értelmezni tudjuk őket és ha szükséges, képesek legyünk létrehozni ilyen deklarációkat. Jó módszer a deklarációk kis lépésekben történő felépítésére atypedefparancs, amelyen majd a6.7. pontbanfoglalkozunk. Egy másik lehetőség, amivel most fogunk megismerkedni, egy programpár, amelyek egyike az érvényes C nyelvű deklarációt szöveges leírássá alakítja, ill. a másik a szöveges leírásból C nyelvű deklarációt hoz létre. A programmal kapott szöveges leírás már balról jobbra olvasható.Az első,dclnevű program a bonyolultabb, és ennek feladata a C-beli deklarációk szavakra fordítása úgy, ahogy ez az alábbi példákban* látható:

*[Itt a példát meghagytuk az eredeti formájában, mivel a magyarítás (fordítás) teljes átírást igényelt volna a deklarációk szóbeli megfogalmazásának magyar mondatszerkezetre való átalakítása miatt. Magyar mondatszerkezet esetén a kiírt deklarációk sokkal bonyolultabban kombinálhatók össze értelmes mondatokká. Reméljük, az angol változat senkinek nem fog gondot okozni. (A fordító)]
char **argv
   argv: pointer to pointer to char
int (*daytab)[13]
   daytab: pointer to array[13] of int
int *daytab[13]
   daytab: array[13] of pointer to int
void *comp
   comp: function returning pointer to void
void (*comp)()
   comp: pointer to function returning void
char(*(*x())[])()
   x: function returning pointer to array[] of
   pointer to function returning char 
char(*(*x[3])())[5]
   x: array[3] of pointer to function returning
   pointer to array[5] of char

Adclprogram azA. Függelék 8.5. pontjábanrészletesen leírt deklarátor által specifikált grammatikán alapszik. A deklarátor (rövidítvedcl) egyszerűsített alakja:

dcl:                   opcionális_*direkt-dcl

direkt-dcl:        név
                        (dcl)
                        direkt-dcl()
                        direkt-dcl[opcionális_méret]

Szavakba foglalva, adcldirekt-dcl, ha (esetleg) megelőzi egy*. Egydirekt-dclegynév, vagy egy zárójelezettdcl, vagy egydirekt-dcl, amit zárójel követ, vagy egydirekt-dcl, amit szögletes zárójel és opcionális méretmegadás követ.Ez a grammatika a deklarációk elemzésére használható. Példaképpen nézzük a következő deklarátort:

(*pfa[])()

Ebbenpfa-t, mint egy nevet azonosíthatjuk és ígydirekt-dcltípusú. Ekkorpfa[]szinténdirekt-dcl. Ezután a*pfa[]-ról felismerjük, hogydcltípusú, ezért(*pfa[])direkt-dcl. Ekkor(*pfa[])()egydirekt-dclés így a definíció szerint az egész kifejezésdcltípusú. Az elemzés menetét az alábbi elemzőfával szemléltethetjük (adirekt-dclhelyett adir-dclrövidítést használva):

img/12.png

Adclprogram legfontosabb része adirdclésdclfüggvénypár, amelyek a vázlatosan ismertetett grammatika szerint elemzik a deklarációt. Mivel a grammatika rekurzívan van definiálva, ezért az egyes függvények is rekurzívan hívják egymást mindaddig, amíg fel nem ismerik a deklaráció egy darabját. Ennek megfelelően ez egy rekurzívan leszálló elemző program.

/* dcl: egy deklarátor elemzése */
void dcl(void)
{
   int ns;

   for (ns = 0; gettoken() == '*';)
      ns++; /* a *-ok számolása */
   dirdcl();
   while (ns-- > 0)
      strcat(out, " pointer to") ;
}

/* dirdcl: egy direkt deklarátor elemzése */
void dirdcl(void)
{
   int type;

   if(tokentype == '(') { /* (dcl) */
      dcl();
      if (tokentype != ')')
         printf("error: missing )\n");
      } else if (tokentype == NAME) /* változó név */
         strcpy (name, token);
      else
         printf("error: expected name or (dcl)\n");
      while((type=gettoken()) == PARENS || type == BRACKETS)
         if (type == PARENS)
            strcat(out, " function returning");
         else {
            strcat(out, " array");
            strcat(out, token);
            strcat(out, " of");
         } 
}

Mivel a programot csak példának szántuk, nem igazán „bombabiztos”, adclprogramban van néhány jelentős megszorítás: csak az egyszerű adattípusokat (mintcharvagyint) képes kezelni és a rosszul elhelyezett szóközök is megzavarhatják a működését. Mivel a programban nincs hibaállapot-helyreállítás, így az érvénytelen deklarációk is hibás működéshez vezetnek. Ezeknek a hibáknak a kijavítását a gyakorlott programozókra bízzuk.A program globális változói és amaineljárás:

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

#define MAXTOKEN 100

enum { NAME, PARENS, BRACKETS };

void dcl(void);
void dirdcl(void);

int gettoken(void);
int tokentype; /* az utolsó jel típusa */
char token[MAXTOKEN]; /* az utolsó jel karaktersorozata */
char name[MAXTOKEN]; /* az azonosító neve */
char datatype[MAXTOKEN]; /* adattípus = char, int stb. */
char out[1000]; /* a kimenetet tartalmazó karaktersorozat */

main() /* deklarációk megfogalmazása szavakban */ 
{
   while (gettoken() != EOF) { /* 1. jel a sorban */
      strcpy(datatype, token); /* ez az adattípus */
      out[0] = '\0';
      dcl(); /* a sor további részeinek elemzése */
      if (tokentype != '\n')
         printf("syntax error\n");
      printf("%s: %s %s\n", name, out, datatype);
   }
   return 0; 
}

Agettokenfüggvény átlépi a szóközöket és tabulátorokat, majd megkeresi a következő szintaktikai elemet (tokent) a bemeneti karaktersorozatban. Egy token lehet egy név, egy kerek zárójelpár, egy szögletes zárójelpár (amiben esetleg egy szám áll) vagy bármilyen egymagában álló karakter.

int gettoken(void) /* visszatér a következő jellel */
{
   int c, getch(void);
   void ungetch(int);
   char *p = token;
   while ((c = getch()) == ' ' || c == '\t')
      ;
   if (c == '(') {
      if ( (c = getch ()) == ')') {
         strcpy (token, "()");
         return tokentype = PARENS;
      } else {
         ungetch(c);
         return tokentype = '(';
      }
   } else if (c == '[') {
      for (*p++ = c; (*p++ = getch()) != ']' ; )
         ;
      *p = '\0';
      return tokentype = BRACKETS;
   } else if (isalpha(c)) {
      for (*p++ = c; isalnum(c = getch ());)
         *p++ = c;
   *p = '\0';
   ungetch(c);
   return tokentype = NAME;
   } else
      return tokentype = c;
}

Agetchésungetchfüggvényeket a4. fejezetbenmár ismertettük.A feladat megfordítása viszonylag egyszerű, főleg ha nem törődünk a feleslegesen generált zárójelekkel. Azundclprogram az „x is a function returning a pointer to an array of pointers to functions returningchar” (xchartípusú adatokkal visszatérő függvények mutatóiból alkotott tömb mutatójával visszatérő függvény) alakú szóbeli leírásból, ami az

x () * [] * () char

karaktersorozattal fejezhető ki, előállítja a

char (*(*x())[])()

deklarációt. A rövidített bemeneti szintaxis lehetővé teszi, hogy újra agettokenfüggvényt használjuk. Azundclfüggvény ugyanazokat a külső változókat használja, mint adcl.

/* undcl: a szóbeli megfogalmazást deklarációvá alakítja */
main()
{
   int type;
   char temp[MAXTOKEN];

   while (gettoken() != EOF) {
      strcpy(out, token);
      while ((type = gettoken()) != '\n')
         if (type == PARENS || type == BRACKETS)
            strcat(out, token);
         else if (type =='*') {
            sprintf(temp, "(*%s)", out);
            strcpy(out, temp);
         } else if (type == NAME) {
            sprintf (temp, "%s %s", token, out);
            strcpy(out, temp);
         } else
            printf("invalid input at %s\n", token);
         printf ("%s\n", out);
   }
   return 0;
}

A programban használtsprintffüggvény aprintf-hez hasonló könyvtári függvény, aprintf-nek megfelelően formátumozza a kiírandó adatokat, de kiírás helyett az első argumentumában (aminek karaktersorozatnak kell lenni) tárolja. Bővebb leírása a7.2. pontban, ill. aB. Függelékbentalálható.

5.18.gyakorlat. Egészítse ki adclprogramot a bemeneti hibákat megszüntető hibahelyreállító eljárással!

5.19.gyakorlat. Módosítsa azundclprogramot úgy, hogy ne írjon ki felesleges zárójeleket a deklarációkban!

5.20.gyakorlat. Bővítse ki adclprogramot úgy, hogy kezelni tudja a függvényargumentum típusú ésconst-hoz hasonló minősítőket tartalmazó deklarációkat is!

4. FEJEZETTartalom6. FEJEZET



Translate »