Kihagyás

2. gyakorlat

Bíró

A félév során a Bírót használjuk a ZH -hoz, míg a Bíró2-n órai és gyakorló feladatok lesznek

Be kell regisztrálni a Bíróra és Bíró2-re is

Ismétlés

  • beolvasás, kíratás (cin, cout)
  • karakterláncok tárolása string típusban

Objektumorientált programozás

Az objektumorientált programozás alapvetéseit már mindenki elsajátította a Programozás I. tantárgy keretein belül, illetve az előadáson ismét lesz (azonban ha valakinek mégis szükséges lenne az ott tanultak felelevenítése, az látogasson el ide).

Osztályok létrehozása

Osztályok létrehozása a class kulcsszóval történik, az osztályunkat záró kapcsos zárójel után pontosvesszővel is le kell zárni (gyakori hiba, hogy ez lemarad).

1
2
3
class Kurzus {
  // ...
};

Természetesen az osztályhoz tartozó adattagokat is deklarálhatjuk itt, a már ismert típusokkal.

1
2
3
4
5
6
7
8
9
#include <iostream>
#include <string>

using namespace std;

class Kurzus {
  string nev, kod;
  unsigned max;
};

Láthatóságok

A láthatóságok itt is léteznek, jelentésük azonos a Java esetében tanultakkal, azonban csak 3 darab van belőlük: public, private, protected, nincs package private láthatóság (hiszen package-ek sincsenek C++-ban). A kulcsszavakat itt már nem kell minden egyes adattag, függvény elé kiírni, elegendő egyszer megadni a kívánt láthatóságot, majd kettőspont után felsorolni az adott láthatósághoz tartozó tagokat. Ha nem adunk meg semmit, akkor az osztályok esetében automatikusan private láthatóságot jelent. A fentivel megegyező kód:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#include <iostream>
#include <string>

using namespace std;

class Kurzus {
private:
  string nev, kod;
  unsigned max;
};

Egy láthatósági módosító nem csak egyszer szerepelhet a kódban, azonban általában a fejlesztés végén összevonásra kerülnek az azonos láthatóságú deklarációk, hogy egy láthatóság egyszer szerepeljen (de ez nem kötelező). Az alábbi kód érvényes C++ kód:

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

using namespace std;

class Kurzus {
private:
  string nev;
public:
  string kod;
private:
  unsigned max;
protected:
  int terem_ferohely;
private:
  string terem_neve;
private:
  string egyeb_informacio;
};

Getter/Setter

Ahogy tanultuk korábban, a private láthatóság csak az osztályon belüli elérést tesz lehetővé, a protected az osztályon kívül a leszármazott osztályok számára is biztosítja az elérést, a public pedig teljesen nyilvános elérést jelent.

Az alapelvek itt is hasonlók, mint Javaban, vagyis jó volna, ha az objektumunk fontos adatait csak úgy a külvilágból nem módosítaná senki, csak és kizárólag ellenőrzött módon, az objektum által történjen változás az objektum állapotában (például, hogy egy Ember objektum ne lehessen -23891 éves).

Ehhez lehetőségünk van itt is getter és setter függvényeket írni. Tartva az elnevezési konvenciókat a getter függvény a kurzuson get_adattag, a setter pedig set_adattag néven lesz használva (de előfordulhat az is, hogy a Javaból tanult camel-case változatot használják, azaz getAdattag és setAdattag az elnevezés).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#include <iostream>
#include <string>

using namespace std;

class Kurzus {
  string nev;
public:
  string get_nev() {
    return nev;
  }

  void set_nev(string uj_nev) {
    nev = uj_nev;
  }
};

A getter és setter metódusok mellett természetesen tetszőleges további metódust is készíthetünk az osztályunkhoz.

Konstruktor

Természetesen itt is készíthetünk konstruktorokat az osztályainkhoz, amik az osztály példányosításakor fognak lefutni. Itt szokás inicializálni az adattagjainkat, valamint itt szokás dinamikusan memóriát foglalni. A konstruktor lehet paraméter nélküli (default), vagy pedig rendelkezhet tetszőleges számú paraméterrel. Azonos paraméter számú konstruktorból is lehet több (a típusok sorrendjének azonban mindenképpen különböznie kell). Ezeket azért tehetjük meg, mert C++ esetében a függvényeket és metódusokat többek közt (erről később) a nevük, paraméter számuk és paraméter típusuk határozza meg. Természetesen teljesen azonos "kinézetű" (adott scope-ban lévő, azonos nevű, azonos paraméterezésű) függvényből itt sem lehet több.

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

using namespace std;

class Kurzus {
  string nev, kod;
  unsigned max;
public:
 // parameteres
  Kurzus(string n, string k, int m) {
    nev = n;
    kod = k;
    max = m;
  }
  // default -y nem kell parameter
  Kurzus() {
    nev = "nincs";
    kod = "nincs";
    max = 1;
  }
  //...
};

This

A this segítségével az objektum adattagjait, metódusait érhetjük el, hivatkozhatunk az objektumra. Ez egy pointer magára az objektumra (fontos, hogy nem az osztályhoz, hanem az objektumhoz tartozik). Mivel pointer, a -> operátort használjuk. Természetesen használható a (*this).adattag forma is, azonban a szebb kód érdekében ezt kevésbé használjuk. A this használata hasznos lehet, ha egy paraméter formális neve megegyezik egy adattagunk nevével (további használatáról később lesz szó).

Példányosítás

Az elkészült osztályunkat ezt követően példányosíthatjuk, amelynek több módja is van (ezen az órán a legegyszerűbbel fogunk megismerkedni). Az objektum létrehozásához szükségünk van a típusdeklarációra, majd pedig a változónév megadására. Ezt követően pedig zárójelben jönnek a paraméterek. Ha paraméter nélküli konstruktort szeretnénk meghívni, akkor pedig TILOS a változónév után üres zárójelpárt tenni (nem változó definíció lesz, a jelenség neve "Most vexing parse", bővebben a Wikipédián olvashatsz róla).

 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
#include <iostream>
#include <string>

using namespace std;

class Kurzus {
  string nev, kod;
  unsigned max;
public:
  Kurzus(string n, string k, int m) {
    nev = n;
    kod = k;
    max = m;
  }

  Kurzus() 
    : nev("default-name-placeholder")
    ,kod("default-kod-placeholder")
    ,max(0) {}

  string get_nev() {
    return nev;
  }

  string get_kod() {
    return kod;
  }

  unsigned get_max() {
    return max;
  }
};

int main() {
  Kurzus k("Programozas II.", "IB302G-1", 25);
  cout << k.get_nev() << " (" << k.get_kod() << ", max: " << k.get_max() << ")" << endl;

  Kurzus default_initialized;
  // default konstruktor. NEM KELL ()!! ROSSZ: Kurzus default_initialized(); !!
  cout << default_initialized.get_nev()
  << " (" << default_initialized.get_kod()
  << ", max: " << default_initialized.get_max() << ")" << endl;
}

Paraméter átadási módok

Lehetséges módok

  • érték szerint: az objektum másolódik, eredeti objektum nem változik. Költséges.
  • pointer szerint: objektum címe, dereferencia. Eredeti objektum módosítása. Gyors, magas hibalehetőség.
  • referencia: eredeti objektumon dolgozik. Nincs dereferencia. Biztonságos használat.

Az elkészült Kurzus osztályt paraméterként átadva, annak egy metódusát szeretnénk meghívni, ami módosítja az objektum állapotát (vagyis valamelyik adattag értékét megváltoztatja.) Ebben az esetben pointer szerint kell átadni a Kurzust, hogy a módosítások az eredeti objektumon történjenek.

 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
48
49
50
51
52
53
54
55
#include <iostream>
#include <string>

using namespace std;

class Kurzus {
  string nev, kod;
  unsigned max;
public:
  Kurzus(string nev, string kod) {
    this->nev = nev;
    this->kod = kod;
    this->max = 25;
  }

  Kurzus(string nev, string kod, int max) {
    this->nev = nev;
    this->kod = kod;
    this->max = max;
  }

  string get_nev() {
    return nev;
  }

  string get_kod() {
    return kod;
  }

  unsigned get_max() {
    return max;
  }

  // hibakezelessel nem foglalkozunk
  void letszam_noveles(int valtozas) {
    max += valtozas;
  }
};

// szandekosan nem a Kurzus osztaly metodusa
void letszam_valtoztatas(Kurzus* k) {
  int valtozas;
  cin >> valtozas;
  k->letszam_noveles(valtozas);
}

int main() {
  string nev, kod;
  int max;
  cin >> nev >> kod >> max;
  Kurzus k(nev, kod, max);
  cout << k.get_nev() << " (" << k.get_kod() << ", max: " << k.get_max() << ")" << endl;
  letszam_valtoztatas(&k);
  cout << k.get_nev() << " (" << k.get_kod() << ", max: " << k.get_max() << ")" << endl;
}

Ahogy látszik is (és ahogy C-ben már tanultuk), az objektumnak a címét kell képezni, pointer szerint kell átvenni az objektumot, és utána úgy is kell használni (dereferencia, -> operátor)

Referencia

Mivel a pointerek használata nehézkes lehet, bonyolíthatja a kódot, hibaforrás is lehet, valamint használatuk esetén további ellenőrzéseket is be kellhet vezetni, ezért sokan nem szívesen használják azokat. Azért, hogy hasonló módon (eredeti objektum) tudjuk átadni a változót és a pointer dereferenciával se kelljen foglalkozni, használhatunk referenciákat. A referencia az eredeti objektumnak felel meg, csak másik scope-ban és más néven. Az eredeti objektumon dolgozik, nem kell dereferálni, nem lehet NULL / nullptr értéke és a jelölt objektum nem állítható át (ellentétben azzal, amikor egy pointert másik címre állítunk).

 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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// cim szerinti atadas referencia segitsegevel
#include <iostream>
#include <string>

using namespace std;

class Kurzus {
  string nev, kod;
  unsigned max;
public:
  Kurzus(string nev, string kod) {
    this->nev = nev;
    this->kod = kod;
    this->max = 25;
  }

  Kurzus(string nev, string kod, int max) {
    this->nev = nev;
    this->kod = kod;
    this->max = max;
  }

  string get_nev() {
    return nev;
  }

  string get_kod() {
    return kod;
  }

  unsigned get_max() {
    return max;
  }

  // hibakezelessel nem foglalkozunk
  void letszam_noveles(int valtozas) {
    max += valtozas;
  }
};

// a parameter egy pointer
void letszam_valtoztatas(Kurzus* k) {
  int valtozas;
  cin >> valtozas;
  k->letszam_noveles(valtozas);
}

// a parameter egy referencia, azaz meg tudja valtoztatni a hivas helyen levo objektumot
// irasmodban azonban egyszerubb/olvashatobb
void letszam_valtoztatas(Kurzus& k) {
  int valtozas;
  cin >> valtozas;
  k.letszam_noveles(valtozas);
}

int main() {
  string nev, kod;
  int max;
  cin >> nev >> kod >> max;
  Kurzus kurzus_valtozo(nev, kod, max);
  cout << kurzus_valtozo.get_nev() << " (" << kurzus_valtozo.get_kod() << ", max: " << kurzus_valtozo.get_max() << ")" << endl;
  letszam_valtoztatas(&kurzus_valtozo);
  cout << kurzus_valtozo.get_nev() << " (" << kurzus_valtozo.get_kod() << ", max: " << kurzus_valtozo.get_max() << ")" << endl;
  letszam_valtoztatas(kurzus_valtozo);
  cout << kurzus_valtozo.get_nev() << " (" << kurzus_valtozo.get_kod() << ", max: " << kurzus_valtozo.get_max() << ")" << endl;
}

referencia alias

Mint az ábrán látható, az átadott kurzusból nem egy másolat készül, hanem az eredetin hajtjuk végre a módosítást, tehát egy Kurzust adunk át. Eddig ezt pointerekkel tudtuk elérni. Nagyon fontos, hogy a Kurzus& és Kurzus* külön típus. Mivel a 2 külön típus, a függvény overloadolható referencia és pointer típussal is, azonban egyszerű Kurzus típussal nem (amennyiben készítettünk egy overloadot referencia típussal)!

Megengedett:

1
2
void foo(Kurzus){...}
void foo(Kurzus*){...}

Megengedett:

1
2
void foo(Kurzus&){...}
void foo(Kurzus*){...}

Nem megengedett, mivel nem egyértelmű (hiszen mindkét függvénynek csak önmagában egy Kurzus objektumot adnánk át, így a fordító nem tudná, hogy melyik függvényt is szeretnénk meghívni, míg a pointer/referencia között átadáskor is teszünk különbséget):

1
2
void foo(Kurzus){...}
void foo(Kurzus&){...}

Hiba: error: call of overloaded ‘foo(Kurzus&)’ is ambiguous

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; // nem fordul

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++; // nem fordul
*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.

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) {}
};

Friend tagok

A friend tagok tekinthetők az OOP megszegésének, azonban sokszor szükség van használatukra. Ha friend tagot adunk az osztályunkhoz, akkor a barátként megjelölt elem hozzáfér a privát láthatóságú elemekhez is; ezért is tekinthető az OOP megtörésének.

Fontos, hogy a friend elem NEM az osztály része csupán egy kitüntetett különálló elem.

 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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#include <iostream>

using namespace std;

class Kurzus {
    string nev, kod;
    unsigned max = 30;
public:
    Kurzus(string nev, string kod) : nev(nev), kod(kod) {}

    string get_nev() {
        return nev;
    }

    friend class Tanulmanyi_osztaly;
};

class Tanulmanyi_osztaly {
    string vezeto = "Elsa";
public:
    /**
     * Egy kurzus atnevezese az uj nevre ha megfelelo a jogosultsag.
    */
    bool kurzus_atnevezes(Kurzus &k, string uj_nev, string jovahagyo) {
        bool eredmeny = false;

        if (jovahagyo == vezeto) {
            k.nev = uj_nev; //mivel a Tanulmanyi_osztaly friend class a Kurzusnak, igy a privat adattagok elerhetok.
            eredmeny = true;
        }

        return eredmeny;
    }

    // Egy egesz osztaly helyett egy fuggveny is lehet friend, ebben az esetben a fejlecet kell felvenni friendkent
    // A fuggveny hozzaferhet az osztaly privat lathatosagu reszeihez is.
    friend void vezeto_valtas(Tanulmanyi_osztaly &, string, string);
};

/**
* A kapott TO vezetojenek megvaltoztatasa, ha a jelszo helyes.
*/
void vezeto_valtas(Tanulmanyi_osztaly &to, string uj_vezeto, string titkos_jelszo) {
    if ("secret" == titkos_jelszo) {
        to.vezeto = uj_vezeto;
    }
}

int main() {
    Kurzus k("TypoName", "IBG-5659-05");
    Tanulmanyi_osztaly to;

    cout << k.get_nev() << endl;
    cout << to.kurzus_atnevezes(k, "Helyes nev", "Elsa") << endl;
    cout << k.get_nev() << endl;

    //k.nev = "Helyes nev"; forditasi hibat okozna, mert private ebben a kontextusban

    vezeto_valtas(to, "Anna", "secret");

    cout << to.kurzus_atnevezes(k, "Helyes nev", "Elsa") << endl;
    // most mar a megvaltozott vezeto miatt ez sikertelen.
}

A friend kulcsszóról bővebben.

Otthoni gyakorló feladatok

  1. Készíts egy SIMKartya osztályt.
    • A SIM kártyának meg kell adni a számát létrehozáskor, amit később nem lehet megváltoztatni.
    • Bárki lekérdezheti a számát.
    • Megoldás
  2. Készíts egy Mobiltelefon osztályt.
    • A Mobiltelefonba be lehessen rakni egy SIM kártyát.
      • Milyen kapcsolat van a mobiltelefon és a kártya között?
    • Ki lehessen cserélni a kártyát.
    • Ki lehessen venni a kártyát.
    • Megoldás
  3. Legyen egy függvény, amelyik kicseréli a telefonban a kártyát.
    • A paraméterek legyenek érték szerint átadva. Nézzük meg, hogy a hívás helyén valóban kicserélődött-e a telefonban a kártya.
    • Megoldás
  4. Valósítsuk meg a cserét pointer segítségével.
    • Mi történik, ha a kártyát nem pointerként adjuk át?
    • Megoldás
  5. Mutassuk be ugyanezt a cserét referencia használattal.
    • Mi változott? Vizsgáljuk meg a használat és a hívás helyét is.
    • Megoldás
  6. Készítsd egy Szolgaltato osztályt.
    • Egy már létező SIM kártyának csak a szolgáltató változtathatja meg a számát.
    • Megoldás

Otthoni gyakorló feladatok II

  1. Készíts egy Immunrendszer osztályt, melynek két egész szám adattagja van, melyek privát láthatóságúak. Az egyik adattag neve vedelem, a másik tamadas. Lehessen az osztályt default konstruktorral inicializálni, amikor a vedelem és a tamadas értéke is 10. Lehessen úgy is inicializálni az osztályt, hogy mindkét adattag értéke paraméterből jön.

    Részmegoldás
  2. Készíts egy Virus osztályt, melynek két privát adattagja van: nev (string) és tamadas (int). Legyen olyan konstruktora, amivel mindkét adattagot lehet inicializálni és olyan is, amivel csak a nevet, a tamadas érték pedig ilyenkor legyen 10.

    Részmegoldás
  3. Valósítsd meg a vírus immunrendszer elleni támadását! Legyen az Immunrendszer osztálynak egy tamadast_elszenved publikus metódusa, melynek egyetlen paramétere egy Vírus objektum. Ha a vírus támadas értéke nagyobb, mint az immunrendszeré, akkor csökkenjen az immunrendszer védelem értéke eggyel. A védelem érték ne csökkenjen nulla alá. Ha eléri a nullát vagy az alá akarnánk csökkenteni, legyen kiírva a standard outputra: "a [vírus neve] gyozott."

    Részmegoldás

Utolsó frissítés: 2022-09-15 09:40:31