Kihagyás

C++ osztályok

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 C++-ban (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. Az elnevezéseket tekintve jelen jegyzetben a Java CamelCase elnevezési konvenciót alkalmazzuk (ha tehetjük), azaz osztályok neve nagy betűvel kezdődik, metódusok és adattagok neve kicsivel, és a szó határokon a szavakat egybe, de nagy kezdőbetűvel írjuk. C++ esetében nem íratlan szabály ezen konvenció alkalmazása. Gyakran találkozhatunk olyan kódokkal, amelyek az úgynevezett snake_case konvenciót követik, azaz a space-eket az elnevezésekben aláhúzás jellel helyettesítik.

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 teremFerohely;
private:
  string mTeremNeve;
private:
  string mEgyebInformacio;
};

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.

 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 getNev() {
    return nev;
  }

  void setNev(string ujNev) {
    nev = ujNev;
  }
};

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

Objektumok inicializálása

Az objektumok inicializálásáért a konstruktor felel. Az inicializálás mellett itt szokás dinamikusan memóriát foglalni, ha valamelyik adattagnak erre van szüksége. 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. Figyeljünk arra, hogy primitív adattagok esetében NEM inicializálódik 0-ra az adattag értéke, ha az adattagnak nincs egyéb inicializálása!

 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 - nem kell parameter
  Kurzus() {
    nev = "nincs";
    kod = "nincs";
    max = 1;
  }
  //...
};

this

A this segítségével hivatkozhatunk az objektumra, általa el tudjuk érni az adattagjait, metódusait. Mivel ez egy pointer magára az objektumra (fontos, hogy nem az osztályhoz, hanem az objektumhoz tartozik), így a -> operátort használjuk, ha ezen keresztül hivatkozni szeretnénk adattagra, metódusra. 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ó).

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 így hibás!!! Az ilyen esetek elkerülése érdekében érdemes a kódot eleve úgy írni, hogy az inicializáló listában a sorrend megegyezzen a deklaráció sorrendjével.

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

Default adattag értékek

C++11 óta itt is lehetőségünk van alapértelmezett értéket adni az adattagjainknak deklarációkor. Így megússzuk azt, hogy minden konstruktorban be kelljen állítani az adattagoknak értéket (ez főleg akkor jöhet jól, ha ugyanazt az értéket szeretnénk mindenhol beállítani, így ha mégis más alapértelmezett értéket szeretnénk beállítani, nem kell az összes konstruktort módosítani). Persze ettől még megtehetjük, ilyenkor a konstruktorban lévő érték felülírja az alapértelmezett értéket. Ez is ismerős lehet Javaból.

1
2
3
4
class Kurzus {
  string nev, kod;
  unsigned max = 25;
}

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 konstruktor szerepe

A konstruktor feladata az objektum inicializálása, általában a paraméterben kapott értékek 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& getNev() const {
    return nev;
  }

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

Ez azért fontos, mert ha ki akarjuk 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. Ha nem lenne default konstrukora a Hallgato osztálynak, akkor egyszerű módon nem tudnánk belőle tömböt létrehozni (csak ha expliciten inicializálunk minden elemet)

 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 megadni 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.

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& getNev() const {
    return nev;
  }

  const string& getKod() 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.

Metódusok, függvények

Default paraméter értékek

Az úgynevezett "boilerplate" (felesleges) kódsorok elkerülése érdekében, lehetőségünk van default paraméter értékeket megadni a függvényeinknek. Ez akkor tud jól jönni, amikor nagyon hasonló függvényeket szeretnénk írni, amik csak a paraméterlistájukban különböznek (pl. ha nem adjuk meg a max. létszámot, akkor azt 25-nek veszi). Azért, hogy ne kelljen egy adott függvényt (a benne lévő potenciális hibával) annyiszor lemásolni, lehetőségünk van megadni a paramétereknek alapértelmezett értéket, amit a függvény fejlécében a paraméter neve után tehetjük meg egyenlőség jellel.

Ezzel kapcsolatban a legfontosabb szabály, hogy mindig csak és kizárólag az utolsó valamennyi paraméternek lehet alapértelmezett értéke. Ez persze nem zárja ki, hogy az utolsó összesnek legyen, de olyat nem lehet, hogy csak a 2. és 5. paraméternek adott alapértelmezett értéket. A default paraméterekkel kapcsolatos összes szabály itt érthető el.

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

int main() {
  Kurzus k1("Programozas II.", "IB302G-1", 23);
  Kurzus k2("Programozas II.", "IB302G-1"); // itt is ugyanaz a konstruktor hivodik
}

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); // forditasi hiba?
}

Objektumok példányosítása

Ha megvan az osztályunk, annak adattagjai, illetve konstruktorai, akkor akár példányosíthatjuk is az osztályt. Azaz hozhatunk létre belőle objektumokat. Ennek több módja is van (egyelőre 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
44
#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 = "nincs";
    kod = "nincs";
    max = 1;
  }

  string getNev() {
    return nev;
  }

  string getKod() {
    return kod;
  }

  unsigned getMax() {
    return max;
  }
};

int main() {
  Kurzus k("Programozas II.", "IB302G-1", 25);
  cout << k.getNev() << " (" << k.getKod() << ", max: " << k.getMax() << ")" << endl;

  Kurzus default_initialized;
  // default konstruktor. NEM KELL ()!! ROSSZ: Kurzus default_initialized(); !!
  cout << default_initialized.getNev()
  << " (" << default_initialized.getKod()
  << ", max: " << default_initialized.getMax() << ")" << endl;
}

Kimenet

Programozas II. (IB302G-1, max: 25)
nincs (nincs, max: 1)

Objektumok megszüntetése

A destruktor hasonló a már jól ismert konstruktorhoz, azonban ez az objektum életének végéhez kötődik. Míg a konstruktor az objektum létrejöttekor fut le, a destruktor akkor, amikor az objektum megszűnik létezni. Ez semmilyen extra hívást nem jelent, teljesen automatikus. Az eddig létrehozott objektumainkat lokálisan hoztuk létre. Amikor a blokk megszűnik, amelyben létrejöttek, akkor meghívódik az objektumok destruktora, amely adott esetben képes arra például, hogy az objektum által foglalt memóriát felszabadítsa, az objektum által használt erőforrásokat visszaadja.

Fontos, hogy nem összekeverendő a Java finalize metódusával!

Jelölése is mutatja, hogy mennyire hasonlít a konstruktorhoz, hiszen ez is az osztály nevével kell megegyezzen, csupán a tilde (~) jel előzi meg azt. A konstruktorokkal ellentétben a destruktornak nem lehetnek paraméterei, és csak egy darab lehet belőle egy osztályon belül.

Példa destruktorra és az automatikus lefutásra:

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

using namespace std;

class Magic {
  int luckyNumber;
public:
  Magic(int num) : luckyNumber(num) {
  }

  //Destruktor:
  ~Magic() {
    cout << "Your lucky number was: " << luckyNumber << endl;
  }

  int getLuckyNumber() const {
    return luckyNumber;
  }
};

int main() {
  Magic m(5);
  cout << m.getLuckyNumber() << endl;
  cout << "Ez az utolso kiiratas" << endl;
}

Kimenet

5
Ez az utolso kiiratas
Your lucky number was: 5

A fenti példában látszólag az utolsó kiíratás után semmi sem szerepel, mégis a programot lefuttatva az utolsó kimenet a

Your lucky number was: 5

Ez azért történik, mert az m nevű objektum a main végén szűnik meg, így még a destruktorában lévő kódrészlet (kiíratás) lefut.

Az ilyen működés nagyon hasznos lehet, mivel gyakran használunk olyan objektumokat, melyek valamilyen erőforrást (dinamikus memória, hálózati kapcsolat, fájl, stb.) használnak. Ezeknek a felszabadításáról, bezárásáról gondoskodnunk kell. C-ben jól ismert példa lehet egy fájl olvasása:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#include <stdio.h>
#include <stdlib.h>

int main() {
  FILE *fptr;
  fptr = fopen("file","r");
  if (fptr == NULL) {
    printf("Error!");
    return 1;
  }
  //olvassunk adatokat a fájlból
  fclose(fptr);
  return 0;
}

Azt leszámítva, hogy a hibakezelés másként működik, fontos kiemelni, hogy a fájlt a használat végén be kell zárjuk, vissza kell adjuk a rendszernek. Igaz, kisebb példánál nem jelentős, azonban ha az utolsó használat után még hosszú ideig futhat a program, nem mindegy, hogy a fájlt elengedjük vagy továbbra is foglaljuk erőforrásként. Ez a felelősség a programozóra hárul, hogy ne felejtse el meghívni az fclose metódust.

Ha egy fájlt kezelő osztályt képzelünk el C++-ban, akkor annak a destruktorában megtehetjük, hogy minden műveletet helyesen lezárunk a fájlon és azt visszaadjuk a rendszernek azzal, hogy bezárjuk. Ekkor a programozónak nem kell ezzel törődnie máshol, biztosan nem marad el, és csakis akkor történik meg, mikor a fájl már nem használható, hiszen az objektum megszűnésekor fut le a destruktor.

Paraméter átadás

A paraméter átadásnak két nagyobb csoportját különítjük el:

  • bemenő módú vagy érték szerinti paraméterátadás: az objektum másolódik, eredeti objektum nem változik. Költséges.
  • be/ki menő módú vagy cím szerinti paraméterátadás: a hívás során módosulhat ez eredeti objektum. Ezt megoldhatjuk referencia segítségével, amikor a hívott metódus az eredeti objektumon dolgozik. (Pointerekkel is megoldhatjuk, de annak használata nagyobb körültekintést igényel, és kevésbé biztonságos, ezért most ezzel nem foglalkozunk.)

A referencia az eredeti objektumnak felel meg, csak adott esetben másik scope-ban és más néven. Pont azért, mert a referencia egy másik helyen definiált változó "átnevezése", ezért definiálásakor rögtön meg kell adni, hogy mire referál. Ezért is kellett a konstruktorban mindenképp inicializálni a referencia adattagot.

 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
// 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 getNev() {
    return nev;
  }

  string getKod() {
    return kod;
  }

  unsigned getMax() {
    return max;
  }

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

// a parameter egy referencia, azaz meg tudja valtoztatni a hivas helyen levo objektumot
// irasmodban azonban egyszerubb/olvashatobb
void letszamValtoztatas(Kurzus& k) {
  int valtozas = 4;
  k.letszamNoveles(valtozas);
}

int main() {
  string nev="Programozas II.", kod="IB302G-1";
  int max=25;
  Kurzus kurzusValtozo(nev, kod, max);
  cout << kurzusValtozo.getNev() << " (" << kurzusValtozo.getKod() << ", max: " << kurzusValtozo.getMax() << ")" << endl;
  letszamValtoztatas(kurzusValtozo);
  cout << kurzusValtozo.getNev() << " (" << kurzusValtozo.getKod() << ", max: " << kurzusValtozo.getMax() << ")" << endl;
}

Kimenet

Programozas II. (IB302G-1, max: 25)
Programozas II. (IB302G-1, max: 29)

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. Figyeljünk arra, hogy nem működik az, ha két metódus között csak annyi a különbség, hogy valamely paraméter vagy érték, vagy referencia szerint van átadva. Azaz nem megengedett a következő eset, 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):

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

Hiba:

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

const

A const segítségével konstans ,,dolgokat'' hozhatunk létre, a legegyszerűbb változata ennek egy konstans változó. Minden olyan esetben, amikor azt szeretnénk, hogy egy változó értéke biztosan ne változzon, használjuk!

1
const int szam = 42;

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 getNev() 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 getNev() const {
    return nev;
  }

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

Ekkor a Hallgato-ból készült objektumoknál biztosak lehetünk, hogy pl. a getNev() 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.getNev(); //helyes, mert a konstans objektum nem módosul
  string kod = h.getKod(); //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 getNev() 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& getNev() {
    return nev;
  }
};

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

Persze ekkor a visszaadott értéken keresztül mégiscsak módosítható lenne az objektum egy adattagja. Ennek kivédésére a visszaadott értéket is 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& getNev() const {
    return nev;
  }

  const string& getKod() 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 apró módosítást végezni. Tegyük fel azt az esetet, hogy a Kurzusnak van egy információs adattagja. Ezt le tudjuk kérdezni, természetesen ezzel nem módosul a Kurzus, így const methoddal 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. Ekkor azonban a metódust már nem alkalmazhatjuk konstans objektumokra.
  • vagy marad a const a metódus, de ha az adattagot ellátjuk a mutable módosítóval, akkor a metódus engedi ennek változtatását.

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 getInformation methodban.
...

  const string & getInformation() 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. De akkor mi értelme a cím szerinti paraméterátadásnak itt? Ha az a cél, hogy ne módosuljon a paraméter, miért nem használunk érték szerinti paraméter átadást? Primitív típusoknál, vagy kisebb osztályoknál nyilván nem probléma érték szerint átadni a paramétert. Ez a paraméter bemásolódik a stackre, és onnan elérjük, használjuk. Nagyobb méretű objektumok esetében (pl. nagyon hosszú sztring, összetett adattípusok, stb.) azonban a stackre másolás is időigényes lehet, nem mellesleg könnyen "el is használhatjuk" így a stack területé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.

Friend tagok

A láthatóságok megadásakor láttuk, hogy igazából elég szigorú a C++ ebben a tekintetben, ha egy osztály nem leszármazottja egy másik osztálynak, akkor tulajdonképpen annak csak a public adattagjait, metódusait érheti el. Azonban C++ esetében is lehet olyan, hogy vannak olyan osztályok, akikről tudjuk, hogy "megbízhatóak" az osztályra nézve, és akik talán nagyobb kontrollt kaphatnak egy-egy másik osztály felett, vagy legalábbis az osztály bizonyos metódusai felett. Ilyenkor használhatjuk a friend módosítót. Ha egy osztály egy másik osztály friendje, akkor ez az osztály látja a másik osztály private tagjait is. Nemcsak osztály kaphat friend jelzőt, hanem tetszőleges függvény is, ilyenkor természetesen a függvényen belül érhetjük el az osztály rejtett tagjait.

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 getNev() {
    return nev;
  }

  friend class TanulmanyiOsztaly;
};

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

    if (jovahagyo == vezeto) {
      k.nev = ujNev; //mivel a TanulmanyiOsztaly 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 vezetoValtas(TanulmanyiOsztaly &, const string&, const string&);
};

/**
* A kapott TO vezetojenek megvaltoztatasa, ha a jelszo helyes.
*/
void vezetoValtas(TanulmanyiOsztaly &to, const string& ujVezeto, const string& titkosJelszo) {
    if ("secret" == titkosJelszo) {
        to.vezeto = ujVezeto;
    }
}

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

  cout << k.getNev() << endl;
  cout << to.kurzusAtnevezes(k, "Helyes nev", "Elsa") << endl;
  cout << k.getNev() << endl;

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

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

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

Kimenet

TypoName
1
Helyes nev
0

A friend kulcsszóról bővebben.


Utolsó frissítés: 2024-08-02
Létrehozva: 2020-09-09