Kihagyás

4. gyakorlat

Ismétlés

  • osztály deklaráció: class Name {...};
  • this
  • pointer és referencia használata
  • friend

const

A const módosítót már ismerjük Programozás alapjairól, de hogy biztosan mindenkinek eszébe jusson, a gyakorlati anyagban is megmutatjuk.

A const segítségével konstans ,,dolgokat'' hozhatunk létre, a legegyszerűbb változata egy konstans változó.

1
const int szam = 42;

A pointerek esetében kicsit más a helyzet, ugyanis ott több helyre is kerülhet a const. Az első esetben a módosító a pointer típusa elé kerül, ebben az esetben a mutatott érték lesz az, ami konstans (létrehozunk egy mutatót, ami egy konstans int értékre mutat). A pointer viszont nem konstans, így azt megváltoztathatjuk például a ++ operátor segítségével.

1
2
3
4
5
6
7
int szam = 42;
const int* szamraMutato = &szam;
szamraMutato++;
//*szamraMutato = 20;

cout << "szam cime: " << &szam << " szamraMutato cime: " << szamraMutato << endl;
cout << "szam erteke: " << szam << " szamraMutato erteke: " << *szamraMutato << endl;

A második esetben a const a változó típusa után kerül. Ebben az esetben a pointer az, ami konstans lesz, a mutatott érték nem lesz konstans.

1
2
3
4
5
6
7
int szam = 42;
int* const szamraMutato = &szam;
//szamraMutato++;
*szamraMutato = 20;

cout << "szam cime: " << &szam << " szamraMutato cime: " << szamraMutato << endl;
cout << "szam erteke: " << szam << " szamraMutato erteke: " << *szamraMutato << endl;

const metódus

Getter függvényeknél azt biztosan elmondhatjuk, hogy a célja csak annyi, hogy egy adott értéket le tudjunk kérdezni az objektumunkról, de semmilyen módosítást sem csinálnak. C++-ban lehetőség van arra, hogy garantáljuk, hogy az adott metódus tényleg ne módosítsa az adott objektumot. Ennek következtében

  • a fejlesztőnek segítséget ad, mert fordítási időben kiderül, ha a metódus mégis megváltoztatná az objektum állapotát;
  • az osztályt használó biztos lehet abban, hogy nem történik módosítás;
  • és végül a metódus használható lesz const objektumokra is.

Ezt a const kulcsszóval tehetjük meg a függvény neve után kiírva:

1
string get_nev() const { return name; }

Egy kerekebb példa:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class Hallgato {
  string nev, kod;
public:
  Hallgato(const string& nev, const string& kod) {
    this->nev = nev;
    this->kod = kod;
  }

  string get_nev() const {
    return nev;
  }

  string get_kod() {  //ez most szandekosan nem const
    return kod;
  }
};

Ekkor a Hallgato-ból készült objektumoknál biztosak lehetünk, hogy pl. a get_nev() metódust meghívva nem módosul az objektum, különben le sem fordult volna. Ez azért hasznos, mert tudjuk, hogy a függvényhívás után ugyan olyan lesz az objektum. Ennél hasznosabb következmény, ha pl. a Hallgato-ból létrehozunk egy konstans objektumot, akkor arra is meghívhatjuk a függvényt.

1
2
3
4
5
int main() {
  const Hallgato h("Nev", "Kod");
  string nev = h.get_nev(); //helyes, mert a konstans objektum nem módosul
  string kod = h.get_kod(); //helytelen, mert nem biztos, hogy nem módosul a konstans
}

Getter függvények esetében nemcsak azt kell biztosítanunk, hogy a getter nem módosítja az objektumot (const módosító), hanem azt is, hogy amit visszaadunk azon keresztül nem módosítható az objektumunk. Ennek egy triviális megoldása, ha a visszaadandó értékről egy másolatot készítünk s azt adjuk vissza. Ekkor az eredeti érték igazából nem kerül ki az objektumon kívülre, azonban ennek az a hátránya, hogy másolatokat kell csinálnunk.

A fenti példában a

1
string get_nev() const { return name; }

getter esetében valójában a name string-ből létrejött egy másolat s azt adtuk vissza. Ha ez az objektum hatalmas, akkor a másolás igen költséges művelet is lehet, de mindenképp fölösleges. Ehelyett használhatunk referenciát is. Ekkor nem lesz másolás, de elérheti más is a visszaadott elemet (hiszen a referencia valami elérése más néven).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
#include <string>

using namespace std;

class Kurzus {
  string nev, kod;
  unsigned felvette = 0;
public:
  Kurzus(const string& nev, const string& kod) {
    this->nev = nev;
    this->kod = kod;
  }

  string& get_nev() {
    return nev;
  }
};

int main() {
  Kurzus k("Programozas II.", "IB302G-1");
  k.get_nev() = "asd"; // valid
  cout << k.get_nev() << endl;
}

Ennek kivédésére az értéket konstanssá tehetjük, így másolás nélkül adunk vissza értéket és biztosítjuk, hogy a visszaadott elemen keresztül nem módosulhat az objektum.

Ezek alapján a Hallgato osztályt a következőképpen írhatjuk meg:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class Hallgato {
  string nev, kod;
public:
  Hallgato(const string& nev, const string& kod) {
    this->nev = nev;
    this->kod = kod;
  }

  const string& get_nev() const {
    return nev;
  }

  const string& get_kod() const {  //ez most szandekosan const
    return kod;
  }
};

Ekkor már biztos, hogy:

  • konstans objektumoknak is hívható a getter (vagy bármely const) metódusa
  • másolás nélkül (hatékonyan) adunk ki értéket
  • a kiadott érték nem használható az objektum módosítására

(Ha konstans metódusnál referenciával térnénk vissza konstans típus nélkül, fordítási hibát kapunk.)

Const és mutable kulcsszavak értelmezése

Sok esetben van szükségünk arra, hogy biztosítani tudjunk egy metódust konstans objektumokra is, így const method-ot kell definiálnunk, azonban mégis szeretnénk valamilyen módosítást végezni. Tegyük fel azt az esetet, hogy a Kurzusnak van információs adattagja. Ezt le tudjuk kérdezni, természetesen ezzel nem módosul a Kurzus, így const method-al tesszük ezt. Ha szeretnénk egy számlálót léptetni, hogy hányan kérdezték le a metódust pl. statisztikai okokból, akkor

  • vagy egy globális változóba számlálunk (ez azonban csúnya, és több objektum esetében hibás megoldás)
  • vagy elhagyjuk a const method jelzőt, adattagban így tudjuk tárolni a lekérdezéseket. -> A const objektumokat ezzel kizárjuk.
  • vagy mégis módosítjuk a const method-dal az egyik adattagot.

A megfelelő megoldás a harmadik pont. A mutable kulcsszót akkor alkalmazzuk, ha tudjuk, hogy egy adattagot olyan metódusban akarunk módosítani, mely const megjelölésű. Ezzel az információt szolgáltató const method nem ad fordítási hibát az adattag módosításakor, hiszen megadtuk, hogy az az adattag komolyabban nem módosítja az objektumot, még mindig ugyan annak a konstans értéknek vehetjük.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class Kurzus {
  string information;
  mutable unsigned int lekerdezesek = 0; // a mutable kulcsszó nélkül hibát kapnánk a get_information methodban.
...

  const string & get_information() const {
    ++lekerdezesek;
    return information;
  }
};

const referencia használata paraméterben

A példában a Kurzus paramétereit konstans string referenciákkal várjuk. Mivel konstansok, ezért biztos, hogy nem tudjuk módosítani azokat. Azonban a referenciákat arra tudtuk használni, hogy a cím szerinti átadás helyett egy szebb formában tudjuk ugyan azt az elemet másik helyen módosítani. Ezzel ezt elveszítjük, tehát mi a haszna az érték szerinti átadással szemben?

Egyszerű pl. int típusnál ez nem olyan jelentős, azonban nézzük meg, mi a helyzet ha egy nagyobb méretű objektumot adunk át. Tegyük fel, hogy a Kurzus neve és kódja is több MB-os string. Ha érték szerint adnánk át:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class Kurzus {
 public:
  Kurzus(string nev, string kod) {
    this->nev = nev;
    this->kod = kod;
  }
...
};

int main() {
  string nev = "A nagyon sok karakter...";
  string kod = "Megint nagyon sok karakter...";
  Kurzus(nev, kod);
}

Ekkor van egy objektum a névnek és egy objektum a kódnak. Mivel érték szerint adunk át, nem az objektumok címei kerülnek átadásra, és nem is referencia, hanem az érték adódik át (jelen esetben a szöveg). Ez azt jelenti, hogy az a sok-sok MB-nyi adat lemásolódik és egy-egy új objektumban létrejön. Ez fölösleges memóriahasználatot és fölösleges adatmásolást jelent.

Ezt kiküszöbölhetjük, ha konstans referenciát használunk:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class Kurzus {
 public:
  Kurzus(const string& nev, const string& kod) {
    this->nev = nev;
    this->kod = kod;
  }
...
};

int main() {
  string nev = "A nagyon sok karakter...";
  string kod = "Megint nagyon sok karakter...";
  Kurzus(nev, kod);
}

A kódnak s névnek most is van egy-egy objektuma, azonban a függvény meghívásakor az értékük nem lemásolódik, hanem referencia szerint átadódik, azaz máshonnan utalhatunk rá. Az értéküket tudjuk így is (másolás nélkül is). Azt a veszélyt pedig, hogy módosítaná valaki az adatot, lekezeltük a const módosító használatával.

Tehát kifejezetten nagy objektumok esetében, de általánosságban is, jobb konstans referenciát átadni mint csupán értéket, melyről másolat készül ideiglenesen.

Objektumorientált programozás

Default konstruktor szerepe

A konstruktor feladata az objektum inicializálása, általában a paraméterben kapott értékeket felhasználásával. Azonban a default konstruktor úgy inicializálja, hogy ,,nem kap'' paramétereket, amivel inicializálhatná az objektumot. Így kérdéses, hogy miért akarunk olyan lehetőséget biztosítani, hogy a konstruktor valamilyen ,,alapértelmezett'' értékkel inicializálja az objektumot (pl. a Hallgato-t üres névvel és kóddal).

A Hallgato osztályt pl. névvel és kóddal látjuk el, de készítünk egy konstruktort, mely egyiket sem állítja be.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class Hallgato {
  string nev, kod;
public:
  Hallgato() {
  }

  Hallgato(const string& nev, const string& kod) {
      this->nev = nev;
      this->kod = kod;
  }

  const string& get_nev() const {
    return nev;
  }

  const string& get_kod() const {
    return kod;
  }
};

Ez azért fontos, mert ha kiakarjuk bővíteni a Kurzus osztályt úgy, hogy a feljelentkezett Hallgato-kat egy tömbben tárolja, fordítási hibát kapnánk.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const unsigned KURZUS_LIMIT = 10;

class Kurzus {
  string nev, kod;
  unsigned felvette = 0;
  Hallgato hallgatok[KURZUS_LIMIT]; //inicializált elemek, de hogyan?
public:
  Kurzus(const string& nev, const string& kod) {
    this->nev = nev;
    this->kod = kod;
  }
  ...
};

Mikor egy tömböt létrehozunk pl. a Hallgato osztályból, akkor a tömb elemek inicializálódnak. Nézzük meg, hogyan is iniciálizálunk egy objektumot! Természetesen a konstruktor által, azonban kérdéses, hogy milyen értékeket kellene megadnia a rendszernek. Természetesen nem tudja, hogy minek kellene ott szerepelnie, ezért hibát jelez. Azonban ha van default konstruktor, akkor tud olyan konstruktort hívni, melynek nem kell paramétert átadnia, így minden elem inicializálása a megadott módon lezajlik. Nulla paraméteres konstruktort akkor is hívhatunk, ha egy paraméteres konstruktornak minden paraméterének van default értéke. Ez könnyedén ambiguous hívást okozhat!

A probléma abból származott, hogy a tömb elemeinek inicializálásakor nem tudta a rendszer kitalálni, hogy milyen értékekkel akartunk inicializálni. Ha ezt a problémát megszüntetjük, vagyis megadjuk a várt értékeket, akkor default konstruktor nélkül is létrehozhatunk tömböt. Ekkor azonban minden elemet egyesével inicializálnunk kell.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Hallgato {
  string nev, kod;
public:
  Hallgato(const string& nev, const string& kod) {
    this->nev = nev;
    this->kod = kod;
  }

  const string& get_nev() const {
    return nev;
  }

  const string& get_kod() const {
    return kod;
  }
};

class Kurzus {
  string nev, kod;
  unsigned felvette = 0;
  Hallgato hallgatok[2] = {{"Kreatív Név", "KNAA.SZE"}, {"IV Béla", "IBAA.SZE"}};
public:
  Kurzus(const string& nev, const string& kod) : nev(nev), kod(kod) {
  }
  // ...
};

Mivel tömböt a {} jelek között inicializálhatunk s egy-egy objektumot is inicializálhatunk a {} jelek között a fönti példában egyesével megadtuk, hogy mik legyenek a 0. és az 1. indexű elemek konstruktor hívásakor átadott paraméterek. Ez nem tanácsos, főleg ha több elemről beszélünk.

Konstruktor inicializáló lista

C++-ban a konstruktorban való adatinicializálásnak létezik egy preferált módja, ez pedig az inicializáló lista használata (initializer list). Ezt érdemes megtanulni, mert ez a leggyakrabban használt megoldás a konstruktoroknál (és bizonyos adattagokat más módon nem is lehet inicializálni).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class Kurzus  {
  string nev, kod;
  unsigned max;
public:
  Kurzus(string nev, string kod) : nev(nev), kod(kod), max(25) {
  }

  Kurzus(string nev, string kod, int max) : nev(nev), kod(kod), max(max) {
  }
};

A konstruktor fejléce után kettőspontot teszünk, majd pedig felsoroljuk az adattagokat és mögöttük zárójelben a kezdeti értéküket, amik például a konstruktor paraméterei is lehetnek. Itt nem történik névütközés, mert az inicializáló listában mindig csak az osztály adattagjai lehetnek, a paraméter neve pedig elfedi a konstruktor scope-jában az adattag nevét, így a nev(nev) például teljesen értelmes, hiszen a külső név csak adattag lehet, míg a zárójelben lévő érték pedig jelen esetben a paramétert jelenti. A fenti példa, amikor az adattagok és a paraméterek nevei különbözők:

1
2
3
4
5
6
7
class Kurzus  {
  string nev, kod;
  unsigned max;
public:
  Kurzus(string nev_p, string kod_p, int max_p) : nev(nev_p), kod(kod_p), max(max_p) {
  }
};

Fontos megjegyezni, hogy minden esetben a deklaráció sorrendjében kerülnek végrehajtásra az inicializálások, nem pedig a konstruktor kódjában megadott sorrendben, majd ezt követően fut le a konstruktor törzse. Az alábbi kód hibás!!!

1
2
3
4
5
6
7
class Koordinata {
  int x, y;
public:
  Koordinata(int s) : y(s), x(y) {   // ennek a jelentese
                                     //   x = y; de az y itt meg nem definialt
  }                                  //   y = s;
}

Nézzünk példát arra, hogy mi történik akkor, ha inicializáló lista nélkül szeretnénk a következő elemeket beállítani:

  • const adattag
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Koordinata {
  const int x,y;    // ezeknek az adattagoknak nem valtozhat az erteke.

 public:
  Koordinata(int x, int y) : y(y) { // az y hiaba konstans inicializalo listaban kaphat erteket,
                                    // de utana mar nem valtoztathato meg.
      this->x = x;                  // ebben az esetben a konstans x-et felülirtuk,
                                    // amit nem tehetunk meg const elemmel
                                    // forditasi hibat kapunk
  }
};

Helyesen:

1
2
3
4
5
6
class Koordinata {
  const int x,y;    // ezeknek az adattagoknak nem valtozhat az erteke.

 public:
  Koordinata(int x, int y) : x(x), y(y) {}  // megfelelo sorrendben inicializalt const adattagok
};
  • referencia
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Kor {
  const unsigned r; // azt mar tudjuk, hogy a const elemet init listaban kell beallitanunk
  Koordinata& kozeppont;

 public:
  Kor(Koordinata& kord, unsigned r) : r(r) {
      kozeppont = kord; // ebben az esetben a kozeppont hasznalhato lenne,
      // de a referencia nem kapott erteket. Azt tudjuk, hogy a referencianak mindig kell erteket adni
      //Ez a kod forditasi hibat okoz.
      // Referencia csakis az inicializalo listaban allithato be.
  }
};

Helyesen:

1
2
3
4
5
6
7
class Kor {
  const unsigned r; // azt mar tudjuk, hogy a const elemet init listaban kell beallitanunk
  Koordinata& kozeppont;

 public:
  Kor(Koordinata& kord, unsigned r) : r(r), kozeppont(kord) {}
};

Delegating konstruktor

Megeshet, hogy két konstruktor működése elég hasonló, egyik kicsit több/másabb mint egy másik. Szerencsére itt is lehetőségünk van a kódmásolás elkerülésére, és meghívhatjuk az egyik konstruktorból a másikat. Ez hasonló lesz az inicializáló listához, annyi különbséggel, hogy az adattagok inicializálása helyett egy másik konstruktort is meghívunk (de ilyenkor már nem inicializálhatunk adattagot a konstruktor inicializáló listában). Ehhez annyit kell tennünk, hogy kiírjuk az osztály nevét, majd zárójelek között a paramétereket (mintha egy egyszerű metódushívás lenne). Megjegyzés: az itt látott megoldással lehet majd az örökölt osztály konstruktorát is meghívni.

1
2
3
4
5
public:
  Kurzus(string nev, string kod, int max) : nev(nev), kod(kod), max(max) {
  }
  Kurzus(string nev, string kod) : Kurzus(nev, kod, 25) { // a masik konstruktor hivasa
  }

default metódus

Már volt szó a default konstruktorról. Ha nem írjuk ki, van default konstruktor, azonban ha már bármilyen másik konstruktort írunk, alapból nem lesz generált default konstruktor, ekkor meg kell írnunk magunknak. Ha már egy üres konstruktort írunk, kérdéses lehet, hogy az default akar lenni, vagy elfelejtettük megírni. Ha a default kulcsszót használjuk, rövidebb és olvashatóbb kódot kapunk.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Kurzus {
  unsigned max_letszam;

 public:
  Kurzus(unsigned max_letszam) : max_letszam(max_letszam) {}    // emiatt nincsen default konstruktor
  /*
  Kurzus(){}    // nem tudni, mit is akartunk
  */
  Kurzus() = default;   // Ez a kifejezes sokkal beszedesebb es egy default konstruktor szerepet teljesiti
//....
};

deleted metódus

Sokszor kell bizonyos dolgokat megtiltanunk a felhasználónak. C++ esetében ezt egy osztályra a deleted metódussal tehetjük meg.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class Kurzus {
  unsigned max_letszam;
 public:
  Kurzus() = default;
  Kurzus(unsigned max_letszam) : max_letszam(max_letszam){}
  // Kurzus(int max_letszam) : max_letszam(max_letszam){}  
  // Ha az int parameters konstruktor letezne, akkor akar egy negativ szam is atadhato lenne,
  // es mivel van konverzio unsigned <--> int, igy a rendszer ertelmezni fogja azt anelkul,
  // hogy megirnank az int verziot.
  // Ha ezt le akarjuk tiltani, ki kell torolnunk a metódust
  Kurzus(int max_letszam) = delete; // Igy mar ha intet adnak at, akkor nem az unsignedre konvertalodik,
  // hanem erzekeli ezt a konstruktort s latja, hogy le van tiltva, forditasi hibat okoz.
};

int main() {
    int i = -5;
    unsigned u = 3;

    Kurzus k(u);
    Kurzus k2(i);
}

operátor kiterjesztés

Az eddig ismert alap típusokra léteznek operátorok. Egy egész típusú változó operátora lehet pl. összeadás (+), kivonás (-), értékadás (=), inkrementálás (++). Ezeknek a jól megszokott hatásuk van a változó értékére. Az általunk megvalósított osztályokra eddig nem tudtuk megmondani, hogy mi történjen pl. összeadás esetén. Erre C++-ban van lehetőség, azaz megtudjuk mondani, hogy mi történjen ha az osztályunkhoz az adott operátorral összekötünk egy másik típust. Ezt operátor kiterjesztésnek hívják.

Vegyük alapul, hogyan működik pl. az összeadás operátor az egész számokra. Mit is jelent, ha leírjuk az a+b kifejezést:

1
a+b => össze kell adni: a, b => összead(a,b) => +(a,b) => operator+(a,b)

Fontos észre venni, hogy az a és b értéke nem változik meg, és a kifejezés eredményének tárolására egy új ,,objektum'' jön létre (3+2 esetén 5, ami szintén egy egész), tehát van visszatérési értéke. Egész számok esetén ez egy egész szám lesz, tehát ha kiegészítjük a fentit a visszatérési értékkel, akkor a következő módon fog kinézni:

1
int operator+(a,b) => int operator+(int, int)

A saját osztályunkra nézve hasonlóan néz ki, csak meg kell határoznunk, hogy mit mivel szeretnénk összeadni és ennek mi lesz az eredménye. Ha vesszük a Kurzus osztályt és hozzáadunk egy Hallgato osztályt, akkor egy bővített kurzust kapunk, ami szintén egy Kurzus. Tehát a két paraméter: Kurzus, Hallgato (ami valójában konstans referencia lesz). A visszatérési értéke típus Kurzus.

1
Kurzus operator+(Kurzus, Hallgato)

Mivel az operátor viselkedése szorosan kötődik a Kurzushoz (így fogalmaztuk meg) OOP szerint az osztályon végzett műveletet rakjuk be az osztályba magába. Ezt a metódust megírhatjuk mint a Kurzus osztály tagfüggvénye. Ha tagfüggvény, akkor az első paraméter (Kurzus) adott is, hiszen ha meghívjuk egy objektum operator+ függvényét, az első paraméter maga az objektum, így a paraméterlistából azt el is hagyhatjuk.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#define KURZUS_LIMIT 10

class Kurzus {
  string nev, kod;
  unsigned felvette = 0;
  Hallgato hallgatok[KURZUS_LIMIT];
public:
  Kurzus(const string& nev, const string& kod) : nev(nev), kod(kod) {
  }

  Kurzus operator+(const Hallgato& h) const { // const, mert a Kurzus nem valtozik meg
    Kurzus res = *this;
    if ( res.felvette == KURZUS_LIMIT ) {
      cout << "A kurzus megtelt. Nem lehet felvenni." << endl;
    } else {
      res.hallgatok[res.felvette++] = h;
    }

    return *res; //Kurzussal térünk vissza
  }
};

Ez hozzáadja a Kurzus-hoz a kapott hallgatót, ha még elfér. Hasonló szemantikája van, mint az egész számoknál.

A használata a következő:

1
2
3
4
5
6
7
8
int main() {

  Kurzus kurzus("Programozas II.", "IB302G-1");
  Hallgato hallgato("Kreatív Név", "KNAA.SZE");

  kurzus = kurzus + hallgato;
  // a levezetésünk szerint valójában: kurzus = kurzus.operator+(hallgato);
}

Az egészeknél ha egy változóhoz hozzá akarunk adni egy értéket az összeadást a következőképpen kell használnunk:

1
a = a + 5

Ennek rövidített változata:

1
a += 5

Ezt szintén megtehetjük a saját osztályunkra ha az operator+=-t definiáljuk felül.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const unsigned KURZUS_LIMIT = 10;

class Kurzus {
  string nev, kod;
  unsigned felvette = 0;
  Hallgato hallgatok[KURZUS_LIMIT];
public:
  Kurzus(const string& nev, const string& kod) : nev(nev), kod(kod) {
  }

  Kurzus& operator+=(const Hallgato& h) { // NEM konstans, mert ehhez adjuk hozza
    if ( felvette == KURZUS_LIMIT ) {
      cout << "A kurzus megtelt. Nem lehet felvenni." << endl;
    } else {
      hallgatok[felvette++] = h;
    }

    return *this;
  }
};

int main() {

  Kurzus k("Programozas II", "IB302G-1");
  Hallgato h("Kreatív Név", "KNAA.SZE");

  k += h1;
}

Ez a megvalósítás meglehetősen hasonló, azonban fontos kiemelni néhány jelentős különbséget! Az első, hogy a visszatérési érték nem Kurzus, hanem Kurzus& és nem hozunk létre egy újabb Kurzus típusú objektumot. Ennek oka a következő:

Mikor az operator+-t használtuk, akkor az operandusok változatlanok maradnak, és létrejött egy új objektum, mely az összeget tárolta el és teljesen független a másik két operandustól. Ezt az alábbi ábra vizualizálja:

operator+

Ha ezt akartuk használni arra, hogy az eredeti változónk módosuljon, akkor a += értékadó operátort kell használni. Ekkor az eredeti változó objektumát felülírtuk az eredmény objektummal.

operator+

operator+

Az operator+= használatakor már magában az operátorban benne van az értékadás, tehát az eredeti objektum módosul. Ezért is kell referenciával visszatérni, hiszen ugyan arról az objektumról beszélhetünk.

A hallgató hozzáadása is hasonlóan működik:

operator+

Fontos, hogy az eredeti Kurzus módosult, a Hallgato bekerült, de megmaradt. Ezért lehetett a paraméterben const &.

operator+

Mivel az operátorokat függvényeknek vettük, ezért a meghatározásoknál nemcsak az operátor neve, hanem a paraméter típusa is meghatározza. Megtehetjük, hogy csinálunk egy operator+=-t csak most Kurzus paraméterrel.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#define KURZUS_LIMIT 10

class Kurzus {
  string nev, kod;
  unsigned felvette = 0;
  Hallgato hallgatok[KURZUS_LIMIT];
public:
  Kurzus(const string& nev, const string& kod) : nev(nev), kod(kod) {
  }

  Kurzus& operator+=(const Hallgato& h) {
    if ( felvette == KURZUS_LIMIT ) {
      cout << "A kurzus megtelt. Nem lehet felvenni." << endl;
    }
    else {
      hallgatok[felvette++] = h;
    }

    return *this;
  }

  Kurzus& operator+=(const Kurzus& k) {
    if ( k.felvette + this->felvette == KURZUS_LIMIT ) {
      cout << "A két kurzus nem vonható össze. Nincs elég hely" << endl;
    }
    else {
      for ( unsigned i = 0; i < k.felvette; ++i ) {
        hallgatok[felvette++] = k.hallgatok[i];
      }
    }

    return *this;
  }
};

int main() {

  Kurzus k("Programozas II", "IB302G-1");
  Kurzus k2("Programozas II 2", "IB302G-13");

  Hallgato h1("Kreatív Név", "KNAA.SZE");
  Hallgato h2("Kreatívabb Név", "KBAA.SZE");

  k += h1;
  k2 += h2;
  k += k2;
}

Ez hasonlóképpen történik, mint Hallgató esetén csak a paraméter és a viselkedés más.

operator+

Természetesen, az objektumhoz adott paraméter most sem módosul, így az lehet const &.

operator+

indexer operátor

Az indexer operátor különleges operátornak is tekinthető. Szemantikáját általában tömbökhöz kötjük, elemek eléréséhez (de tetszőlege értelmezést adhatunk ennek is). Ehhez az operator[](int) metódust kell kiterjeszteni. Ha egy tömbben elakarunk érni egy elemet, azzal több dolgot is tehetünk. Egyszerűen lekérdezhetjük (módosítás nélkül):

1
cout << int_array[2];

Vagy az értéket módosíthatjuk:

1
int_array[2] = 55;

Mivel az első esetben csak az értékre vagyunk kíváncsiak, nem kell tudnunk módosítani azt. A második esetben már a visszaadott értéket szeretnénk módosítani, vagyis igazából az int_array objektumban lévő 2. index-en lévő elemet szeretnénk felülírni.

Azt már vettük, hogy ha egy függvény módosíthatja vagy sem az objektumot azt a const kulcsszóval jelezhetjük. Az első esetben nem módosíthatjuk tehát használjuk a const kulcsszót, a második esetben pedig nem.

Természetesen nem elég a metódust konstanssá tenni, hiszen ha a visszatérési értékben kiszivárog egy "kiskapu", akkor az objektumot elronthatjuk. Ennek kivédése érdekében a visszatérési értéket is máshogy kell megadnunk. Erről már szó volt a const-ról szóló részben. Az indexer operátorok kiterjesztése a Kurzus osztályra (annak éri meg, hiszen annak van tömbhöz köthető jellege):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#define KURZUS_LIMIT 10

class Kurzus {
  string nev, kod;
  unsigned felvette = 0;
  Hallgato hallgatok[KURZUS_LIMIT];
public:
  Kurzus(const string& nev, const string& kod) : nev(nev), kod(kod) {
  }

  //eset 1
  Hallgato& operator[](unsigned i) {
    if (i < felvette)
      return hallgatok[i];
    // valodi hibakezeles szukseges, ami majd kesobb jon
    cout << "HIBA: nincs " << i << " darab hallgato a kurzushoz rendelve" << endl;
    return hallgatok[0]; // nem szep, de ...
  }

  //eset 2
  const Hallgato& operator[](unsigned i) const {
    if (i < felvette)
      return hallgatok[i];
    // valodi hibakezeles szukseges, ami majd kesobb jon
    cout << "HIBA (const): nincs " << i<< " darab hallgato a kurzushoz rendelve" << endl;
    return hallgatok[0]; // nem szep, de ...
  }
};

Az első esetben egy referenciát adunk vissza, tehát ahova visszaadjuk, ott elérik az adott elemet pl. felül is írhatják az értékét. Mivel ezzel módosulhat a Kurzus, ez a függvény nem is const. A második esetben szintén referenciát adunk vissza, azonban ez most const & tehát nem írható felül az értéke. Ez biztosítja, hogy akármit is teszünk, a Kurzus nem fog megváltozni, így ez a függvény lehet const.

++ / -- operátorok

A ++a, a++, --a és a-- kifejezések mindenki számára ismerősek. A különbség a pre és post verzió közt annyi pl. intekre nézve, hogy a pre verzióban a visszatérési érték (kifejezés értéke) már a növelt értéket adja, míg a post esetben megváltozik a változó, de a kiértékeléskor még az előző értéket kapjuk.

Mivel ezek is operátorok, ezeket is felül lehet definiálni. Azonban a pre s post verziók közt leírásban nem sok különbséget találunk, így valahogyan meg kell különböztetni azokat. Az operator kiterjesztése során a post verzióhoz egy plusz (nem használt) paramétert adunk, csupán a megkülönböztetés miatt.

Működésük miatt a visszatérési értékük sem teljesen egyezik meg. Nézzünk egy példát rá!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>
class Kurzus {
  unsigned letszam = 0;

 public:
  Kurzus operator++(int) {  // a nem hasznalt parameter mutatja, hogy post incrementalast hajtunk vegre
    std::cout << "Udv a Kurzus post ++ operatorabol" << std::endl;
    Kurzus tmp = *this; // Mivel a post verzio eseteben a regi allapotot szoktuk visszaadni,
    // arrol kell egy mentest keszitenunk.
    letszam++;
    return tmp; // mivel egy lokalis valtozot adunk vissza, ami megszunik a metodus vegen
    // nem adhatunk vissza pl. referenciat.
  }

  Kurzus& operator++() {
      std::cout << "Udv a Kurzus pre ++ operatorabol" << std::endl;
      ++letszam;
      return *this; // mivel a pre verzioban a valtoztatott erteket adjuk vissza,
      // nem kell masolatot kesziteni es visszaadhatjuk az objektumot magat, hiszen
      // a mar frissitett adat kell nekunk.
  }
};

int main() {
    Kurzus k;
    ++k;
    k++;
}

Hasznos a kettőt együtt megvalósítani s ekkor az egyiket a másik felhasználásával, hiszen ekkor hiba javításakor csak egy helyen kell javítanunk.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <iostream>
class Kurzus {
  unsigned letszam = 0;

 public:
  Kurzus operator++(int) {  // a nem hasznalt parameter mutatja, hogy post incrementalast hajtunk vegre
    Kurzus tmp = *this; // Mivel a post verzio eseteben a regi allapotot szoktuk visszaadni,
    // arrol kell egy mentest keszitenunk.
    /* Az alabbi resz ugyan az mint a pre verzioban. Hivjuk azt
    * Igaz, most csak egy sor, de ez akar tobb, osszetettebb kod is lehet.
    letszam++;
    */
    operator++();   // Ezzel a pre verziot meghivtuk, a noveles megtortent
    // az elozo ertek ugyan ugy lementesre kerult, a funkcionalitas megmaradt.
    return tmp; // mivel egy lokalis valtozot adunk vissza, ami megszunik a metodus vegen
    // nem adhatunk vissza pl. referenciat.
  }

  Kurzus& operator++() {
      ++letszam;
      return *this; // mivel a pre verzioban a valtoztatott erteket adjuk vissza,
      // nem kell masolatot kesziteni es visszaadhatjuk az objektumot magat, hiszen
      // a mar frissitett adat kell nekunk.
  }
};

int main() {
    Kurzus k;
    ++k;
    k++;
}

Feladatok

  1. Gyakorló feladatsor

    • Készíts egy Meme osztály. Két string típusú adattagja van, láthatóságuk legyen privát. Nevük: szoveg, kep. Legyen hozzá egy két paraméteres publikus konstruktor, amivel beállítjuk az adattagokat. Írj gettereket is!

      Részmegoldás
    • Készíts egy void print_meme() metódust, melynek paramétere egy ^^konstans^^ Meme referencia. Egy sorban kiírja a meme szövegét majd egy rákövetkező sorban a képet is. Hogyan kell módosítani az előbb megírt gettereket?

      Részmegoldás
    • Készíts egy MemeGyujtemeny osztályt. Ez maximum tíz Memét tud letárolni egy tömbben. Mivel kell kiegészíteni a Meme osztályt, hogy működjön a tömb létrehozás?

      Részmegoldás
    • Definiáld felül a & és a &= operátorokat a MemeGyujtemeny osztályban úgy, hogy a baloldali operandus egy MemeGyujtemeny legyen, a jobboldali pedig egy Meme. A MemeGyujtemeny tömbjében az első üres helyre kerüljön be az új meme. Ennek megfelelően módosítsuk úgy a MemeGyujtemeny osztályt, hogy le tudja tárolni, aktuálisan hány meme-t tárol!

      • MemeGyujtemeny & Meme művelet esetén ne legyen módosítva egyik operandus sem, a visszatérési érték pedig egy lemásolt, bővített MemeGyujtemeny objektum legyen.
      • MemeGyujtemeny &= Meme művelet esetén a bal oldali MemeGyujtemeny objektum módosuljon, ahhoz adjuk hozzá az új elemet. MemeGyujtemeny referencia legyen visszaadva.
      • Ha nincs már hely a gyűjteményben, ne legyen hozzáadás, csak történjen meg a megfelelő típusok visszadaása (módosítás nélküli másolat illetve módosítás nélküli bal oldali operandus)
      Részmegoldás
    • Definiáld felül az indexer [] operátort a MemeGyujtemeny osztályban úgy, hogy adja vissza az i-edik letárolt memét. Legyen konstans és nem konstans verziója is. Túlindexelés esetén legyen kiírva a "Hiba" szöveg a cout-ra és a 0-ás indexű meme legyen visszaadva.

      • Hozz létre az osztályokon kívül egy void print_gyujtemeny függvényt, amely konstans MemeGyujtemeny paramétert vár és egy for ciklusban kiíratja a print_meme segítségével az összes letárolt memét. Gettert csak az aktuális meme szám lekérdezéséhez írj!
      Részmegoldás
    Teljes megoldás

Utolsó frissítés: 2020-10-26 09:25:33