6. gyakorlat: származtatott típusok

1 Tic-tac-toe – 2D tömb

Tic-tac-toe

3×3-as amőba (tic-tac-toe) játékot csinálunk. A 3×3-as pálya egyes állapotai lehetnek: üres (még nem rajzolt oda senki), kör és iksz.

Megoldás

A pálya egyes celláinak tárolására egyik megoldás az, hogy karaktereket használunk. A szóköz jelentheti az üres cellát, az x az egyik, az o pedig a másik játékost. A pálya ezen cellák két dimenziós tömbje – vagyis tömbben tömb. Mivel egy sorban sok ilyen van; és a sorokból is sok van.

A programban meg kell vizsgálni, az egyes sorokban, oszlopokban stb. van-e az x játékosnak nyerő helyzete; ha bármelyikben igen, akkor az egész állás számára nyerő.

A pálya kirajzolása ciklusban ciklus. Vegyük észre: egy dimenziós tömb → ciklus, két dimenziós tömb (tömbben tömb) → ciklusban ciklus.

#include <stdio.h>

int main()
{
    typedef char Babu;
    typedef Babu Palya[3][3];

    Palya p = {
      { 'o', 'o', 'o' },
      { 'x', 'o', ' ' },
      { 'x', 'x', ' ' },
    };
    int x, y;
    int nyert;

    /* kirajzolás */
    printf("+---+\n");
    for (y=0; y<3; y++) {
        printf("|");
        for (x=0; x<3; x++)
            printf("%c", p[y][x]);
        printf("|\n");
    }
    printf("+---+\n");

    /* nyert-e az x? */
    nyert=0;
    /* vizszintesen --- */
    for (y=0; y<3; y++)
        if (p[y][0]=='x' && p[y][1]=='x' && p[y][2]=='x')
            nyert=1;
    /* fuggolegesen ||| */
    for (x=0; x<3; x++)
        if (p[0][x]=='x' && p[1][x]=='x' && p[2][x]=='x')
            nyert=1;
    /* atlosan \ */
    if (p[0][0]=='x' && p[1][1]=='x' && p[2][2]=='x')
        nyert=1;
    /* atlosan / */
    if (p[0][2]=='x' && p[1][1]=='x' && p[2][0]=='x')
        nyert=1;

    if (nyert)
        printf("iksznek van nyero harmasa!\n");
    else
        printf("iksznek nincs nyero harmasa.\n");

    return 0;
}

A megoldás kicsit kezdetleges. A karakteres ábrázolás hátránya, hogy az egyes cellák ezen kívül más értéket is felvehetnek. Ennek kiküszöbölésére is jó az ún. felsorolt típus, amelyről az előadáson lesz szó.

Ugyancsak, a nyerést ellenőrző programrészt legjobb lenne paraméteresen megoldani – vagyis egy függvényre lenne szükségünk, amely a pályát (tömböt) paraméterként kapja, és egy játékost is, akinek a nyerő állását ellenőrizni kell. (Hiszen ugyanígy működne a program, ha a kör játékos szempontjából kellene ellenőrizni a nyerő állást.) Erről is a következő előadáson lesz szó.

2 Időpontok

Írjunk programot, amely egy struktúrában időpontot tárol: óra, perc. Írjunk függvényeket ehhez:

Megoldás

A 60 perc és a 24 óra kezelésére a percek hozzáadásánál nagyon jól használható a maradékképzés. Pl. 16:55+10 esetén: :55+10 = :65, ami helyett a következő óra :05 kellene. 65%60, vagyis a 60-nal osztás maradéka pont a percet adja, 65/60, maga az osztás pedig 1-et, amennyivel az órát meg kell növelni. Az órát utána egyszerűen 24-gyel modulózzuk, mert a napokkal már nem kell foglalkozni.

A kivonás nem megy ilyen egyszerűen, mert (5-10)%60 = (-5)%60 = -50. Ott sajnos külön kell kezelni a keletkező negatív értékeket.

#include <stdio.h>

/* Időpontot tárol egy napon belül: óra és perc. */
typedef struct Ido {
    int ora, perc;
} Ido;

/* Kiírja a paraméterként kapott időpontot óra:perc formában. */
void ido_kiir(Ido i)
{
    printf("%02d:%02d", i.ora, i.perc);
}

/* Hozzáad a megadott i időponthoz p percet, és visszatér
 * az így kapott időponttal. A hozzáadott percek száma azért
 * unsigned, mert negatív számra helytelenül működne a függvény. */
Ido ido_hozzaad(Ido i, unsigned p)
{
    Ido uj;
    uj.perc=(i.perc + p)%60;
    uj.ora=(i.ora + (i.perc + p)/60)%24;
    return uj;
}

/* Kiszámolja, hány perc telt el i1-től i2-ig. A paraméterek
 * sorrendje a kivonásnál megszokott: i2-i1, rendre a
 * kisebbítendő és a kivonandó. Azzal a feltételezéssel ad
 * helyes eredményt, hogy a két időpont egy napon van. */
int ido_eltelt(Ido i2, Ido i1)
{
    return i2.ora*60-i1.ora*60+i2.perc-i1.perc;
}

/* Kivon valahány percet a megadott időpontból, és az így keletkező
 * új időponttal tér vissza. Negatív p-re helytelenül működne, ezért
 * unsigned a paramétere. */
Ido ido_kivon(Ido i, unsigned p)
{
    Ido uj;
    uj.perc = i.perc-p;
    uj.ora = i.ora;
    while (uj.perc < 0) {
        uj.perc += 60;
        uj.ora -= 1;
    }
    while (uj.ora < 0)
        uj.ora += 24;
    return uj;
}


int main()
{
    Ido i1 = { 11, 50 }, i2 = { 12, 10 }, i3 = { 3, 30 };

    printf("i1 = "); ido_kiir(i1); printf("\n");
    printf("i2 = "); ido_kiir(i2); printf("\n");
    printf("i3 = "); ido_kiir(i3); printf("\n");

    printf("i2-i1 = %d\n", ido_eltelt(i2, i1));
    printf("i1+195 = "); ido_kiir(ido_hozzaad(i1, 195)); printf("\n");
    printf("i2-195 = "); ido_kiir(ido_kivon(i2, 195)); printf("\n");
    printf("i3-240 = "); ido_kiir(ido_kivon(i3, 240)); printf("\n");

    return 0;
}

3 Római számok II.

Elevenítsük fel a római számok kiírása programot! Ott a számok értéke szerint csökkenő sorrendben haladtunk. Valahogy így:

…
if (szam>=5) { printf("V"); szam-=5; }
if (szam>=4) { printf("IV"); szam-=4; }
while (szam>=1) { printf("I"); szam-=1; }
…

Figyelmesen vizsgálva a problémát rájöhettünk arra, hogy bármelyik feltétel kicserélhető ciklusra ebben a feladatban. Azért írtunk feltételt a szam>=5 kifejezéshez, mert tudjuk, hogy legfeljebb csak egyszer fog teljesülni – de akár írhattunk volna ciklust is.

Ha mindegyik helyre ciklus kerül, akkor így néz ki a kód:

…
while (szam>=5) { printf("V"); szam-=5; }
while (szam>=4) { printf("IV"); szam-=4; }
while (szam>=1) { printf("I"); szam-=1; }
…

Ez a sorminta már csak arra vár, hogy ciklust csináljunk belőle. Írjuk át a programot!

Megoldás

Minden római számhoz (pl. V, egy sztring C-ben) egy érték tartozik (pl. 5, egy egész szám C-ben), ez struktúrába való. A sok római szám struktúrái pedig tömbbe. Használhatjuk azt a trükköt, mint a lenti bankautomatánál is, hogy egy oda nem illő értékkel megjelöljük a tömb végét. Mivel a rómaiak nem használtak 0-t, a tömb végét jelölheti egy olyan elem, ahol az érték 0.

#include <stdio.h>

int main()
{
   typedef struct Romai {
      char romai[5];
      int ertek;
   } Romai;
   Romai szamjegyek[] = {
      { "XC", 90 },
      { "L", 50 },
      { "XL", 40 },
      { "X", 10 },
      { "IX", 9 },
      { "V", 5 },
      { "IV", 4 },
      { "I", 1 },
      { "", 0 }      /* tömb végét jelző */
   };
   int szam;

   printf("Mi a szám? ");
   scanf("%d", &szam);
   if (szam<1 || szam>99)
      printf("Ez nekem már sok. Csak 99-ig tudom.\n");
   else {
      int i;

      i=0;
      while (szamjegyek[i].ertek>0) {
         while (szam>=szamjegyek[i].ertek) {
            printf("%s", szamjegyek[i].romai);
            szam-=szamjegyek[i].ertek;
         }
         ++i;  /* következő római szám */
      }

      printf("\n");
   }

   return 0;
}

A programba épített táblázat egy inicializált tömb a megfelelő római jelekkel és a hozzájuk tartozó értékkel. Ezt a függvényen belül is létrehozhatjuk, mivel máshol nem használjuk. A struktúra definícióját egybe is építhetjük a tömb változó létrehozásával. Ilyenkor még az is lehetséges, hogy a struktúra névtelen, mivel sehol máshol nem kell hivatkozni név szerint a típusra:

struct {
   char romai[5];
   int ertek;
} szamjegyek[] = {
   { "XC", 90 },
   { "L", 50 },
   { "XL", 40 },
   { "X", 10 },
   { "IX", 9 },
   { "V", 5 },
   { "IV", 4 },
   { "I", 1 },
   { "", 0 }      /* tömb végét jelző */
};

4 Bankautomata II.

Idézzük fel a múltkori bankautomatás feladatot! A feladatkiírás azt kérte, hogy írjunk egy programot, amely egy adott pénzösszeget a megadott névértékű bankjegyekre és érmékre bont le. Pl. 4200 Ft = 2×2000 Ft + 1×200 Ft.

Csavarjuk meg ezt! Az automata rekeszei végesek. Tároljuk el azt is, hogy melyik bankjegyből és érméből épp mennyi van! Írjuk meg a programot, amelyik úgy ad pénzt, hogy ezt figyelembe veszi!

Megoldás

Ez egy kiváló példa a struktúra használatára. Összetartozó adat a rekeszben található címlet és a hozzá tartozó darabszám, pl. hogy 20000 forintosból 20 darab van. Hogy mindkettő egész szám (a forint és a darab), senkit ne jtévesszen meg, ez nem tömb! Minden rekeszhez egy struktúra tartozik. A rekeszekből viszont sok van, és egyformák: ezért a rekeszeknek egy tömbje lesz. A használt adatszerkezet struktúrák tömbje: struct Rekesz penzek[].

A ciklusban először egy osztással kiszámoljuk, hogy mennyi kellene az adott címletből. Utána pedig megnézzük, van-e annyi egyáltalán. Ha nincs, akkor csak kevesebbet adunk ki.

#include <stdio.h>

int main()
{
    typedef struct Rekesz {
        int ertek;
        int darab;
    } Rekesz;
    Rekesz penzek[]={
        {20000, 20},    /* huszezresbol 20 db */
        {10000, 0},     /* tizezres kifogyott */
        {1000, 10},
        {500, 50},
        {20, 197},
        {10, 123},
        {5, 19},
        {0, 0}          /* nullaval jelzem a tomb veget */
    };
    int mennyit;
    int i;

    printf("Mennyit kene adni? ");
    scanf("%d", &mennyit);

    printf("Az automata kiadja:\n");
    for (i=0; penzek[i].ertek!=0; i++) {
       int hany_db;

       hany_db=mennyit/penzek[i].ertek; /* ennyit kene */
       if (penzek[i].darab<hany_db)     /* nincs ennyi? */
           hany_db=penzek[i].darab;     /* jobb hijan... */

       if (hany_db>0) { /* ha adunk ebbol (mert kell es mert van) */
          printf("%d db %d Ft-os.\n", hany_db, penzek[i].ertek);
          mennyit-=hany_db*penzek[i].ertek;
          penzek[i].darab-=hany_db;   /* innen kivesszuk. */
       }
    }
    if (mennyit!=0)
        printf("Nem tudok rendesen adni! Kene meg: %d Ft\n", mennyit);

    return 0;
}

A fenti algoritmus amúgy nem tökéletes. Pl. ha 6000-t kérünk, és van 5000-es és 2000-es, de nincs 1000-es, akkor ki akar adni egy 5000-est, és utána megáll – nem veszi észre, hogy 3 darab 2000-essel megoldható. A tökéletes megoldáshoz az ún. visszalépéses keresést kellene alkalmazni, amelyhez a tudnivalók majd később szerepelnek az előadáson.

5 Dátumok, öröknaptár

Írjunk programot, amely egy struktúrában dátumot tárol: év, hónap, nap. Kezeljék ezeket függvények:

Megoldás

#include <stdio.h>


/* a dátum típusunk */
typedef struct Datum {
    int ev, honap, nap;
} Datum;


/*kiírja a dátumot éééé.hh.nn formában */
void datum_kiir(Datum d)
{
    printf("%4d.%02d.%02d", d.ev, d.honap, d.nap);
}

/* segédfüggvény: szökőév-e? */
int szokoev(int ev)
{
    return ev%400==0 || (ev%100!=0 && ev%4==0);
}

/* megmondja, hogy az év hányadik napja */
int datum_hanyadik(Datum d)
{
    /* hány egész hónapból adódó nap telt el eddig */
    int honapok[]={ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
    int hanyadik, h;
 
    hanyadik=0;
    for (h=1; h<d.honap; ++h)
        hanyadik+=honapok[h-1];
    hanyadik+=d.nap;
    if (szokoev(d.ev) && d.honap>2)
        hanyadik+=1;
 
    return hanyadik;
}

/* hány nap telt el d1-től d2-ig?
 * csak akkor működik helyesen, ha d2>d1. */
int datum_kivon(Datum d2, Datum d1)
{
    int kulonbseg, ev;

    /* a különbség: amennyi különbség van a napok között */
    kulonbseg=datum_hanyadik(d2)-datum_hanyadik(d1);
    /* plusz amennyi különbség van az évek között */
    for (ev=d1.ev; ev<d2.ev; ev+=1)
        kulonbseg += szokoev(ev) ? 366:365;
    return kulonbseg;
}

/* megmondja, milyen napra esett az adott nap.
 * 1=hétfő, 2=kedd, 7=vasárnap. */
int milyen_nap(Datum d)
{
    Datum viszonyitas = { 1900, 1, 1 }; /* hétfő */

    /* megnézzük, hány nap telt el. modulo 7 miatt 0..6
     * lesz az eredmény (7 nap egy héten), ahol 0 lesz
     * a hétfő, mert a fenti dátumhoz képest. */
    return datum_kivon(d, viszonyitas)%7 + 1;
}


int main()
{
    Datum ma={ 2013, 2, 4 }, eleje={ 2012, 9, 3 };

    printf("Ma: ");
    datum_kiir(ma);
    printf(", a hét %d. napja.\n", milyen_nap(ma));

    printf("A szorgalmi időszak kezdete: ");
    datum_kiir(eleje);
    printf(", ennyi nap telt el: %d.\n", datum_kivon(ma, eleje));
    printf("%d. oktatási hét van.\n", datum_kivon(ma, eleje)/7+1);

    return 0;
}

A datum_kivon() függvény végzi a dátumok kivonását. A működésének az a lényege, hogy kivonja egymásból azt a két számot, amely a két dátum év kezdete óta teltelt napjainak száma; és ehhez adja hozzá az egész eltelt évekből adódó 365 vagy 366 napokat. Ez egy példán jól látszik. Ha a 2012.09.03→2013.10.06 eltelt napokat kell kiszámolni, akkor a 10.06-ból 279, a 09.03-ból 247 adódik. Azaz 279-247=32 nap telik el szept. 3 és okt. 6 között. Ehhez kell hozzáadni még egy évnyit. Ha a hónapok szerint visszafelé megyünk (pl. 2012.09.03→2013.02.25, szeptember→február), akkor az összeg első tagja negatív, de ez utána korrigálódik a hozzáadott teljes év által. (Mintha ugranánk egy évet előre, aztán visszajönnénk a megadott dátumig.)

Megfigyelhetjük, hogy a honapok[] tömbnek mindig az első honap-1 elemét összegezzük. Megírhatnánk úgy is a programot, hogy nem a hónapok napjainak számát, hanem ezeket az összegeket tartalmazza a tömb: 0 (január), 31 (február), 59=31+28 (március) stb. Így az összegző ciklust meg lehetne spórolni, de kicsit nehezebben lenne követhető a forráskód.