Öröklés

Dobra Gábor · 2019.03.14.

A mai óra célja az öröklés használatának és nyelvi elemeinek megismerése.

Felkészülés a gyakorlatra

1. Örökléssel kapcsolatos fogalmak

A gyakorlatvezető segítségével tekintsétek át a fontosabb örökléssel kapcsolatos fogalmakat!

Összefoglaló
  • öröklés
  • ősosztály
  • leszármazott
  • virtuális függvény
  • tisztán virtuális függvény
  • absztrakt osztály
  • :public. (A :private és a :protected nem öröklés!)
  • protected:
  • polimorfizmus
  • viselkedés
  • bővítés, felüldefiniálás
  • kompatibilitás
  • szeletelődés

Tisztázzátok a polimorfizmus fogalmát!

Megoldás

A polimorfizmus jelentése: többalakúság, az öröklés nyújtotta polimorfizmusnál általánosabb fogalom. Az öröklésnél az ősosztály viselkedése az, ami polimorf, méghozzá futásidőben derül ki, hogy hogyan (bárki írhat leszármazottakat, tehát nyitott végű). Szintén polimorf még:

  • C-ben az osztás működése: 1/2 == 0, de 1.0/2 == 0.5, mert az operator/ más típusú operandusokra máshogy viselkedik. Fordítási időben dől el, hogy mikor melyik.
  • C++-ban általánosabban: az operator overload, szintén fordítási idejű.
  • Még általánosabban: függvényoverload, szintén fordítási idejű.
  • template, ami akár a fenti fordítási idejű cuccok kombinációjából is eredhet, lásd a példát.
  • [union, tagged union, variant, futásidejű, zárt végű.]

Fordítási idejű polimorfizmusra két példa egyben (az operator+ és a duplaz függvény is polimorf):

template<typename T>
T duplaz(T const& x)
{
    return x + x;
}

std::cout << duplaz(5) << std::endl;
std::cout << duplaz(std::string("hello")) << std::endl;

Ennek ellenére, ha valaki csak általánosságban beszél polimorfizmusról, valószínűleg az öröklésre gondol, szakcikkekben is szeretik összekeverni.

2. Tic-tac-toe, kiindulás

Írjunk tic-tac-toe játékot! Legyen paraméterezhető a játékosok stratégiája!

Milyen osztályok fognak kelleni a modellezéshez?

Megoldás
  • mező: legyen most char, X, O, szóköz karakterekkel, szorgalmi átírni enumra.
  • tábla: legyen most egy typedef char tabla[3][3]; ami nem szerencsés, szorgalmi osztályt írni a helyére.
  • játék: egyelőre nincs rá szükség, elég lesz egy jatszma_lebonyolit függvény
  • játékos: az ősosztály. Viselkedése: "eléd rakok egy táblát, megmondom, melyikkel játszol te, és mondd meg, mi lesz a következő lépésed!"
  • játékos leszármazottai
class1

3. Tic-tac-toe, játékosok

Írjuk meg a Jatekos osztályt! Milyen interfésze van egy játékosnak?

Megoldás

Fontos, hogy a játékos nem módosíthatja a táblát, hogy ne tudjon "csalni", ezért const&-et kap rá. A visszatérési értékében jelzi, hogy hova akar lépni, és majd a lebonyolító lép helyette! Az egyes leszármazottaknak kell megadnia a stratégiát, ezért virtuális a függvény. Bárki meghívhatja rajta, ezért public. Itt nem tudjuk implementálni, ezért tisztán virtuális, aminek a nyelvi eleme az = 0.

class Jatekos
{
public:
    virtual Pont lep(tabla const& t, char sajat) = 0;
};

Ehhez kelleni fog egy Pont osztály is. Publikus adattagok jók lesznek, és két konstruktor, hogy egyszerűbb legyen használni:

struct Pont
{
    int x;
    int y;
    Pont() : x(0), y(0) {}
    Pont(int x, int y) : x(x), y(y) {}
};

4. Tic-tac-toe, stratégiák

Írj két játékos osztályt:

  • egy nagyon egyszerű gép, ami mindig random helyre lép (már ahova szabad)
  • egy játékos, ami a felhasználótól kérdezi meg, hogy hova szeretne lépni (de előtte kirajzolja neki a táblát),
Megoldás
class2
class RandomJatekos : public Jatekos
{
public:
    Pont lep(tabla const& t, char sajat);
};

class HumanJatekos : public Jatekos
{
public:
    Pont lep(tabla const& t, char sajat);
};

bool lepes_valid(tabla const& t, Pont p)
{
    return p.x >= 0 && p.y >= 0 && p.x < 3 && p.y < 3 && t[p.x][p.y] == ' ');
}

Pont RandomJatekos::lep(tabla const &t, char sajat)
{
    Pont lepes(rand() % 3, rand() % 3);
    while(!lepes_valid(t, lepes))
        lepes = Pont(rand() % 3, rand() % 3);
    return lepes;
}

Pont HumanJatekos::lep(tabla const &t, char sajat)
{
    for (int i = 0; i < 3; ++i)
        for (int j = 0; j < 3; ++j)
            std::cout << t[i][j];

    Pont p;
    do {
        std::cin >> p.x >> p.y;
    } while(!lepes_valid(t, p));

    return p;
}

5. Tic-tac-toe, visszajátszás

Írjunk olyan játékost, ami egy fájlból olvasva egyetlen játékos lépéseit visszajátssza! Ha a játszma lépéseit a két játékosra két külön fájlba tesszük, két ilyet egymás ellen játszatva látni fogjuk a meccset, mint ahogy a sakkban szokás.

Megoldás

A fájl olvasását végezzük std::ifstream-mel, aminek egy példánya ugyanúgy használható, mint az std::cin, mert leszármazottja az std::istream-nek.

Vegyük észre, hogy a lépés függvény tartalma pont ugyanaz lesz, mint a humán játékosnál! Csak az std::cin-t kell lecserélni a fájlra. Jó lenne egy közös ősosztályba tenni, és csak azt paraméterezhetővé tenni, hogy honnan olvassuk a bemenetet. Csináljunk hát egy StreamJatekos osztályt, ami a közös viselkedést tartalmazza, azaz a lep függvény definícióját. A forrásstreamet viszont a leszármazottak adhatják meg, tehát az implementációban cseréljük ki az std::cin-t forras()-ra!

class3
class StreamJatekos : public Jatekos
{
public:
    Pont lep(tabla const& t, char sajat);
    virtual std::istream& forras() = 0;
};

Ezután a HumanJatekos egyszerűsödik:

class HumanJatekos : public StreamJatekos
{
public:
    std::istream& forras()
    {
        return std::cin;
    }
};

A ReplayJatekos-nak adattagja a fájl, amiből olvas, konstruktorában megnyitja fájlnévből, a többiről pedig maga gondoskodik.

class ReplayJatekos : public StreamJatekos
{
    std::ifstream fajl;
public:
    ReplayJatekos(std::string const& fajlnev)
        : fajl(fajlnev.c_str())
    {}

    std::istream& forras()
    {
        return fajl;
    }
};

6. Tic-tac-toe, játszma lebonyolítása

Írjunk függvényt, ami két játékost egymás ellen játszat! Egészítsük ki főprogrammal, amit akár ki is próbálhatunk!

Megoldás
bool jatek_vege(tabla const&)
{
    return false;
}

char ki_nyert(tabla const&)
{
    return ' ';
}

void jatszmat_lebonyolit(Jatekos& iksz, Jatekos& kor)
{
    tabla t;
    for (int i = 0; i < 3; ++i)
        for (int j = 0; j < 3; ++j)
            t[i][j] = ' ';

    char ki_jon = 'X';
    while (!jatek_vege(t))
    {
        Jatekos& aktualis = ki_jon == 'X' ? iksz : kor;
        Pont p = aktualis.lep(t, ki_jon);
        if (!lepes_valid(t, p))
            throw std::logic_error("Rossz lépés");

        t[p.x][p.y] = ki_jon;
        ki_jon = ki_jon == 'X' ? 'O' : 'X';
    }
    char nyertes = ki_nyert(t);
    if (nyertes != ' ')
        std::cout << "A nyertes: " << nyertes << std::endl;
    else
        std::cout << "Döntetlen" << std::endl;
}

int main()
{
    srand(time(NULL));
    RandomJatekos r_j;
    HumanJatekos h;
    jatszmat_lebonyolit(r_j, h);
}

A játékosoknak maguknak kell arra figyelniük, hogy ne lépjenek rosszat, mert ha mégis, akkor baj van. A játék menni fog, csak a leállás nem.

tictactoe.cpp letöltése

7. Tic-tac-toe, további feladatok

  • Csinálj osztályt a tabla-nak! Lehessen akármekkora méretű, és legyen benne két
  • Csinálj enumot a játékosokra: KOR, IKSZ, URES, SEMMI. Az értékek legyenek karakterkódok!
  • Írd meg a jatek_vege és a ki_nyert függvényeket!