1. Komplex példa: táblázatkezelő

A mai órán: írjunk egy egyszerű táblázatkezelő programot!


2. Egy táblázat anatómiája

 A B C D
1
2
3
4
5
6
7

Kihívások:

  1. A képletek értelmezése.
  2. A kereszthivatkozások feloldása.

Gondoljuk meg: a fizetendő összeg kiszámításakor a képlet hivatkozik az egyes italok árára (amiket megszoroz a fogyasztott mennyiséggel). Az italok árai további cellákra hivatkoznak (összetevők ára és mennyisége). Az igazi problémát az jelenti, hogy nagyon nehéz feladat megtalálni azt a bejárási sorrendet, amelyen végighaladva csupa már kiszámított celleértéket használunk csak fel.

A jelen esetben ez azt jelentené, hogy előbb kiszámítjuk az egyes italok árait, majd ezután számoljuk ki a fizetendő összeget. Csakhogy a Long Island Ice Tea egy jóval későbbi cellában helyezkedik el, mint a fogyasztás, így előre-hátra kéne ugrálni a cellák között.

Egy összetett lapon annyira bonyolult összefüggések lehetnek az egyes cellák között, hogy a kiszámítás útvonalának a feltérképezése annyira nehézzé válik, amit már nem érdemes leprogramozni. Ehelyett azt választjuk, hogy elindulunk a bal felső cellától és jobbra lefelé haladva sorra vesszük a cellákat.

Ha egy cella hivatkozik egy másik értékére, akkor elugrunk oda, és megnézzük azt az értéket. Ha az is hivatkozik egy másikra, akkor tovább ugrunk egészen addig, amíg olyan cellákig nem érünk, amelyek rögtön kiértékelhetőek (pl. egy szám vagy csupa számokból álló képlet van bennük, vagy már korábban kiszámítottuk az értéküket).

Előfordulhat olyan eset is, hogy egy cella hivatkozik egy másik cella értékére, ami viszont hivatkozik az előbbire. Például az A3 cella képlete: A3 = B4 * 2, míg a B4-é: B4 = 123 + A3. Az ilyen jellegű hivatkozások nem oldhatóak fel, nem tudjuk meghatározni az értéket. Ugyanakkor a programunk végtelen rekurzióba kerülhet miattuk, ezért fel kell derítenünk, vagy a számolás során észre kell vennünk az ilyen csapdákat. Ráadásul egy ilyen, ún. körkörös hivatkozás előfordulhat több lépésben is – például az A3 hivatkozik a B4-re, ami hivatkozik a C8-ra, ami az A7-re, ami az A3-ra. A programnak ezeket is észre kell vennie, hiszen itt is ugyanúgy előállna a végtelen rekurzió.

Ezeket a problémákat kell megoldania a programunknak.

Kifejezések értelmezése (parsing)

4. Matematikai kifejezések kiértékelése

Matematikai kifejezések

  • Postfix alak: 4 5 7 + *
  • Prefix alak: * 4 + 5 7
  • Infix alak: 4 * (5 + 7)

Ezek mind ugyanazt jelentik.


Infixes alak: 4 * (5 + 7)

Ezt összetett feladat értelmezni: zárójelek, precedenciaszabályok, …

A nyelvi elemzés (parse, parsing) „Helló világ”-ja a négy alapműveletet és a zárójeleket ismerő matematikai kifejezések értelmezése.

Furcsa vagy nem, a legbonyolultabb a fenti három közül a hétköznapi infixes alak, amely esetén az operátort a két operandus közé tesszük. Itt megszoktuk azt, hogy a műveleteknek precedenciája van. Például a szorzásé magasabb, mint az összeadásé. Hogy az 5+7 összeget szorozzuk 4-gyel, a kifejezésben zárójelezni is kell azt.

A postfixes alak esetén az operandusok után van az operátor. Minden operátor az előtte lévő két operandusra vonatkozik. Pl. 5 7 + *, a + jel az előtte álló 5-ösre és 7-esre. Ezt lehetne zárójelezni is, de felesleges, hiszen mindig tökéletesen egyértelmű. Nincsen szükség precedenciaszabályokra sem. Példa egy bonyolultabb képletre:

(3+4)*(5+6) = 3 4 + 5 6 + *

A prefixes alak esetén az operátor az operandusok előtt van. Ez egyrészről lehetne a postfixes visszafelé, másrészről viszont szokás mindig zárójelezni. Ugyanis zárójelezés esetén könnyedén tudjuk azt is jelezni, ha egy operátornak kettőnél több operandusa van:

(+ 4 5 6)

Ez a 4, 5 és 6 számok összege. Ezt infix alakban le sem tudjuk írni, csak két külön összeadással – amely egyrészről ugyanazt jelenti, másrészről viszont korántsem ugyanaz:

4+5+6 = (+ 4 (+ 5 6))

5. A nyelvtani szabályok, EBNF

Kezdetnek próbáljuk megfogalmazni, hogy néz ki egy egész szám:

  • Egy szám legalább egy számjegyből áll.
  • Egy számjegy a '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' szimbólumok valamelyike.
  • Ha a szám több számjegyű, akkor a legelső számjegy nem lehet nulla.


A fenti, szabad nyelvi leírás nehezen áttekinthető és nem formális, ami nehezíti az algoritmizálást.

Az egész szám formális leírása EBNF alakban:

szám              ::= számjegy|(számjegy_nemnulla számjegy+)
számjegy_nemnulla ::= '1'|'2'|'3'|'4'|'5'|'6'|'7'|'8'|'9'
számjegy          ::= '0'|számjegy_nemnulla
  • ::= egy nyelvtani szabály definíciója
  • 'c' egy tényleges karakter a szövegben
  • | opció (vagy az egyik elem van ott, vagy a másik)
  • + egy, vagy több ugyanolyan elem
  • () egy együtt kezelt egység (ua. mint algebrában)
  • egymás után írt szimbólumok pedig egymást kell kövessék a szövegben.

Többféle EBNF alak létezik, mi most a W3C konzorcium szabványát használjuk.

6. A rekurzív alászálló értelmező

recursive
descent
parser

A sztringet értelmező program felépítésének egyik módja, ha az ún. „rekurzív alászálló értelmezőt” valósítjuk meg. Ez tulajdonképp egy visszalépő kereséssel oldja meg a szöveg illesztését a szabályokra.

Ebben minden szabálynak megfeleltetünk egy függvényt. A függvény átveszi az értelmezendő sztring címét, amit a futása során a sikeresen felismert szakasz végére állít át. (Ha nem ismert fel semmit, akkor nem változtatja meg.) A függvény visszatérési értéke logikai típusú, ami igaz, ha sikeresen felismert egy szakaszt, és hamis, ha nem történt illeszkedés. Minden függvény ezentúl paraméterként átvett változók címébe tölti a felismert szakasz értelmezésével kapcsolatos eredményeit.

Minden szabályhoz egy függvény:

bool szabaly(char **szoveg, ……… *eredmeny);

Az illesztő függvény így néz ki egyetlen számjegy illesztése esetén:

bool szamjegy(char **szoveg, char *szamjegy) {
    char *ptxt;
    ptxt = *szoveg;                     // munkaváltozat

    if (ptxt[0]>='0' && ptxt[0]<='9') { // számjegy?
        *szamjegy = ptxt[0];
        *szoveg = ptxt+1;
        return true;                    // illeszkedik :)
    }
    else
        return false;                   // nem illik rá :(
}

Az illesztés menete a fenti példafüggvényre (amely egy számjegyet próbál illeszteni a szövegrészre) a következő:

  • Készítünk egy munkaváltozatot ptxt-be. Ahogy haladunk ennek a szabálynak a feldolgozásával, ezzel dolgozunk.
  • Megpróbáljuk számjegyként értelmezni az aktuális karaktert. Ha sikerült, akkor
    1. a paraméterként kapott változóba írjuk az eredményt,
    2. léptetjük az értelmezett szöveget,
    3. igazzal térünk vissza.

A ptxt változóra bonyolultabb szabályok esetén van igazán szükség. Mert ha a szabály illesztése sikertelen, akkor a *szoveg mutatót nem állítjuk el! Ha sikeres, akkor viszont beállítjuk a soron következő értelmezendő szövegrészre. Ez biztosítja ugyanis a összefűzhetőséget, amit később fogunk megvizsgálni.

7. A szabályok használata

#include <stdio.h>
#include <stdbool.h>

bool szamjegy(char **szoveg, char *szamjegy) {
    char *ptxt;
    ptxt = *szoveg;

    if (ptxt[0]>='0' && ptxt[0]<='9') {
        *szamjegy = ptxt[0];
        *szoveg = ptxt+1;
        return true;
    }
    else
        return false;
}

int main(void) {
    char szoveg[100] = "123 hello";

    char t;
    char *leptet = szoveg;
    bool sikerult = szamjegy(&leptet, &t);
    if (sikerult)
        printf("Sikerült: t = %c!\n", t);
    else
        printf("Nem sikerült.\n");

    printf("%s\n", szoveg);
    for (int i = 0; i < leptet-szoveg; ++i)
        printf(" ");
    printf("^\n");
    return 0;
}

8. Matematikai kifejezés – számok összege

Bonyolultabb, összetett szabályokat az ilyen függvények segítségével fogunk felépíteni. Lássuk, hogyan!

Minta (pattern) összeadások és kivonások leírására:

összeg ::= szám (('+' | '-') szám)*

A * tetszőleges számú (akár 0) ismétlést jelent.


A minta illeszkedik (match) ezekre a példákra:

2
234 + 14
1278 - 897
788 + 567 - 34

9. Matematikai kifejezés – precedenciák

Fontos a precedencia: hogyan tudjuk bevezetni a szorzást/osztást?!

Nem vezethetjük be a '+' és '-' alternatívájaként:

kifejezés ::= szám (('+' | '-' | '*' | '/') szám)*

Mert így nem érvényesül az, hogy pl. a

2 + 3 * 6
kifejezésben a szorzás a 3-ra és a 6-ra vonatkozik: 2+(3*6).

Ötlet: legyen a szorzás és osztás egy különálló egység, így azokat egyben kezeli az értelmező.

Ez azt jelenti, hogy egy új szabályt vezetünk be a szorzás és osztás számára.

kifejezés ::= összeg
összeg    ::= szorzat (('+' | '-') szorzat)*
szorzat   ::= szám    (('*' | '/') szám)*

A szabályok jelentése a fentiek alapján a következő:

  • A kifejezésünk egy összeg
  • Az összeg szorzatokból áll (szorzatok összege)
  • A szorzat pedig számok szorzata.

Nézzük meg, hogyan értelmeződik a 2+3*6:

   2        +     3   *    6
 szám           szám     szám
                \___________/
szorzat            szorzat
\________________________/
          összeg
  1. A kifejezés elejére egy szorzatot próbál illeszteni az összeg kifejezés alapján.
  2. Egy szorzat egy számmal kezdődik – ez rendben van: '2' egy szám.
  3. Utána következhetne egy '*' vagy '/' és egy szám – ez nincs itt meg, hiszen '+' jön, de nem baj, mert egy szorzat lehet egy szám önmagában.
  4. Folytatjuk az összeg értelmezését és találunk is egy '+'-t, tehát megint egy szorzatnak kell jönnie.
  5. Ott egy szorzat: "3*6", amit tehát szorzatként ismerünk fel! Az eredményét összeadjuk az összeg első tagjával.

Figyeljük meg, hogy a fenti nyelvtan továbbra is leírja az összes esetet, amit az összeadás/kivonás vizsgálatakor megnéztünk, hiszen a szorzat bármikor leegyszerűsödhet egy egyszerű számmá, amivel visszakapjuk az eredeti, egy szabályból álló nyelvtant.

10. Matematikai kifejezés – zárójelek

Ami egy kifejezés értelmezését bonyolulttá teszi, az a zárójel!

Minden zárójelen belül egy új kifejezés van: úgy kell értelmezni, mint az egészet, és önálló egységként kell kezelni. Ezen felül: tetszőleges mélységben egymásba ágyazható kifejezéseket kell értelmeznünk.


Megoldás: új szabályt kell bevezetni a zárójel lekezélésére – ezzel megoldjuk, hogy egy egységként legyen lekezelve (precedencia). Mi lehet egy zárójelpáron belül? Egy új matematikai kifejezés, vagyis a legalacsonyabb szabályunk! Ez egy rekurzió.

Új szabályt kell bevezetni a zárójel kezelésére:

kifejezés ::= összeg
összeg    ::= szorzat (('+' | '-') szorzat)*
szorzat   ::= tényező (('*' | '/') tényező)*
tényező   ::= szám | zárójeles
zárójeles ::= '(' kifejezés ')'

Így értelmezhetővé válnak az alábbi alakok is:

Kész a
nyelvtan!
2 + 3 * (4 - 8 * 2)
12 * ((3 - 4) * 14)

Kész a nyelvtan! Nézzük meg, hogyan lehet ezt programmá alakítani!

11. A kiértékelő megírása I.

A rekurzív alászálló értelmező fentebb leírt függvényalakja könnyűvé teszi a nyelvtanok algoritmizálását. A minták az alábbiak.

Opcionalitás

'-'? szám

Vagyis ott vagy van egy elem, vagy nincs:

char *szoveg = "-223";

if (szabaly_minusz(&szoveg, ………)) {
    /* sikeres illeszkedés feldolgozása */
}

/* további részek feldolgozása */
……… szabaly_szam(&szoveg, ………);

Az opcionális elem feldolgozásának menete a következő. Ha a szabaly() az illeszkedése sikeres volt, akkor a szoveg mutató a soronkövetkező, értelmezendő karakterre mutat, és a szabaly() „………”-al jelölt argumentum(ok)ban visszaadta az illesztett rész feldolgozott eredményét. Ilyenkor, kilépve az if blokkból a soronkövetkező szabályok már a léptetett mutatót kapják meg.

Ha nem történt értelmezés, akkor egyszerűen tovább lépünk az if-en és a változatlan mutatót kapják a további szabályok, vagyis ugyanonnan próbálják értelmezni a sztringet, mint ahonnan a szabaly próbálta sikertelenül.

Tetszőleges számú előfordulás

szám ::= nemnullaszámjegy bármilyenszámjegy*

Vagyis lehet akár nulla, egy vagy sok illeszkedő rész:

char *szoveg = "223";

/* előző részek feldolgozása */

while (szabaly_barmilyenszamjegy(&szoveg, ………)) {
    /* első és további illeszkedések feldolgozása */
}

/* további részek feldolgozása */

A működése hasonló a fentihez, csak a szabály illesztése többször is megtörténik. Minden egyes sikeres illesztésnél a szoveg pointer módosul, így a különböző hívások eltérő szövegrészeken dolgoznak.

Legalább egyszeri előfordulás

név ::= vezetéknév keresztnév+

Az if felel az első előfordulásért, a ciklus a többiért:

char *szoveg = "Kiss István Pista";

/* előző részek feldolgozása */

if (szabaly_keresztnev(&szoveg, ………)) {
    /* első illeszkedés feldolgozása */

    while (szabaly_keresztnev(&szoveg, ………)) {
        /* további illeszkedések feldolgozása */
    }
}
else
  return false;  /* nincs illeszkedés! */

A *-nál előfordulhat, hogy egyszer sem lépünk be a ciklusba, de akárhány iteráció is történhet.

Ha sikeres az első illesztés (if), akkor feldolgozzuk, és egy while ciklussal addig próbálunk illeszteni, amíg lehet. Az eredményeket pedig fokozatosan dolgozzuk fel a ciklusmagban.

Akár sikeresek voltak a ciklusbel illesztések, akár nem, illetve akármennyiszer illesztettünk, mindenképpen a soron következő, értelmezendő karakterre mutat a pointer a ciklus alatti kódrészekben.

Opciók

megszólítás ::= 'Tisztelt' ('Hölgyem'|'Uram')

Kihasználjuk a logikai rövidzárat!

char *szoveg = "Tisztelt Uram!";

/* előző részek feldolgozása */
/* pointer az U betűre mutat */

if (szabaly_holgyem(&szoveg, ………)
    || szabaly_uram(&szoveg, ………)) {

   /* bármelyik opció teljesül, itt értékeljük ki */

}
else
   return false;    /* nem illeszkedett! */

/* további részek feldolgozása */

Az opcióknál ha az első szabály sikerrel értékelődik ki, akkor a logikai rövidzár miatt a másodikat már nem is próbálja meg illeszteni! Ha pedig nem sikerült az elsőt illeszteni, csak akkor próbálja meg a másodikat.

Az if belesejébe csak akkor lépünk, ha a szabályok valamelyike sikeresen illeszkedett. Természetesen az argumentumokként kapott értékek alapján, vagy az egyes szabályok visszatérési értékének elmentése segítségével ellenőriznünk kell, hogy melyik szabály illeszkedett ténylegesen.

Egymásra következés

megszólítás ::= 'Tisztelt' címzett

Itt is kihasználjuk a logikai rövidzárat!

char *szoveg = "Tisztelt Uram!";

char *ptxt = szoveg;
if (szabaly_tisztelt(&ptxt, ………)
    && szabaly_cimzett(&ptxt, ………)) {

   /* illeszkedés feldolgozása */

}
else
   return false;    /* nem illeszkedett! */

/* további részek feldolgozása */

Csak siker esetén értékeli ki a második szabályt, tehát az egymásrakövetkezés ki van kényszerítve, de vigyázni kell rá, hogy ha az első szabály illeszkedett, akkor itt odébb lett állítva a pointer, és ezért ilyenkor kiléphetünk úgy az if-ből, hogy nem volt teljes illeszkedés, de a pointer olyan pontra mutat, ami túl van az első még értelmezetlen karakteren. Ezért őt vissza kell állítani sikertelen esetben (vagy ha függvényben vagyunk, meg sem változtatni a cím szerinti paramétert, és visszatérni HAMIS értékkel.)

12. A kiértékelő megírása II.

Egy bonyolultabb szabály C illesztő függvénnyé alakítva:

bool szorzat(char **szoveg, int *ertek) {
    char *ptxt = *szoveg;
    int val;

    if (szam(&ptxt, &val)) {
        int val2;
        char c;
        while (csillagper_szam(&ptxt, &c, &val2)) {
            if (c == '*') val *= val2;
            else val /= val2;
        }
        *ertek = val;
        *szoveg = ptxt;
        return true;
    } else {
        return false;
    }
}
szorzat ::= szám (('*' | '/') szám)*

Az első számot val-ban menti el, majd ( ('+' | '-') szam)* illesztést próbál csinálni az egymásrakövetkezés és a tetszőleges számú ismétlődés együttes alkalmazásával, és val mindig megváltoztatja a történtek függvényében.

Itt szükség van még egy változóra (val2), amelyben a további szorzat részeredményeket eltárolhatjuk, hiszen a val értékét csak mi változtathatjuk az első illeszkedés után.

A nyelvtani szabályok alapján az elemző kód megalkotása automatizálható. Erre vannak is programok, pl. a yacc, amelyik C kódot generál. A Spirit pedig egy olyan függvénykönyvtár, amely a C++ nyelvet egészíti ki úgy, hogy a nyelv eszközeivel EBNF-szerű kifejezéseket tudunk megfogalmazni.

Buktató: vegyes logikai kifejezések?

Van néhány buktató, amire figyelni kell. Az egyik:

('Tisztelt' név) | 'Helló'
if ((tisztelt(&szoveg, ………) && nev(&szoveg, ………))
    || hello(&szoveg, ………)) {
    /* illeszkedés feldolgozása */
}

Ha tisztelt() illeszkedik, de nev() nem, akkor megpróbálja hello()-t illeszteni, de tisztelt() elállította a mutatót! Ilyenkor vissza kell állítani a mutatót a szöveg elejére az ÉS kapcsolat sikertelensége esetén, és utána lehet megpróbálni hello()-t illeszteni.

Buktató: ciklusban egymásra következés?

A másik:

(szabály1 szabály2)*
while (szabaly1(&szoveg, ………) && szabaly2(&szoveg, ………) {
    /* illeszkedés feldolgozása */
}

Ez sem jó ötlet. Itt is megsértjük a fenti szabályokat, miszerint ha sikerül az illesztés, léptetjük a pointert, ha nem sikerül, akkor pedig marad. Ugyanis előfordulhat, hogy szabaly1() illeszkedik, de szabaly2() nem. Ilyenkor a teljes kifejezés értéke hamis, és a végrehajtás nem kerül be a ciklusmagba – de a szabaly1() elállította a mutatót!

13. Kifejezés kiértékelő működés közben

Példa bemenet:

3+4*5
A parser letölthető
az infoc-ről!

A teljes program letölthető erről a linkről: parser.c.

Ez annyiban tud többet a fentiekben bemutatottnál, hogy bárhol elfogad szóközöket is a kiértékelt kifejezésben (erre utal a nevében a ws szócska: whitespace). A megvalósításában azonban mindenhol a fent bemutatottakat követi. Mivel a leírt nyelvtanunk nem ismeri a negatív számokat, ezért csak pozitív és csak egész számokon működik a program! Pl. 5+-3 azt fogja mondani, hogy nem tudja értelmezni.

Amit még vizsgál ez a program, az pedig az, hogy teljes kifejezést kapott-e; tehát hogy a sztring elején lévő értelmezhető kifejezés után nincsenek-e további karakterek. Ugyanis pl. egy "1 + 2 * 3 (" tartalmú sztringnél az eddigi kifejezés szabályunk eljutna az "1 + 2 * 3"-ig, utána pedig azt mondaná, hogy az illesztés megtörtént. Vagyis amikor a teljes sztringet vizsgáljuk, akkor meg kell azt is nézni, hogy az illesztés után a sztring végére értünk-e.

A cellák kiértékelése

15. A cellák kiértékelése

Térjünk át a táblázatkezelő részre. A cellák kiértékelése:

  • meg kell különböztetni a szöveget és a képletet tartalmazó cellákat,
  • fel kell tudni dolgozni a cellahivatkozásokat,
  • észre kell venni a körkörös hivatkozásokat és jelezni kell őket.
 A B C
1
2
3
4
5

Kiértékelés menete pl. B2-re:

B2 ┬→ B1
   └→ B5 → B4

A cellák bejárása rekurzívan történik:

FÜGGVÉNY kiszamit(cellacím) {
    HA (van cella hivatkozás)
        kiszamit(hivatkozott cella);

    cella érték beállítása;
}

Vagyis kiszámítjuk a hivatkozott cellák értékét, mert utána az aktuális cella értéke már megmondható.

16. A körkörös hivatkozás

 A B
1
2

A1→B1→B2→A1

A probléma a fenti kóddal az, hogy ha körkörös hivatkozás van, akkor végtelen lesz a rekurzió. Kell egy megfelelő leállási feltétel.

A megoldást az jelenti, hogy a cellabejárás során megjelöljük a cellákat, ahol jártunk már. Így ha jelölt cellára visszajutunk, akkor az körkörös hivatkozást jelent. Ekkor hibaüzenetet adunk a cella címének megjelölésével. Amikor egy cella értékét sikeresen kiszámítottuk, a bejelölést megszüntetjük, így „teszünk rendet” magunk után.

FÜGGVÉNY kiszamit(cellacím) → logikai {
    HA (jártunk itt)
        VISSZA: HAMIS;

    cella megjelölése;    + jel

    HA (van cella hivatkozás) {
        HA (!kiszamit(hivatkozott cella))
            VISSZA: HAMIS;
    }
    cellaérték beállítása;

    cellajelölés törlése; - jel

    VISSZA: IGAZ;
}

17. A táblázat reprezentálása: az adatszerkezet

Maga a táblázat egy kétdimenziós tömb:

typedef struct Tablazat {
    Cella **adat;           /* 2D din. tömb */
    int szelesseg;
    int magassag;
} Tablazat;

A táblázat egy cellája:

typedef struct Cella {
    char *tartalom;         /* din. sztring */

    bool kiszamitva;
    double ertek;           /* ne kelljen többször */

    bool mar_jartunk_itt;   /* bejáráshoz */
} Cella;

A Cella struktúrában a kiszamitva mező azt jelenti, hogy az adott körben már meghatároztuk a cella tartalmát. Ez két okból történhetett: a bejárás során már érintettük a cellát vagy egy korábbi cella hivatkozott rá és a hivatkozás feloldásakor ugrottunk ide. Ha a kiszamitva igaz, akkor a ertek mező tartalmazza a cella érvényes értékét.

A mar_jartunk_itt adattagot csak bejárás közben használjuk majd, hogy a körkörös hivatkozásokat fel tudjuk ismerni. Ezt a jelölést nem feltétlenül kellene itt a cellánál tárolnunk; bejárás közben építhetnénk hozzá külön adatszerkezetet is. De mivel tudjuk, hogy minden cellához pontosan egy jelölés kell csak, ez a legkézenfekvőbb és egyben legegyszerűbb megoldás. BSZ2 tárgyból lesz szó gráfbejáró algoritmusokról, ahol ez a probléma ugyanígy fel fog merülni.

18. Egy cella értékének kiszámítása

bool cella_kiszamit(Tablazat *t, int sor, int oszlop) {
    Cella *cella = &t->adat[sor][oszlop];

    if (cella->mar_jartunk_itt) return false;    // körkörös
    if (cella->kiszamitva) return true; // már kész

    cella->mar_jartunk_itt = true;

    double ertek;
    if (kiertekel(cella->tartalom, &ertek, t)) { // ok?
        cella->ertek = ertek;
        cella->kiszamitva = true;
        cella->mar_jartunk_itt = false;
        return true;                             // ok!
    } else {
        cella->mar_jartunk_itt = false;
        return false;                            // hiba
    }
}

A cella_kiszamit() hívja a kiertekel()-t:

… és a kiertekel() hívja a cella_kiszamit()-t. Kölcsönös rekurzió!

A kiertekel() függvény meghívása az a pont, ahol kapcsolódunk az előző előadásrészben bemutatott szintaktikai elemzőhöz. Ez lesz az a függvény, amelyik a cellában található matematikai képletet kiértékeli. Persze módosítani kellett, hogy lássa a táblázatot is; a képletek tartalmaznak cellahivatkozásokat is, ezért arra szükség lehet.

19. Táblázatkezelő futás közben

Letölthető:
tablazatkezelo.zip
Utasítás: kiir
───┬───────────┬─────────┬─────────┬─────────┬─────────┬
   │     A     │    B    │    C    │    D    │    E    │
───┼───────────┼─────────┼─────────┼─────────┼─────────┼
 0 │ Osszetevo │ Rum     │      12 │ Ft/ml   │         │
 1 │           │ Cola    │       2 │ Ft/ml   │         │
 2 │ CubaLibre │         │     800 │ Ft      │         │
 3 │           │         │         │         │         │
 4 │           │         │         │         │         │
───┴───────────┴─────────┴─────────┴─────────┴─────────┴

Utasítás: vizsgal c2

C2:  =C0*50+C1*100

Utasítás: _

A táblázatkezelő letölthető: tablazatkezelo.zip. A szokásos módon a nagyobb programhoz bemutatjuk a függvények hívási összefüggéseit mutató, egyszerűsített ábrát, a nagyobb program áttekintéséhez.

Érdemes megfigyelni, hogy ebben két kör is van. Az egyik az összegszorzattényezőzárójelesösszeg kör, amely a rekurzív alászálló értelmező része. A másik pedig a cella_kiszámítkiértékel...cella_kiszámít kör, amely az előző dián bemutatott működést biztosítja. A cella matematikai képletet tartalmaz, a matematikai képlet pedig cellát hivatkozhat.

20. A cella módosítása

Csak akkor írhatunk bele, ha nem viszünk be körkörös hivatkozást:

void cella_modosit(Tablazat *t, int sor, int oszlop, char *tartalom) {
   char *regi = t->adat[sor][oszlop].tartalom;

   /* megpróbáljuk betenni az újat */
   char *uj = (char *) malloc(strlen(tartalom) + 1);
   strcpy(uj, tartalom);
   t->adat[sor][oszlop].tartalom = uj;

   /* sikerült? */
   if (cella_kiszamit(t, sor, oszlop)) { // ok?
      free(regi);
   } else {                                       // hiba!
      free(uj);
      t->adat[sor][oszlop].tartalom = regi;
   }
}

A cella újra kiszámítása csak a körkörös hivatkozások felfedésére jó, mint hibakereső. Más cellák hivatkozásait is elronthatjuk ezzel.

21. Fordítási függőségek csökkentése

Minden modul mutassa a lehető legkisebb felületet!

elemzo.h
#include "tablazat.h"

bool kiertekel(char *szoveg, double *ertek, Tablazat *t);

Az elemzőnek egyedül a kiertekel függvényét ismeri a táblázatkezelő, hiszen ez az ún. "kezdőszabály". Ez az a szabály, ami egy teljes kifejezés szerkezetét leírja. Az egyes részleteket leíró szabályok függvényeit csak modulon belül hívjuk, így ezeket a külvilágnak nem kell ismerniük.

Az elemző többi függvénye statikus – tehát a modulra nézve lokális.

elemzo.c
#include "elemzo.h"

static bool szokoz(char **txt)         { ……… }
static bool karakter(char **txt, ………)  { ……… }
static bool szam(char **txt, ………)      { ……… }
static bool cellacim(char **txt, ………)  { ……… }
static bool osszeg(char **txt, ………)    { ……… }
static bool szorzat(char **txt, ………)   { ……… }
static bool tenyezo(char **txt, ………)   { ……… }
static bool zarojeles(char **txt, ………) { ……… }

Miért jó, ha így csináljuk? Két okból. Egyrészt, aki használja az elemzőt, nem kell gondolkodjon, hogy a sok számára érthetetlen függvény közül melyiket kell hívja. Egy függvény érhető el, a kiertekel(). Másrészt a fejlécfájlt más forrásba is beillesztjük, ezért ha változik, azokat a forrásokat is újra kell fordítani. Ha a kifelé nem mutatott részeket nem írjuk bele, akkor az ottani változtatások miatt nem kell feleslegesen sok mindent újrafordítani a programban.

Epilógus

23. https://infoc.eet.bme.hu/

Egy C99 függvény belsejében elhelyezhetünk egy Web címet:

#include <stdio.h>

int main(void) {
    int i = 0;

    https://infoc.eet.bme.hu/
    printf("Helló, InfoC!\n");
    i++;

    if (i < 10) goto https;

    return 0;
}

Ez azért van, mivel a kettősponttal egy címkét jelölhetünk meg a kódban, ahova a goto utasítással lehet ugrani. A címke neve itt https lesz – így az if-fel és a goto https-sel egy hátultesztelő ciklust valósítunk meg. Természetsen a címkék neve egyedi kell legyen, tehát egy függvényben csak egy https:// kezdetű címünk lehet.

A // a C99 óta kommentet is jelöl. Ez a C++ nyelvből származik, onnan „szivárgott vissza” az ősébe, a C-be.




24. typedef void Ize, Semmi;

A void typedef-elhető:

typedef void Ize, Semmi;

Semmi foreach(Ize *ettol, Ize *eddig, Semmi (*fv)(Ize *),
              Semmi (*kov)(Ize **)) {
    for (Ize *iter = ettol; iter != eddig; kov(&iter))
        fv(iter);
}

Semmi intkiir(Ize *pi) {
    printf("%d ", *(int *)pi);
}

Semmi intptrnovel(Ize **pi) {
    ++*(int**)pi;   // here be dragons
}

foreach(tomb, tomb+5, intkiir, intptrnovel);

25. Mit ír ki?

int main(void) {
    int a[] = { 0, 10, 20, 30, 40, 50 };

    // drunk, fix later
    printf("%d", 3[a]);

    return 0;
}

Tipp: az összeadás kommutatív: tagjai felcserélhetőek.

int main(void) {
    int c;

    c = 1["Hello"];
    printf("%c", c);

    return 0;
}

A sztring C-ben: csak egy karaktertömb, semmi más. Ha tömb, akkor kifejezésben a rá mutató pointer képződik. Az indexelés összeadássá és dereferálássá alakul fordítás közben, tehát az 1["Hello"] kifejezés *(1 + (char*) "Hello")-t jelent.

26. #define trollkodás

Mire jó még a #define?

A for (;;) ciklus végtelennek számít. Mivel nincs ciklusfeltétele, ezért nem vizsgálunk benne olyan állítást, ami hamis lehetne, és megállítaná a ciklust. Ha meg nem állítunk semmit, az igaz. for ever, örökké fut.

#define ever (;;)

for ever {
    printf("I ♥ U\n");
}

No meg aztán, a struct és a union szintaktikája „vészesen” hasonlít egymásra:

struct szamok {
    int i;
    double d;
};
union szamok {
    int i;
    double d;
};

Tehát a teendők:

  • Bekapcsolni a szobatárs gépét
  • Megkeresni a fejlesztőkörnyezet stdio.h-ját
  • Beírni az elejére: #define struct union
  • Várni a hatást :D

27. Öndokumentáló kód!

main(                int l
  ){char            h=-1,*
   c="\\"          "/";
    scanf        ("%d"
     ,&l);      while
       (h++<   l*2)
          printf(
        "%*c%*c\n"
       ,h-2*(h/l)*(
      h%l)-h   /l+1,c
    [h/l],      2*(l-h
   )+4*(h        /l)*(h
  %l)+2*          (h/l)-
 1,c[1-            h/l]);}

Juhász Bálint:
X kirajzolása

         palacsInt_a
        pancake_sort(
  palacsInt_a *t, int meret)     //=====
   {int i,f_db;if(meret==1)     //
   {return 0;}i=maxkeres(t,    //
      meret);f_db=0;if        //
     (i==0){fordit(t,0,      //
        meret-1);f_db       /*
---------------------------*/
    =1;}else if(i!=meret
       -1){fordit(t
      ,0,i);fordit(t,
  0,meret-1);f_db=2;}return
pancake_sort(t,meret-1)+f_db;}

Dömők Dávid:
Palacsintarendezés

28. IOCCC – Mit ír ki?

#include <stdio.h>
main(t,_,a) char *a; {
return!0<t?t<3?main(-79,-13,a+main(-87,1-_,main(-86,0,a+1)+a)):
1,t<_?main(t+1,_,a):3,main(-94,-27+t,a)&&t==2?_<13?
main(2,_+1,"%s %d %d\n"):9:16:t<0?t<-72?main(_,t,
"@n'+,#'/*{}w+/w#cdnr/+,{}r/*de}+,/*{*+,/w{%+,/w#q#n+,/#{l+,/n{n+,/+#n+,/#\
;#q#n+,/+k#;*+,/'r :'d*'3,}{w+K w'K:'+}e#';dq#'l \
q#'+d'K#!/+k#;q#'r}eKK#}w'r}eKK{nl]'/#;#q#n'){)#}w'){){nl]'/+#n';d}rw' i;#\
){nl]!/n{n#'; r{#w'r nc{nl]'/#{l,+'K {rw' iK{;[{nl]'/w#q#n'wk nw' \
iwk{KK{nl]!/w{%'l##w#' i; :{nl]'/*{q#'ld;r'}{nlwb!/*de}'c \
;;{nl'-{}rw]'/+,}##'*}#nc,',#nw]'/+kd'+e}+;#'rdq#w! nr'/ ') }+}{rl#'{n' ')# \
}'+}##(!!/")
:t<-50?_==*a?putchar(31[a]):main(-65,_,a+1):main((*a=='/')+t,_,a+1)
:0<t?main(2,2,"%s"):*a=='/'||main(0,main(-61,*a,
"!ek;dc i@bK'(q)-[w]*%n+r3#l,{}:\nuwloca-O;m .vpbks,fxntdCeghiry"),a+1);
}

Ehhez hasonló C kódokat a The International Obfuscated C Code Contest oldalon lehet találni. Ők minden évben megrendeznek egy versenyt, hogy ki tud olvashatatlanabb C kódot írni.

Hogy működik a fenti? Reverse Engineering the Twelve Days of Christmas.