III. fejezet - Objektum-orientált programozás C++ nyelven

Tartalom
III.1. Bevezetés az objektum-orientált világba
III.1.1. Alapelemek
III.1.2. Alapvető elvek
III.1.2.1. Bezárás, adatrejtés (encapsulation , data hiding)
III.1.2.2. Öröklés (inheritance)
III.1.2.3. Absztrakció (abstraction)
III.1.2.4. Polimorfizmus (polymorphism)
III.1.3. Objektum-orientált C++ programpélda
III.2. Osztályok és objektumok
III.2.1. A struktúráktól az osztályokig
III.2.1.1. Egy kis ismétlés
III.2.1.2. Adatok és műveletek egybeépítése
III.2.1.3. Adatrejtés
III.2.1.4. Konstruktorok
III.2.1.4.1. Taginicializáló lista alkalmazása
III.2.1.4.2. Az objektumok explicit inicializálása
III.2.1.5. Destruktor
III.2.1.6. Az osztály objektumai, a this mutató
III.2.2. Az osztályokról bővebben
III.2.2.1. Statikus osztálytagok
III.2.2.2. Az osztályok kialakításának lehetőségei
III.2.2.2.1. Implicit inline tagfüggvények alkalmazása
III.2.2.2.2. Osztálystruktúra a C++/CLI alkalmazásokban
III.2.2.2.3. A tagfüggvények tárolása külön modulban
III.2.2.3. Barát függvények és osztályok
III.2.2.4. Mi szerepelhet még az osztályokban?
III.2.2.4.1. Objektumok konstans adattagjai
III.2.2.4.2. Hivatkozás típusú adattagok
III.2.2.4.3. Adattag objektumok
III.2.2.5. Osztálytagokra mutató pointerek
III.2.3. Operátorok túlterhelése (operator overloading)
III.2.3.1. Operátorfüggvények készítése
III.2.3.2. Típus-átalakító operátorfüggvények használata
III.2.3.3. Az osztályok bővítése input/output műveletekkel
III.3. Öröklés (származtatás)
III.3.1. Osztályok származtatása
III.3.2. Az alaposztály(ok) inicializálása
III.3.3. Az osztálytagok elérése öröklés esetén
III.3.3.1. Az öröklött tagok elérése
III.3.3.2. A friend viszony az öröklés során
III.3.4. Virtuális alaposztályok a többszörös öröklésnél
III.3.5. Öröklés és/vagy kompozíció?
III.3.5.1. Újrahasznosítás kompozícióval
III.3.5.2. Újrahasznosítás nyilvános örökléssel
III.4. Polimorfizmus (többalakúság)
III.4.1. Virtuális tagfüggvények
III.4.2. A virtuális függvények felüldefiniálása (redefine)
III.4.3. A korai és a késői kötés
III.4.3.1. A statikus korai kötés
III.4.3.2. A dinamikus késői kötés
III.4.3.3. A virtuális metódustábla
III.4.4. Virtuális destruktorok
III.4.5. Absztrakt osztályok és interfészek
III.4.6. Futás közbeni típusinformációk osztályok esetén
III.5. Osztálysablonok (class templates)
III.5.1. Osztálysablon lépésről-lépésre
III.5.2. Általánosított osztály definiálása
III.5.3. Példányosítás és specializáció
III.5.4. Érték- és alapértelmezett sablonparaméterek
III.5.5. Az osztálysablon „barátai” és statikus adattagjai
III.5.6. A C++ nyelv szabványos sablonkönyvtára (STL)
III.5.6.1. Az STL felépítése
III.5.6.2. Az STL és C++ tömbök
III.5.6.3. Az STL tárolók használata
III.5.6.4. Az STL tároló adaptációk alkalmazása

Az objektum-orientált programozás (OOP) olyan modern programozási módszertan (paradigma), amely a program egészét egyedi jellemzőkkel rendelkező, önmagukban is működőképes, zárt programegységek (objektumok) halmazából építi fel. Az objektum-orientált programozás a klasszikus strukturált programozásnál jóval hatékonyabb megoldást nyújt a legtöbb problémára, és az absztrakt műveletvégző objektumok kialakításának és újrafelhasználásának támogatásával nagymértékben tudja csökkenteni a szoftverek fejlesztéséhez szükséges időt.

III.1. Bevezetés az objektum-orientált világba

Az objektum-orientált programozás a „dolgokat” („objektumokat”) és köztük fennálló kölcsönhatásokat használja alkalmazások és számítógépes programok tervezéséhez. Ez a módszertan olyan megoldásokat foglal magában, mint a bezárás (encapsulation), a modularitás (modularity), a többalakúság (polymorphism) valamint az öröklés (inheritance).

Felhívjuk a figyelmet arra, hogy az OOP nyelvek általában csak eszközöket és támogatást nyújtanak az objektum-orientáltság elveinek megvalósításához. Könyvünkben egy rövid áttekintés után, mi is csak az eszközök bemutatására szorítkozunk.

III.1.1. Alapelemek

Először ismerkedjünk meg az objektum-orientált témakör alapelemeivel! A megértéshez nem szükséges mély programozási ismeret megléte.

Osztály (class)

Az osztály (class) meghatározza egy dolog (objektum) elvont jellemzőit, beleértve a dolog jellemvonásait (attribútumok, mezők, tulajdonságok) és a dolog viselkedését (amit a dolog meg tud tenni, metódusok (módszerek), műveletek, funkciók).

Azt mondhatjuk, hogy az osztály egy tervrajz, amely leírja valaminek a természetét. Például, egy Teherautó osztálynak tartalmazni kell a teherautók közös jellemzőit (gyártó, motor, fékrendszer, maximális terhelés stb.), valamint a fékezés, a balra fordulás stb. képességeket (viselkedés).

Osztályok önmagukban biztosítják a modularitást és a strukturáltságot az objektum-orientált számítógépes programok számára. Az osztálynak értelmezhetőnek kell lennie a probléma területén jártas, nem programozó emberek számára is, vagyis az osztály jellemzőinek „beszédesnek” kell lenniük. Az osztály kódjának viszonylag önállónak kell lennie (bezárás – encapsulation). Az osztály beépített tulajdonságait és metódusait egyaránt az osztály tagjainak nevezzük (C++-ban adattag, tagfüggvény).

Objektum (object)

Az osztály az objektum mintája (példája). A Teherautó osztály segítségével minden lehetséges teherautót megadhatunk, a tulajdonságok és a viselkedési formák felsorolásával. Például, a Teherautó osztály rendelkezik fékrendszerrel, azonban az énAutóm (objektum) fékrendszere elektronikusvezérlésű (EBS) vagy egyszerű légfékes is lehet.

Példány (instance)

Az objektum szinonimájaként az osztály egy adott példányáról is szokás beszélni. A példány alatt a futásidőben létrejövő aktuális objektumot értjük. Így elmondhatjuk, hogy az énAutóm a Teherautó osztály egy példánya. Az aktuális objektum tulajdonságértékeinek halmazát az objektum állapotának (state) nevezzük. Ezáltal minden objektumot az osztályban definiált állapot és viselkedés jellemez.

Metódus (method)

Metódusok felelősek az objektumok képességeiért. A beszélt nyelvben az igéket hívhatjuk metódusoknak. Mivel az énAutóm egy Teherautó, rendelkezik a fékezés képességével, így a Fékez() ez énAutóm metódusainak egyike. Természetesen további metódusai is lehetnek, mint például az Indít(), a GáztAd(), a BalraFordul() vagy a JobbraFordul(). A programon belül egy metódus használata általában csak egy adott objektumra van hatással. Bár minden teherautó tud fékezni, a Fékez() metódus aktiválásával (hívásával) csak egy adott járművet szeretnénk lassítani. C++ nyelven a metódus szó helyett a tagfüggvény kifejezést használjuk.

Az énAutóm objektum (a Teherautó osztály példánya)
III.1. ábra - Az énAutóm objektum (a Teherautó osztály példánya)


Üzenetküldés (message passing)

Az üzenetküldés az a folyamat, amelynek során egy objektum adatokat küld egy másik objektumnak, vagy “megkéri” a másik objektumot valamely metódusának végrehajtására. Az üzenetküldés szerepét jobban megértjük, ha egy teherautó szimulációjára gondolunk. Ebben egy sofőr objektum a „fékezz” üzenet küldésével aktiválhatja az énAutóm Fékez() metódusát, lefékezve ezzel a járművet. Az üzenetküldés szintaxisa igen eltérő a különböző programozási nyelvekben. C++ nyelven a kódszintű üzenetküldést a metódushívás valósítja meg.

III.1.2. Alapvető elvek

Egy rendszer, egy programnyelv objektum-orientáltságát az alábbi elvek támogatásával lehet mérni. Amennyiben csak néhány elv valósul meg, objektum-alapú rendszerről beszélünk, míg mind a négy elv támogatása az objektum-orientált rendszerek sajátja.

III.1.2.1. Bezárás, adatrejtés (encapsulation , data hiding)

A fentiekben láttuk, hogy az osztályok alapvetően jellemzőkből (állapot) és metódusokból (viselkedés) épülnek fel. Azonban az objektumok állapotát és viselkedését két csoportba osztjuk. Lehetnek olyan jellemzők és metódusok, melyeket elfedünk más objektumok elől, mintegy belső, privát (private, védett - protected) állapotot és viselkedést létrehozva. Másokat azonban nyilvánossá (public) teszünk. Az OOP alapelveinek megfelelően az állapotjellemzőket privát eléréssel kell megadnunk, míg a metódusok többsége nyilvános lehet. Szükség esetén a privát jellemzők ellenőrzött elérésére nyilvános metódusokat készíthetünk.

Általában is elmondhatjuk, hogy egy objektum belső világának ismeretére nincs szüksége annak az objektumnak, amelyik üzenetet küld. Például, a Teherautó rendelkezik a Fékez() metódussal, amely pontosan definiálja, miként megy végbe a fékezés. Az énAutóm vezetőjének azonban nem kell ismernie, hogyan is fékez a kocsi.

Minden objektum egy jól meghatározott interfészt biztosít a külvilág számára, amely megadja, hogy kívülről mi érhető el az objektumból. Az interfész rögzítésével az objektumot használó, ügyfél alkalmazások számára semmilyen problémát sem jelent az osztály belső világának jövőbeni megváltoztatása. Így például egy interfészen keresztül biztosíthatjuk, hogy pótkocsikat csak a Kamion osztály objektumaihoz kapcsoljunk.

III.1.2.2. Öröklés (inheritance)

Öröklés során egy osztály specializált változatait hozzuk létre, amelyek öröklik a szülőosztály (alaposztály) jellemzőit és viselkedését, majd pedig sajátként használják azokat. Az így keletkező osztályokat szokás alosztályoknak (subclass), vagy származtatott (derived) osztályoknak hívni.

Például, a Teherautó osztályból származtathatjuk a Kisteherautó és a Kamion alosztályokat. Az énAutóm ezentúl legyen a Kamion osztály példánya! Tegyük fel továbbá, hogy a Teherautó osztály definiálja a Fékez() metódust és az fékrendszer tulajdonságot! Minden ebből származtatott osztály (Kisteherautó és a Kamion) örökli ezeket a tagokat, így a programozónak csak egyszer kell megírnia a hozzájuk tartozó kódot.

Az öröklés menete
III.2. ábra - Az öröklés menete


Az alosztályok meg is változtathatják az öröklött tulajdonságokat. Például, a Kisteherautó osztály előírhatja, hogy a maximális terhelése 20 tonna. A Kamion alosztály pedig az EBS fékezést teheti alapértelmezetté a Fékez() metódusa számára.

A származtatott osztályokat új tagokkal is bővíthetjük. A Kamion osztályhoz adhatunk egy Navigál() metódust. Az elmondottak alapján egy adott Kamion példány Fékez() metódusa EBS alapú fékezést alkalmaz, annak ellenére, hogy a Teherautó osztálytól egy hagyományos Fékez() metódust örökölt; rendelkezik továbbá egy új Navigál() metódussal, ami azonban nem található meg a Kisteherautó osztályban.

Az öröklés valójában „egy” (is-a) kapcsolat: az énAutóm egy Kamion, a Kamion pedig egy Teherautó. Így az énAutóm egyaránt rendelkezik a Kamion és a Teherautó metódusaival.

A fentiekben mindkét származtatott osztálynak pontosan egy közvetlen szülő ősosztálya volt, a Teherautó. Ezt az öröklési módot egyszeres öröklésnek (single inheritance) nevezzük, megkülönböztetve a többszörös örökléstől.

A többszörös öröklés (multiple inheritance) folyamán a származtatott osztály, több közvetlen ősosztály tagjait örökli. Például, egymástól teljesen független osztályokat definiálhatunk Teherautó és Hajó néven. Ezekből pedig örökléssel létrehozhatunk egy Kétéltű osztályt, amely egyaránt rendelkezik a teherautók és hajók jellemzőivel és viselkedésével. A legtöbb programozási nyelv (ObjectPascal, Java, C#) csak az egyszeres öröklést támogatja, azonban a C++-ban mindkét módszer alkalmazható.

Többszörös öröklés
III.3. ábra - Többszörös öröklés


III.1.2.3. Absztrakció (abstraction)

Az elvonatkoztatás a probléma megfelelő osztályokkal való modellezésével egyszerűsíti az összetett valóságot, valamint a probléma - adott szempontból - legmegfelelőbb öröklési szintjén fejti ki hatását. Például, az énAutóm az esetek nagy többségében Teherautóként kezelhető, azonban lehet Kamion is, ha szükségünk van a Kamion specifikus jellemzőkre és viselkedésre, de tekinthetünk rá Járműként is, ha egy flotta elemeként vesszük számba. (A Jármű a példában a Teherautó szülő osztálya.)

Az absztrakcióhoz a kompozíción keresztül is eljuthatunk. Például, egy Autó osztálynak tartalmaznia kell egy motor, sebességváltó, kormánymű és egy sor más komponenst. Ahhoz, hogy egy Autót felépítsünk, nem kell tudnunk, hogyan működnek a különböző komponensek, csak azt kell ismernünk, miként kapcsolódhatunk hozzájuk (interfész). Az interfész megmondja, miként küldhetünk nekik, illetve fogadhatunk tőlük üzenetet, valamint információt ad arról, hogy az osztályt alkotó komponensek milyen kölcsönhatásban vannak egymással.

III.1.2.4. Polimorfizmus (polymorphism)

A polimorfizmus lehetővé teszi, hogy az öröklés során bizonyos (elavult) viselkedési formákat (metódusokat) a származtatott osztályban új tartalommal valósítsunk meg, és az új, lecserélt metódusokat a szülő osztály tagjaiként kezeljük.

Példaként tegyük fel, hogy a Teherautó és a Kerekpár osztályok öröklik a Jármű osztály Gyorsít() metódusát. A Teherautó esetén a Gyorsít() parancs a GáztAd() műveletet jelenti, míg Kerekpár esetén a Pedáloz() metódus hívását. Ahhoz, hogy a gyorsítás helyesen működjön, a származtatott osztályok Gyorsít() metódusával felül kell bírálnunk (override) a Jármű osztálytól örökölt Gyorsít() metódust. Ez a felülbíráló polimorfizmus.

A legtöbb OOP nyelv a parametrikus polimorfizmust is támogatja, ahol a metódusokat típusoktól független módon, mintegy mintaként készítjük el a fordító számára. A C++ nyelven sablonok (templates) készítésével alkalmazhatjuk ezt a lehetőséget.

III.1.3. Objektum-orientált C++ programpélda

Végezetül nézzük meg az elmondottak alapján elkészített C++ programot! Most legfontosabb az első benyomás, hiszen a részletekkel csak a könyvünk további részeiben ismerkedik meg az Olvasó.

#include <iostream>
#include <string>
using namespace std;
 
class Teherauto {
   protected:
      string gyarto;
      string motor;
      string fekrendszer;
      string maximalis_terheles;
   public:
      Teherauto(string gy, string m, string fek, 
                double teher) {
         gyarto = gy;
         motor = m;
         fekrendszer = fek;
         maximalis_terheles = teher;
      }
      void Indit() { }
      void GaztAd() { }
      virtual void Fekez() {
           cout<<"A hagyomanyosan fekez."<< endl; 
      }
      void BalraFordul() { }
      void JobbraFordul() { }
};
 
class Kisteherauto : public Teherauto {
    public:
      Kisteherauto(string gy, string m, string fek) 
            : Teherauto(gy, m, fek, 20) {  }
};
 
class Kamion : public Teherauto {
    public:
      Kamion(string gy, string m, string fek, double teher) 
            : Teherauto(gy, m, fek, teher) {  }
      void Fekez() { cout<<"A EBS-sel fekez."<< endl; }
      void Navigal() {}
};
 
int main() {
    Kisteherauto posta("ZIL", "Diesel", "légfék");
    posta.Fekez();      // A hagyomanyosan fekez.
    Kamion enAutom("Kamaz", "gázmotor", "EBS", 40);
    enAutom.Fekez();    // A EBS-sel fekez.
}

A könyvünk további fejezeteiben bemutatjuk azokat a C++ nyelvi eszközöket, amelyekkel megvalósíthatjuk a fenti fogalmakkal jelölt megoldásokat. Ez az áttekintés azonban nem elegendő az objektum-orientált témakörben való jártasság megszerzéséhez, ez csak a belépő az OOP világába.

III.2. Osztályok és objektumok

Az objektum-orientált gondolkodásmód absztrakt adattípusának (ADT – abstract data type) elkészítésére kétféle megoldás is a rendelkezésünkre áll a C++ nyelvben. A C++ nyelv struct típusa a C nyelv struktúra típusának kiterjesztését tartalmazza, miáltal alkalmassá vált absztrakt adattípusok definiálására. A C++ nyelv az új class (osztály) típust is biztosítja számunkra.

A struct és a class típusok adattagokból (data member) és ezekhez kapcsolódó műveletekből (tagfüggvényekből – member function) épülnek fel. Mindkét adattípussal készíthetünk osztályokat, azonban a tagok alapértelmezés szerinti hozzáférésének következtében a class típus áll közelebb az objektum-orientált elvekhez. Alapértelmezés szerint a struct típus minden tagja nyilvános elérésű, míg a class típus tagjaihoz csak az osztály tagfüggvényeiből lehet hozzáférni.

Az osztálydeklaráció két részből áll. Az osztály feje a class/struct alapszó után az osztály nevét tartalmazza. Ezt követi az osztály törzse, amelyet kapcsos zárójelek fognak közre, és pontosvessző zár. A deklaráció az adattagokon és tagfüggvényeken kívül, a tagokhoz való hozzáférést szabályzó, később tárgyalásra kerülő public (nyilvános, publikált), private (saját, rejtett, privát) és protected (védett) kulcsszavakat is tartalmaz, kettősponttal zárva.

class OsztályNév {
  public:
   típus4 Függvény1(paraméterlista1) {  }
   típus5 Függvény2(paraméterlista2) {  }
  protected:
    típus3 adat3;
  private:
    típus1 adat11, adat12;
    típus2 adat2;
};

Az class és a struct osztályok deklarációját a C++ programban bárhol elhelyezhetjük, ahol deklaráció szerepelhet, azonban a modul szintű (fájl szintű) megadás felel meg leginkább a modern programtervezési módszereknek.

III.2.1. A struktúráktól az osztályokig

Ebben a fejezetben a már meglévő (struct típus) ismereteinkre építve lépésről-lépésre jutunk el az objektumok alkalmazásáig. Az alfejezetekben felvetődő problémákat először hagyományos módon oldjuk meg, majd pedig rátérünk az objektum-orientált gondolkodást követő megoldásra.

III.2.1.1. Egy kis ismétlés

Valamely feladat megoldásához szükséges adatokat már eddigi ismereteinkkel is össze tudjuk fogni a struktúra típus alkalmazásával, amennyiben az adatok tárolására a struktúra tagjait használjuk:

struct Alkalmazott{
    int torzsszam;
    string nev;
    float fizetes;
};

Sőt műveleteket is definiálhatunk függvények formájában, amelyek argumentumként megkapják a struktúra típusú változót:

void BertEmel(Alkalmazott& a, float szazalek) {
    a.ber *= (1 + szazalek/100);
}

A struktúra tagjait a pont, illetve a nyíl operátorok segítségével érhetjük el, attól függően, hogy a fordítóra bízzuk a változó létrehozását, vagy pedig magunk gondoskodunk a területfoglalásról:

int main() {
    Alkalmazott mernok;
    mernok.torzsszam = 1234;
    mernok.nev = "Okos Antal";
    mernok.ber = 2e5;
    BertEmel(mernok,12);
    cout << mernok.ber << endl;
 
    Alkalmazott *pKonyvelo = new Alkalmazott;
    pKonyvelo->torzsszam = 1235;
    pKonyvelo->nev = "Gazdag Reka";
    pKonyvelo->ber = 3e5;
    BertEmel(*pKonyvelo,10);
    cout << pKonyvelo->ber << endl;
    delete pKonyvelo;
}

Természetesen a fent bemutatott módon is lehet strukturált felépítésű, hatékony programokat fejleszteni, azonban ebben a fejezetben mi tovább megyünk.

III.2.1.2. Adatok és műveletek egybeépítése

Első lépésként - a bezárás elvének (encapulation) megfelelően - az adatokat és a rajtuk elvégzendő műveleteket egyetlen programegysége foglaljuk, azonban ezt a programegységet már, bár struktúra osztálynak nevezzük.

struct Alkalmazott {
    int torzsszam;
    string nev;
    float ber;
    void BertEmel(float szazalek) {
        ber *= (1 + szazalek/100);
    }
};

Első látásra feltűnik, hogy a BertEmel() függvény nem kapja meg paraméterként az osztály típusú változót (objektumot), hiszen alapértelmezés szerint az objektumon végez műveletet. Az Alkalmazott típusú objektumok használatát bemutató main () függvény is valamelyest módosult, hiszen most már a változóhoz tartozó tagfüggvényt hívjuk:

int main() {
    Alkalmazott mernok;
    mernok.torzsszam = 1234;
    mernok.nev = "Okos Antal";
    mernok.ber = 2e5;
    mernok.BertEmel(12);
    cout << mernok.ber << endl;
 
    Alkalmazott *pKonyvelo = new Alkalmazott;
    pKonyvelo->torzsszam = 1235;
    pKonyvelo->nev = "Gazdag Reka";
    pKonyvelo->ber = 3e5;
    pKonyvelo->BertEmel(10);
    cout << pKonyvelo->ber << endl;
    delete pKonyvelo;
}

III.2.1.3. Adatrejtés

Az osztály típusú változók (objektumok) adattagjainak közvetlen elérése ellentmond az adatrejtés elvének. Objektum-orientált megoldásoknál kívánatos, hogy az osztály adattagjait ne lehessen közvetlenül elérni az objektumon kívülről. A struct típus alaphelyzetben teljes elérhetőséget biztosít a tagjaihoz, míg a class típus teljesen elzárja a tagjait a külvilág elől, ami sokkal inkább megfelel az objektum-orientált elveknek. Felhívjuk a figyelmet arra, hogy az osztályelemek elérhetőségét a private, protected és public kulcsszavak segítségével magunk is szabályozhatjuk.

A public tagok bárhonnan elérhetők a programon belül, ahonnan maga az objektum elérhető. Ezzel szemben a private tagokhoz csak az osztály saját tagfüggvényeiből férhetünk hozzá. (A protected elérést a III.3. szakasz tárgyalt öröklés során alkalmazzuk.)

Az osztályon belül tetszőleges számú tagcsoportot kialakíthatunk az elérési kulcsszavak (private, protected, public) alkalmazásával, és a csoportok sorrendjére sincs semmilyen megkötés.

A fenti példánknál maradva, a korlátozott elérés miatt szükséges további tagfüggvényeket megadnunk, amelyekkel ellenőrzött módon beállíthatjuk (set), illetve lekérdezhetjük (get) az adattagok értékét. A beállító függvényekben a szükséges ellenőrzéseket is elvégezhetjük, így csak érvényes adat fog megjelenni az Alkalmazott típusú objektumokban. A lekérdező függvényeket általában konstansként adjuk meg, ami azt jelöli, hogy nem módosítjuk az adattagok értékét a tagfüggvényből. Konstans tagfüggvényben a függvény feje és törzse közé helyezzük a const foglalt szót. Példánkban a GetBer() konstans tagfüggvény.

class Alkalmazott{
  private:
    int torzsszam;
    string nev;
    float ber;
  public:
    void BertEmel(float szazalek) {
        ber *= (1 + szazalek/100);
    }
    void SetAdatok(int tsz, string n, float b) {
        torzsszam = tsz;
        nev = n;
        ber = b;
    }
    float GetBer() const {
        return ber;
    }
};
 
int main() {
    Alkalmazott mernok;
    mernok.SetAdatok(1234, "Okos Antal", 2e5);
    mernok.BertEmel(12);
    cout << mernok.GetBer() << endl;
 
    Alkalmazott *pKonyvelo = new Alkalmazott;
    pKonyvelo->SetAdatok(1235, "Gazdag Reka", 3e5);
    pKonyvelo->BertEmel(10);
    cout << pKonyvelo->GetBer() << endl;
    delete pKonyvelo;
}

Megjegyezzük, hogy a konstans tagfüggvényekből is megváltoztathatunk adattagokat, amennyiben azokat a mutable (változékony) kulcsszóval deklaráljuk, például:

mutable float ber;

Az ilyen megoldásokat azonban igen ritkán alkalmazzuk.

Megjegyezzük, ha egy osztály minden adattagja nyilvános elérésű, akkor az objektum inicializálására a struktúráknál bemutatott megoldást is használhatjuk, például:

Alkalmazott portas = {1122, "Biztos Janos", 1e5};

Mivel a későbbiek folyamán a fenti forma használhatóságát további megkötések korlátozzák (nem lehet származtatott osztály, nem lehetnek virtuális tagfüggvényei), ajánlott az inicializálást az osztályok speciális tagfüggvényeivel, az ún. konstruktorokkal elvégezni.

III.2.1.4. Konstruktorok

Az osztályokat használó programokban egyik leggyakoribb művelet az objektumok létrehozása. Az objektumok egy részét mi hozzuk részre statikus vagy dinamikus helyfoglalással (lásd fent), azonban vannak olyan esetek is, amikor a fordítóprogram készít ún. ideiglenes objektumpéldányokat. Hogyan gondoskodhatunk a megszülető objektumok adattagjainak kezdőértékkel való (automatikus) ellátásáról? A választ a konstruktornak nevezett tagfüggvények bevezetésével találjuk meg.

class Alkalmazott{
  private:
    int torzsszam;
    string nev;
    float ber;
  public:
    Alkalmazott() {                           // default
        torzsszam = 0;
        nev = "";
        ber = 0;
    }
    Alkalmazott(int tsz, string n, float b) { // paraméteres
        torzsszam = tsz;
        nev = n;
        ber = b;
    }
    Alkalmazott(const Alkalmazott & a) {      // másoló 
        torzsszam = a.torzsszam;
        nev = a.nev;
        ber = a.ber;
    }
    void BertEmel(float szazalek) {
        ber *= (1 + szazalek/100);
    }
    void SetNev(string n) {
        nev = n;
    }
    float GetBer() const {
        return ber;
    }
};
 
int main() {
    Alkalmazott dolgozo;
    dolgozo.SetNev("Kiss Pista");
 
    Alkalmazott mernok(1234, "Okos Antal", 2e5);
    mernok.BertEmel(12);
    cout << mernok.GetBer() << endl;
 
    Alkalmazott fomernok = mernok;
    // vagy: Alkalmazott fomernok(mernok);
    fomernok.BertEmel(50);
    cout << fomernok.GetBer() << endl;
 
    Alkalmazott *pDolgozo = new Alkalmazott;
    pDolgozo->SetNev("Kiss Pista");
    delete pDolgozo;
 
    Alkalmazott *pKonyvelo;
    pKonyvelo = new Alkalmazott(1235, "Gazdag Reka", 3e5);
    pKonyvelo->BertEmel(10);
    cout << pKonyvelo->GetBer() << endl;
    delete pKonyvelo;
 
    Alkalmazott *pFomernok=new Alkalmazott(mernok);
    pFomernok->BertEmel(50);
    cout << pFomernok->GetBer() << endl;
    delete pFomernok;
}

A fenti példában – a függvénynevek túlterhelését alkalmazva - készítettünk egy paraméter nélküli, egy paraméteres és egy másoló konstruktort. Látható, hogy a konstruktor olyan tagfüggvény, amelynek neve megegyezik az osztály nevével, és nincs visszatérési típusa. Az osztály konstruktorát a fordító minden olyan esetben automatikusan meghívja, amikor az adott osztály objektuma létrejön. A konstruktor nem rendelkezik visszatérési értékkel, de különben ugyanúgy viselkedik, mint bármely más tagfüggvény. A konstruktor átdefiniálásával (túlterhelésével) többféleképpen is inicializálhatjuk az objektumokat.

A konstruktor nem foglal tárterületet a létrejövő objektum számára, feladata a már lefoglalt adatterület inicializálása. Ha azonban az objektum valamilyen mutatót tartalmaz, akkor a konstruktorból kell gondoskodnunk a mutató által kijelölt terület lefoglalásáról.

Egy osztály alapértelmezés szerint két konstruktorral rendelkezik: a paraméter nélküli (default) és a másoló konstruktorral. Ha valamilyen saját konstruktort készítünk, akkor a paraméter nélküli alapértelmezett (default) konstruktor nem érhető el, így azt is definiálnunk kell. Saját másoló konstruktort általában akkor használunk, ha valamilyen dinamikus tárterület tartozik az osztály példányaihoz.

A paraméter nélküli és a paraméteres konstruktort gyakran összevonjuk az alapértelmezés szerinti argumentumok bevezetésével:

class Alkalmazott{
  private:
    int torzsszam;
    string nev;
    float ber;
  public:
    Alkalmazott(int tsz = 0, string n ="", float b=0) {
        torzsszam = tsz;
        nev = n;
        ber = b;
    }
    …
}
III.2.1.4.1. Taginicializáló lista alkalmazása

A konstruktorokból az osztály tagjait kétféleképpen is elláthatjuk kezdőértékkel. A hagyományosnak tekinthető megoldást, a konstruktor törzsén belüli értékadást már jól ismerjük. Emellett a C++ nyelv lehetővé teszi az ún. taginicializáló lista alkalmazását. Az inicializáló listát közvetlenül a konstruktor feje után kettősponttal elválasztva adjuk meg. A vesszővel tagolt lista elemei az osztály adattagjai, melyeket zárójelben követnek a kezdőértékek. A taginicializáló lista bevezetésével a fenti példák konstruktorai üressé válnak:

class Alkalmazott{
  private:
    int torzsszam;
    string nev;
    float ber;
  public:
    Alkalmazott(int tsz=0, string n="", float b=0) 
        : torzsszam(tsz), nev(n), ber(b) { }
    Alkalmazott(const Alkalmazott & a) 
        : torzsszam(a.torzsszam), nev(a.nev),  
          ber(a.ber) { }
    …
}

Szükséges megjegyeznünk, hogy a konstruktor hívásakor az inicializáló lista feldolgozása után következik a konstruktor törzsének végrehajtása.

III.2.1.4.2. Az objektumok explicit inicializálása

Egyparaméteres konstruktorok esetén a fordító – szükség esetén - implicit típus-átalakítást használ a megfelelő konstruktor kiválasztásához. Az explicit kulcsszó konstruktor előtti megadásával megakadályozhatjuk az ilyen konverziók alkalmazását a konstruktorhívás során.

Az alábbi példában az explicit kulcsszó segítségével különbséget tehetünk a kétféle (explicit és implicit) kezdőérték-adási forma között:

class Szam 
{
  private:
    int n;
  public:
    explicit Szam( int x) {
        n = x;
        cout << "int: " << n << endl;
    }
    Szam( float x) {
        n = x < 0 ? int(x-0.5) : int(x+0.5);
        cout << "float: " << n << endl;
    }
};
 
int main() {
  Szam a(123);    // explicit hívás
  Szam b = 123;   // implicit (nem explicit) hívás
}

Az a objektum létrehozásakor az explicit konstruktor hívódik meg, míg a b objektum esetén a float paraméterű. Az explicit szó elhagyásával mindkét esetben az első konstruktor aktiválódik.

III.2.1.5. Destruktor

Gyakran előfordul, hogy egy objektum létrehozása során erőforrásokat (memória, állomány stb.) foglalunk le, amelyeket az objektum megszűnésekor fel kell szabadítanunk. Ellenkező esetben ezek az erőforrások elvesznek a programunk számára.

A C++ nyelv biztosít egy speciális tagfüggvényt - a destruktort - amelyben gondoskodhatunk a lefoglalt erőforrások felszabadításáról. A destruktor nevét hullám karakterrel (~) egybeépített osztálynévként kell megadni. A destruktor, a konstruktorhoz hasonlóan nem rendelkezik visszatérési típussal.

Az alábbi példában egy 12-elemű, dinamikus helyfoglalású tömböt hozunk létre a konstruktorokban, az alkalmazottak havi munkaidejének tárolására. A tömb számára lefoglalt memóriát a destruktorban szabadítjuk fel.

class Alkalmazott{
  private:
    int torzsszam;
    string nev;
    float ber;
    int *pMunkaorak;
  public:
    Alkalmazott(int tsz = 0, string n ="", float b=0) {
        torzsszam = tsz;
        nev = n;
        ber = b;
        pMunkaorak = new int[12];
        for (int i=0; i<12; i++) pMunkaorak[i]=0;
    }
    Alkalmazott(const Alkalmazott & a) {
        torzsszam = a.torzsszam;
        nev = a.nev;
        ber = a.ber;
        pMunkaorak = new int[12];
        for (int i=0; i<12; i++) 
            pMunkaorak[i]=a.pMunkaorak[i];
    }
    ~Alkalmazott() {
        delete[] pMunkaorak;
        cout << nev << " torolve" << endl;
    }
    void BertEmel(float szazalek) {
        ber *= (1 + szazalek/100);
    }
    void SetMunkaora(int honap, int oraszam) {
        if (honap >= 1 && honap <=12) {
            pMunkaorak[honap-1]=oraszam;
        }
    }
    float GetBer() const {
        return ber;
    }
};
 
int main() {
    Alkalmazott mernok(1234, "Okos Antal", 2e5);
    mernok.BertEmel(12);
    mernok.SetMunkaora(3,192);
    cout << mernok.GetBer() << endl;
 
    Alkalmazott *pKonyvelo;
    pKonyvelo = new Alkalmazott(1235, "Gazdag Reka", 3e5);
    pKonyvelo->BertEmel(10);
    pKonyvelo->SetMunkaora(1,160);
    pKonyvelo->SetMunkaora(12,140);
    cout << pKonyvelo->GetBer() << endl;
    delete pKonyvelo;
}

A lefordított program minden olyan esetben meghívja az osztály destruktorát, amikor az objektum érvényessége megszűnik. Kivételt képeznek a new operátorral dinamikusan létrehozott objektumok, melyek esetén a destruktort csak a delete operátor segítségével aktivizálhatjuk. Fontos megjegyeznünk, hogy a destruktor nem magát az objektumot szűnteti meg, hanem automatikusan elvégez néhány általunk megadott „takarítási” műveletet.

A példaprogram futtatásakor az alábbi szöveg jelenik meg:

224000
330000
Gazdag Reka torolve
Okos Antal torolve

Ebből láthatjuk, hogy először a *pKonyvelo objektum destruktora hívódik meg a delete operátor használatakor. Ezt követően a main () függvény törzsét záró kapcsos zárójel elérésekor automatikusan aktiválódik a mernok objektum destruktora.

Amennyiben nem adunk meg destruktort, a fordítóprogram automatikusan egy üres destruktorral látja el az osztályunkat.

III.2.1.6. Az osztály objektumai, a this mutató

Amikor az Alkalmazott osztálytípussal objektumokat (osztály típusú változókat) hozunk létre:

Alkalmazott mernok(1234, "Okos Antal", 2e5);
Alkalmazott *pKonyvelo;
pKonyvelo = new Alkalmazott(1235, "Gazdag Reka", 3e5);

minden objektum saját adattagokkal rendelkezik, azonban a tagfüggvények egyetlen példányát megosztva használják (III.4. ábra).

mernok.BertEmel(12);
pKonyvelo->BertEmel(10);

Felvetődik a kérdés, honnan tudja például a BertEmel() függvény, hogy a hívásakor mely adatterületet kell elérnie?

Erre a kérdésre a fordító nem látható tevékenysége adja meg a választ: minden tagfüggvény, még a paraméter nélküliek is, rendelkeznek egy nem látható paraméterrel (this), amelyben a hívás során az aktuális objektumra mutató pointer adódik át a függvénynek. A fentieken kívül minden adattag-hivatkozás automatikusan az alábbi formában kerül be a kódba:

this->adattag

Az Alkalmazott osztály és az objektumai
III.4. ábra - Az Alkalmazott osztály és az objektumai


A this (ez) mutatót mi is felhasználhatjuk a tagfüggvényeken belül. Ez a lehetőség jól jön, amikor egy paraméter neve megegyezik valamely adattag nevével:

class Alkalmazott{
  private:
    int torzsszam;
    string nev;
    float ber;
  public:
    Alkalmazott(int torzsszam=0, string nev="", float ber=0){
        this->torzsszam = torzsszam;
        this->nev = nev;
        this->ber = ber;
    }
};

A this mutató deklarációja normál tagfüggvények esetén Osztálytípus* constthis, illetve const Osztálytípus*const this a konstans tagfüggvényekben.

III.2.2. Az osztályokról bővebben

Az előző alfejezetben eljutottunk a struktúráktól az osztályokig. Megismerkedtünk azokkal a megoldásokkal, amelyek jól használható osztályok kialakítását teszik lehetővé. Bátran hozzáfoghatunk a feladatok osztályok segítségével történő megoldásához.

Jelen fejezetben kicsit továbblépünk, és áttekintjük az osztályokkal kapcsolatos - kevésbé általános - tudnivalókat. A fejezet tartalmát akkor javasoljuk feldolgozni, ha az Olvasó már jártassággal rendelkezik az osztályok készítésében.

III.2.2.1. Statikus osztálytagok

C++ nyelven az osztályok adattagjai előtt megadhatjuk a static kulcsszót, jelezve azt, hogy ezeket a tagokat (a tagfüggvényekhez hasonlóan) megosztva használják az osztály objektumai. Az egyetlen példányban létrejövő statikus adattag közvetlenül az osztályhoz tartozik, így az akkor is elérhető, ha egyetlen objektuma sem létezik az osztálynak.

A statikus adattag inicializálását az osztályon kívül kell elvégezni (függetlenül az adattag elérhetőségétől). Kivételt képeznek a static const egész és felsorolás típusú adattagok, melyek kezdőértéke az osztályon belül is megadható.

Ha a statikus adattag nyilvános elérésű, akkor a programban bárhonnan felhasználhatjuk az osztály neve és a hatókör (::) operátor magadásával. Ellenkező esetben csak az osztály példányai érik el ezeket a tagokat.

Az alábbi példában a statikus tagok használatának bemutatásán túlmenően, a konstansok osztályban való elhelyezésének megoldásait (static const és enum) is szemléltetjük. Az általunk definiált matematikai osztály (Math) lehetővé teszi, hogy a Sin() és a Cos() tagfüggvényeket radián vagy fok mértékegységű adatokkal hívjuk:

#include <iostream>
#include <iomanip>
#include <cmath>
using namespace std;
 
class Math {
  public:
    enum Egyseg {fok, radian};
  private:
    static double dFok2Radian;
    static Egyseg eMode;
  public:
    static const double Pi;
 
    static double Sin(double x)
           {return sin(eMode == radian ? x : dFok2Radian*x);}
    static double Cos(double x)
           {return cos(eMode == radian ? x : dFok2Radian*x);}
    static void Mertekegyseg(Egyseg mode = radian) { 
                                          eMode = mode; }
    void KiirPI() { cout.precision(18); cout<<Pi<<endl;}
};
 
// A statikus adattagok létrehozása és inicializálása
const double Math::Pi = M_PI;
double Math::dFok2Radian = Math::Pi/180;
Math::Egyseg Math::eMode = Math::radian;

A példában látható módon, az osztályon belül egy felsorolást is elhelyezhetünk. Az így elkészített Egyseg típusnévre és a felsorolt (fok, radian) konstansokra az osztálynévvel minősített név segítségével hivatkozhatunk. Ezek a nevek osztály hatókörrel rendelkeznek, függetlenül az enum kulcsszó után megadott típusnévtől (Math::Egyseg): Math::radian, Math::fok.

A statikus adattagok kezelésére általában statikus tagfüggvényeket használunk (Math::Sin(), Math::Cos(), Math::Mertekegyseg()). A statikus tagfüggvényekből azonban a normál adattagokhoz nem férhetünk hozzá, mivel a paramétereik között nem szerepel a this mutató. A nem statikus tagfüggvényekből az osztály statikus tagjait korlátozás nélkül elérhetjük.

A Math osztály lehetséges alkalmazását az alábbiakban láthatjuk:

int main() {
   double y = Math::Sin(Math::Pi/6);  // radiánban számol
   Math::Mertekegyseg(Math::fok);     // fokokban számol
   y = Math::Sin(30);
   Math::Mertekegyseg(Math::radian);  // radiánban számol
   y = Math::Sin(Math::Pi/6);
 
   Math m;                            // oszálypéldány
   m.Mertekegyseg(Math::fok);         // vagy
   m.Mertekegyseg(m.fok);
   y = m.Sin(30);
   m.Mertekegyseg(m.radian);          // vagy
   m.Mertekegyseg(Math::radian);
   y = m.Sin(Math::Pi/6);
   m.KiirPI();
}

III.2.2.2. Az osztályok kialakításának lehetőségei

A C++ nyelv szabályai többféle osztálykialakítási megoldást is lehetővé tesznek. Az alábbi példákban szigorúan elkülönítjük az egyes eseteket, azonban a programozási gyakorlatban ezeket vegyesen használjuk.

III.2.2.2.1. Implicit inline tagfüggvények alkalmazása

Az első esetben az osztály leírásában szerepeltetjük a tagfüggvények teljes definícióját. A fordító az ilyen tagfüggvényeket automatikusan inline függvénynek tekinti. A megoldás nagy előnye, hogy a teljes osztályt egyetlen fejállományban tárolhatjuk, és az osztály tagjait könnyen áttekinthetjük. Általában kisebb méretű osztályok esetén alkalmazható hatékonyan ez a megoldás.

Példaként tekintsük a síkbeli pontok kezelését segítő Pont osztályt!

class Pont {
   private:
      int x,y;
   public:
      Pont(int a = 0, int b = 0) { x = a; y = b; }
      int GetX() const { return x; }
      int GetY() const { return y; }
      void SetX(int a) { x = a; }
      void SetY(int a) { y = a; }
      void Mozgat(int a, int b) { x = a; y = b; }
      void Mozgat(const Pont& p) { x = p.x; y = p.y; }
      void Kiir() const { cout<<"("<<x<<","<<y<<")\n"; }
};
III.2.2.2.2. Osztálystruktúra a C++/CLI alkalmazásokban

A fentiekhez hasonló megoldást követnek a Visual C++ a .NET projektekben, valamint a Java és a C# nyelvek. Szembeötlő különbség, hogy az osztálytagok elérhetőségek szerinti csoportosítása helyett, minden tag hozzáférését külön megadjuk.

class Pont {
   private: int x,y;
   public: Pont(int a = 0, int b = 0) { x = a; y = b; }
   public: int GetX() const { return x; }
   public: int GetY() const { return y; }
   public: void SetX(int a) { x = a; }
   public: void SetY(int a) { y = a; }
   public: void Mozgat(int a, int b) { x = a; y = b; }
   public: void Mozgat(const Pont& p) { x = p.x; y = p.y; }
   public: void Kiir() const { cout<<"("<<x<<","<<y<<")\n"; }
};
III.2.2.2.3. A tagfüggvények tárolása külön modulban

Nagyobb méretű osztályok kezelését segíti, ha a tagfüggvényeket külön C++ modulban tároljuk. Ekkor az osztály deklarációjában az adattagok mellett a tagfüggvények prototípusát szerepeltetjük. Az osztály leírását tartalmazó fejállomány (.H), és a tagfüggvények definícióját tároló modul (.CPP) neve általában megegyezik, és utal az osztályra.

A Pont osztály leírása a Pont.h fejállományban az alábbiak szerint módosul:

#ifndef __PONT_H__
#define __PONT_H__
 class Pont {
   private:
      int x,y;
   public:
      Pont(int a = 0, int b = 0);
      int GetX() const;
      int GetY() const;
      void SetX(int a);
      void SetY(int a);
      void Mozgat(int a, int b);
      void Mozgat(const Pont& p);
      void Kiir() const;
 };
#endif

A tagfüggvények nevét az osztály nevével kell minősíteni (::) a Pont.cpp állományban:

#include <iostream>
using namespace std;
#include "Pont.h"
 
Pont::Pont(int a, int b) { 
   x = a; y = b;
}            
 
int Pont::GetX() const {
   return x; 
} 
 
int Pont::GetY() const {
   return y; 
} 
 
void Pont::SetX(int a) {
   x = a; 
}
 
void Pont::SetY(int a) {
   y = a; 
}
 
void Pont::Mozgat(int a, int b) { 
   x = a; y = b; 
}            
 
void Pont::Mozgat(const Pont& p) {
   x = p.x; y = p.y; 
}
 
void Pont::Kiir() const {
   cout<<"("<<x<<","<<y<<")\n"; 
}

Természetesen explicit módon is alkalmazhatjuk az inline előírást a tagfüggvényekre, azonban ekkor az inline tagfüggvények definícióját a C++ modulból a fejállományba kell áthelyeznünk:

#ifndef __PONT_H__
#define __PONT_H__
class Pont {
   private:
      int x,y;
   public:
      Pont(int a = 0, int b = 0);
      int GetX() const;
      int GetY() const;
      void SetX(int a);
      void SetY(int a);
      void Mozgat(int a, int b);
      void Mozgat(const Pont& p);
      inline void Kiir() const;
};
 
void Pont::Kiir() const {
   cout<<"("<<x<<","<<y<<")\n";
 }
 
#endif

III.2.2.3. Barát függvények és osztályok

Vannak esetek, amikor a C++ nyelv adatrejtés szabályai megakadályozzák, hogy hatékony programkódot készítsünk.  A friend (barát) mechanizmus azonban lehetővé teszi, hogy egy osztály private és protected tagjait az osztályon kívüli függvényekből is elérjük.

A friend deklarációt az osztály leírásán belül, tetszőleges elérésű részben elhelyezhetjük. A „barát” lehet egy külső függvény, egy másik osztály adott tagfüggvénye, de akár egy egész osztály is (vagyis annak minden tagfüggvénye). Ennek megfelelően a friend deklarációban a függvények prototípusát, illetve az osztály nevét szerepeltetjük a class szóval bevezetve.

Felhívjuk a figyelmet arra, hogy barátosztály esetén a „baráti viszony” nem kölcsönös, vagyis csak a friend deklarációban szereplő osztály tagfüggvényei kapnak korlátlan elérést a leírást tartalmazó osztály tagjaihoz.

Az alábbi példában szereplő COsztaly minden tagját korlátozás nélkül eléri a külső Osszegez() függvény, a BOsztaly Szamlal() nyilvános tagfüggvénye valamint az AOsztaly minden tagfüggvénye:

class AOsztaly;
 
class BOsztaly {
 public:
  int Szamlal(int x) { return x++; }
};
 
class COsztaly {
  friend long Osszegez(int a, int b);
  friend int BOsztaly::Szamlal(int x);
  friend class AOsztaly;
  // ...
};
 
long Osszegez(int a, int b) {
  return long(a) + b;
}

További példaként tekintsük a síkbeli pontok leírásához használható egyszerűsített Pont osztályunkat! Mivel a pontok távolságát számító művelet eredménye nem kapcsolható egyik ponthoz sem, így a távolság meghatározására külső függvényt készítünk, amely argumentumként kapja a két pontot. Az adattagok gyors eléréséhez azonban szükséges a közvetlen hozzáférés biztosítása, ami a „barát” mechanizmus révén meg is valósítható.

#include <iostream>
#include <cmath>
using namespace std;
 
class Pont {
      friend double Tavolsag(const Pont & p1,
                             const Pont & p2);
   private:
      int x,y;
   public:
      Pont(int a = 0, int b = 0) { x = a; y = b; }
      int GetX() const { return x; }
      int GetY() const { return y; }
      void SetX(int a) { x = a; }
      void SetY(int a) { y = a; }
      void Mozgat(int a, int b) { x = a; y = b; }
      void Mozgat(const Pont& p) { x = p.x; y = p.y; }
      void Kiir() const { cout<<"("<<x<<","<<y<<")\n"; }
};
 
double Tavolsag(const Pont & p1, const Pont & p2) {
  return sqrt(pow(p1.x-p2.x,2.0)+pow(p1.y-p2.y,2.0));
}
 
int main() {
  Pont p, q;
  p.Mozgat(1,2);
  q.Mozgat(4,6);
  cout<<Tavolsag(p,q)<<endl;
}

III.2.2.4. Mi szerepelhet még az osztályokban?

Az osztályokkal történő eddigi ismerkedésünk során adatokat és függvényeket helyeztünk el az osztályokban. A statikus tagok esetén ezt a sort a statikus konstansokkal és az enum típussal bővítettük.

Az alábbiakban először megnézzük, miként helyezhetünk el az osztályunkban konstanst és hivatkozást, ezt követően pedig röviden áttekintjük az ún. egymásba ágyazott osztályok használatával kapcsolatos szabályokat.

III.2.2.4.1. Objektumok konstans adattagjai

Vannak esetek, amikor az objektumpéldányokhoz valamilyen egyedi konstans értéket szeretnénk kapcsolni, például egy nevet, egy azonosítószámot. Erre van lehetőség, ha az adattagot const előtaggal látjuk el, és felvesszük a konstruktorok taginicializáló listájára.

A következő példában felhasználó objektumokat készítünk, és a felhasználók nyilvános nevét konstansként használjuk:

class Felhasznalo {
    string jelszo;
 public:
    const string nev;
    Felhasznalo(string user, string psw="") : nev(user) {
         jelszo=psw;
    }
    void SetJelszo(string newpsw) { jelszo = newpsw;}
};
 
int main() {
   Felhasznalo nata("Lafenita");
   Felhasznalo kertesz("Liza");
   nata.SetJelszo("Atinefal1223");
   kertesz.SetJelszo("Azil729");
   cout<<nata.nev<<endl;
   cout<<kertesz.nev<<endl;
   Felhasznalo alias = nata;
   // alias = kertesz;   // hiba!
}

Felhívjuk a figyelmet arra, hogy az azonos típusú objektumok közötti szokásos tagonkénti másolás nem működik, amennyiben az objektumok konstans tagokat tartalmaznak.

III.2.2.4.2. Hivatkozás típusú adattagok

Mivel hivatkozást csak már létező változóhoz készíthetünk, az objektum konstruálása során a konstansokhoz hasonló módon kell eljárnunk. Az alábbi példában referencia segítségével kapcsoljuk a vezérlő objektumhoz a jeladó objektumot:

class Jelado {
   private:
      int adat;
   public:
     Jelado(int x) { adat = x; }
     int Olvas() { return adat; }
};
 
class Vezerlo {
   private:
     Jelado& jelado;
   public:
     Vezerlo(Jelado& szenzor) : jelado(szenzor) {}
     void AdatotFogad() { cout<<jelado.Olvas(); }
};
 
int main() {
   Jelado sebesseg(0x17);
   Vezerlo ABS(sebesseg);
   ABS.AdatotFogad();
}
III.2.2.4.3. Adattag objektumok

Gyakran előfordul, hogy egy osztályban egy másik osztály objektumpéldányát helyezzük el adattagként. Fontos szabály, hogy az ilyen osztály objektumainak létrehozásakor a belső objektumok inicializálásáról is gondoskodni kell, amit a megfelelő konstruktorhívás taginicializáló listára való helyezésével érhetünk el.

A konstruktorhívástól eltekinthetünk, ha a tagobjektum osztálya rendelkezik paraméter nélküli (default) konstruktorral, ami automatikus is meghívódik.

A fenti vezérlő-jeladó példaprogramot úgy módosítjuk, hogy a jeladó objektumként jelenjen meg a vezérlő objektumban:

class Jelado {
   private:
      int adat;
   public:
     Jelado(int x) { adat = x; }
     int Olvas() { return adat; }
};
 
class Vezerlo {
   private:
     Jelado jelado;
   public:
     Vezerlo() : jelado(0x17) {}
     void AdatotFogad() { cout<<jelado.Olvas(); }
};
 
int main() {
   Vezerlo ABS;
   ABS.AdatotFogad();
}

III.2.2.5. Osztálytagokra mutató pointerek

C++-ban egy függvényre mutató pointer még akkor sem veheti fel valamely tagfüggvény címét, ha különben a típusuk és a paraméterlistájuk teljesen megegyezik. Ennek oka, hogy a (nem statikus) tagfüggvények az osztály példányain fejtik ki hatásukat. Ugyanez igaz az adattagokhoz rendelt mutatókra is. (További fontos eltérés a hagyományos mutatókhoz képest, hogy a virtuális tagfüggvények mutatón keresztüli hívása esetén is érvényesül a későbbiekben tárgyalt polimorfizmus.)

A mutatók helyes definiálásához az osztály nevét és a hatókör operátort is alkalmaznunk kell:

class Osztaly;

előrevetett osztálydeklaráció,

int Osztaly::*p;

p mutató egy int típusú adattagra,

void (Osztaly::*pfv)(int);

pfv egy olyan tagfüggvényre mutathat, amelyet int argumentummal hívunk, és nem ad vissza értéket.

A következő példában bemutatjuk az osztálytagokra mutató pointerek használatát, melynek során mindkét osztálytagot mutatók segítségével érjük el. Az ilyen mutatók alkalmazása esetén a tagokra a szokásos operátorok helyett a .* (pont csillag), illetve a ->* (nyíl csillag) operátorokkal hivatkozhatunk. Az adattagok és tagfüggvények címének lekérdezéséhez pedig egyaránt a címe (&) operátort kell használnunk.

#include <iostream>
using namespace std;
 
class Osztaly {
  public:
    int a;
    void f(int b) { a += b;}
};
 
int main() {
   // mutató az Osztaly int típusú adattagjára
   int Osztaly::*intptr = &Osztaly::a;
 
   // mutató az Osztaly void típusú, int paraméterű 
   // tagfüggvényére
   void (Osztaly::* fvptr)(int) = &Osztaly::f;
 
   // az objektupéldányok létrehozása
   Osztaly objektum;
   Osztaly * pobjektum = new Osztaly();
 
   // az a adattag elérése mutató segítségével
   objektum.*intptr = 10;
   pobjektum->*intptr = 100;
 
   // az f() tagfüggvény hívása pointer felhasználásával
   (objektum.*fvptr)(20);
   (pobjektum->*fvptr)(200);
 
   cout << objektum.a << endl;     // 30
   cout << pobjektum->a << endl;    // 300
   delete pobjektum;
}

A typedef alkalmazásával egyszerűbbé tehetjük a pointeres kifejezésekkel való munkát:

typedef int Osztaly::*mutato_int;
 
typedef void (Osztaly::*mutato_fv)(int);
 
…
mutato_int intptr = &Osztaly::a;
 
mutato_fv fvptr = &Osztaly::f;

III.2.3. Operátorok túlterhelése (operator overloading)

Az eddigiek során az osztályhoz tartozó műveleteket tagfüggvények formájában valósítottuk meg. A műveletek elvégzése pedig tagfüggvények hívását jelentette. Sokkal olvashatóbb programokhoz juthatunk, ha a függvényhívások helyett valamilyen hasonló tartalmú műveleti jelet alkalmazhatunk.

A C++ nyelv biztosítja annak a lehetőségét, hogy valamely, programozó által definiált függvényt szabványos operátorhoz kapcsoljunk, kibővítve ezzel az operátor működését. Ez a függvény automatikusan meghívódik, amikor az operátort egy meghatározott szövegkörnyezetben használjuk.

Operátorfüggvényt azonban csak akkor készíthetünk, ha annak legalább egyik paramétere osztály (class, struct) típusú. Ez azt jelenti, hogy a paraméter nélküli függvények, illetve a csak alap adattípusú argumentumokat használó függvények nem lehetnek operátorfüggvények. Az operátorfüggvény deklarációjának formája:

        típus operator op(paraméterlista);

ahol az op helyén az alábbi C++ operátorok valamelyike állhat:

[]

()

.

->

++

--

&

new

*

+

-

~

!

/

%

new[]

<<

>>

<

>

<=

>=

==

delete

!=

^

|

&&

||

=

*=

delete[]

/=

%=

+=

-=

<<=

>>=

&=

 

^=

|=

,

->*

       

Az operátorfüggvény típus-átalakítás esetén az alábbi alakot ölti:

        operator típus();

Nem definiálhatók át a tagkiválasztás (.), az indirekt tagkiválasztás (.*), a hatókör (::) , a feltételes (?:), a sizeof és a typeid operátorok, mivel ezek túlterhelése nemkívánatos mellékhatásokkal járna.

Az értékadó (=), a címe (&) és a vessző (,) műveletek túlterhelés nélkül is alkalmazhatók az objektumokra.

Felhívjuk a figyelmet arra, hogy az operátorok túlterhelésével nem változtatható meg az operátorok elsőbbsége (precedenciája) és csoportosítása (asszociativitása), valamint nincs mód új műveletek bevezetésére sem.

III.2.3.1. Operátorfüggvények készítése

Az operátorok túlterhelését megvalósító operátorfüggvények kialakítása nagymértékben függ a kiválasztott műveleti jeltől. Az alábbi táblázatban összefoglaltuk a lehetőségeket. Az operátorfüggvények többségének visszatérési értéke és típusa szabadon megadható.

Kifejezés

Operátor( )

Tagfüggvény

Külső függvény

♣a

+ - * & ! ~

++ --

A::operator ()

operator (A)

a♣

++ --

A::operator (int)

operator (A, int)

a♣b

+ - * / % ^ &

| < > == != <=

>= << >> && || ,

A::operator (B)

operator (A, B)

a♣b

= += -= *= /=

%= ^= &= |= <<=

>>= []

A::operator (B)

-

a(b, c...)

()

A::operator()(B, C...)

-

a->b

->

A::operator->()

-

Az operátorfüggvényeket általában osztályon belül adjuk meg, a felhasználói típus lehetőségeinek kiterjesztése céljából. Az =, (), [] és -> operátorokat azonban csak nem statikus tagfüggvénnyel lehet átdefiniálni. A new és a delete operátorok esetén a túlterhelés statikus tagfüggvénnyel történik. Minden más operátorfüggvény elkészíthető tagfüggvényként vagy külső (általában friend) függvényként.

Az elmondottakat jól szemlélteti a fenti táblázat, ahol csoportokba rendeztük a C++ nyelv átdefiniálható műveleteit, a dinamikus memóriakezelés operátorainak elhagyásával. A táblázatban a ♣ karakter a műveleti jelet helyettesíti, míg az a, b és c valamely A, B és C osztály objektumai.

A túlterhelhető C++ műveleti jelek többségét az operandusok száma alapján két csoportra oszthatjuk. Erre a két esetre az alábbi táblázatban összefoglaltuk az operátorfüggvények hívásának formáit.

Kétoperandusú operátorok esetén:

Megvalósítás

Szintaxis

Aktuális hívás

tagfüggvény

X op Y

X.operator op(Y)

külső függvény

X op Y

operator op(X,Y)

Egyoperandusú operátorok esetén:

Megvalósítás

Szintaxis

Aktuális hívás

tagfüggvény

op X

X.operator op()

tagfüggvény

X op

X.operator op(0)

külső függvény

op X

operator op(X)

külső függvény

X op

operator op(X,0)

Bizonyos műveleteket átdefiniálása során a szokásostól eltérő megfontolásokra is szükség van. Ezen operátorokat a fejezet további részeiben ismertetjük.

Példaként tekintsük az egész számok tárolására alkalmas Vektor osztályt, amelyben túlterheltük az indexelés ([]), az értékadás (=) és az összeadás (+, +=) műveleteit! Az értékadás megvalósítására a tömb elemeinek másolása érdekében volt szükség. A + operátort barátfüggvénnyel valósítjuk meg, mivel a keletkező vektor logikailag egyik operandushoz sem tartozik. Ezzel szemben a += művelet megvalósításához tagfüggvényt használunk, hiszen a művelet során a bal oldali operandus elemei módosulnak. Az osztály teljes deklarációját (inline függvényekkel) a Vektor.h állomány tartalmazza.

#ifndef __VektorH__
#define __VektorH__
 
class Vektor {
inline friend Vektor  operator+ (const Vektor& v1, 
                                 const Vektor& v2);
  private:
    int meret, *p;
  public:
    // ----- Konstruktorok -----
    // Adott méretű vektor inicilaizálása
    Vektor(int n=10) {
        p = new int[meret=n];
        for (int i = 0; i < meret; ++i)  
            p[i] = 0;       // az elemek nullázása
    }
    // Inicializálás másik vektorral - másoló konstruktor
    Vektor(const Vektor& v) {
         p = new int[meret=v.meret];
         for (int i = 0; i < meret; ++i) 
            p[i] = v.p[i];   // az elemek átmásolása
    }
    // Inicializálás hagyományos n-elemu vektorral
    Vektor(const int a[], int n) {
        p = new int[meret=n];
        for (int i = 0; i < meret; ++i)
           p[i] = a[i];
    }
 
    // ----- Destruktor -----
    ~Vektor() {delete[] p; }
 
    // ----- Meretlekérdező tagfüggvény -----
    int GetMeret() const { return meret; } 
 
    // ----- Operátorfüggvények -----
    int& operator [] (int i) {
        if (i < 0 || i > meret-1)  // indexhatár-ellenőrzés
            throw i;
        return p[i];
    }
 
    const int& operator [] (int i) const {
        return p[i];
    }
 
    Vektor operator = (const Vektor& v) {
        delete[] p;
        p=new int [meret=v.meret];
        for (int i = 0; i < meret; ++i)
           p[i] = v.p[i];
        return *this;
    }
 
    Vektor operator += (const Vektor& v) {
        int m = (meret < v.meret) ? meret : v.meret;
        for (int i = 0; i < m; ++i)
           p[i] += v.p[i];
        return *this;
    }
};
 
// ----- Külső függvény  -----
inline Vektor operator+(const Vektor& v1, const Vektor& v2) {
    Vektor osszeg(v1);
    osszeg+=v2;
    return osszeg;
}
#endif

A példaprogram megértéséhez néhány megjegyzést kell fűznünk a programkódhoz.

  • Az indexelés műveletéhez két operátorfüggvény is készítettünk, a másodikat a konstans vektorokkal használja a fordító. A két operator[]() függvény egymás túlterhelt változatai, bár a paramétersoruk azonos. Ez azért lehetséges, mivel a C++ fordító a függvény const voltát is eltárolja a függvény lenyomatában.

  • A this pointer az objektumra mutat, azonban a *this kifejezés magát az objektumot jelenti. Azok a Vektor típusú függvények, amelyek a *this értékkel térnek vissza, valójában az aktuális objektum másolatát adják függvényértékül. (Megjegyezzük, hogy Vektor& típusú függvények a return *this; utasítás hatására az aktuális objektum hivatkozását szolgáltatják.)

A Vektor osztály felhasználását az alábbi programrészlet szemlélteti:

#include <iostream>
using namespace std;
#include "Vektor.h"
 
void show(const Vektor& v) {
  for (int i=0; i<v.GetMeret(); i++)
    cout<<v[i]<<'\t';
  cout<<endl;
}
 
int main() {
    int a[5]={7, 12}, b[7]={2, 7, 12, 23, 29};
    Vektor x(a,5);     // x:  7  12   0   0  0
    Vektor y(b,7);     // y:  2   7  12  23 29  0  0
    Vektor z;          // z:  0   0   0   0  0  0  0  0  0  0
    try {
       x = y;          // x:  2   7  12  23 29  0  0
       x = Vektor(a,5);// x:  7  12   0   0  0
       x += y;         // x:  9  19  12  23 29
       z = x + y;      // z: 11  26  24  46 58
       z[0] = 102;
       show(z);        // z:102  26  24  46 58
    }
    catch (int n) {
        cout<<"Hibas tombindex: "<<n<<endl;
    }
    const Vektor v(z);
    show(v);            // v:102  26  24  46 58
    // v[0] = v[1]+5;   // error: assignment of read-only…
}

III.2.3.2. Típus-átalakító operátorfüggvények használata

A C++ nyelv támogatja, hogy osztályainkhoz típuskonverziókat rendeljünk. A felhasználó által definiált típus-átalakítást végző operátorfüggvény deklarációja:

        operator típus();

A függvény visszatérési értékének típusa megegyezik a függvény nevében szereplő típussal. A típuskonverziós operátorfüggvény csak visszatérési típus és argumentumlista nélküli tagfüggvény lehet.

Az alábbi példában a Komplex típust osztályként valósítjuk meg. Az egyetlen nem Komplex típusú argumentummal rendelkező konstruktor elvégzi a más típusról – a példában doubleKomplex típusra történő konverziót. A fordított irányú átalakításhoz double nevű konverziós operátort készítünk.

#include <cmath>
#include <iostream>
using namespace std;
 
class Komplex {
  public:
    Komplex () { re=im=0; }
    Komplex(double a) : re(a), im(0) { }
    // konverziós konstruktor
    Komplex(double a, double b) : re(a), im(b) { }
    // konverziós operátor
    operator double() {return sqrt(re*re+im*im);}
 
    Komplex operator *= (const Komplex & k) {
       Komplex t;
       t.re=(k.re*re)-(k.im*im);
       t.im=(k.re*im)+(re*k.im);
       return *this = t;
    }
    void Kiir() const { cout << re << "+" << im << "i"; }
  private:
    double re, im;
  friend Komplex operator*(const Komplex&, const Komplex&);
};
 
Komplex operator*(const Komplex& k1, const Komplex& k2) {
   Komplex k=k1;
   k*= k2;
   return k;
}
 
int main() {
   Komplex k1(7), k2(3,4), k3(k2);
   cout << double(k3)<< endl;           // a kiírt érték:  5
   cout <<double(Komplex(10))<< endl;   // a kiírt érték: 10
   Komplex x(2,-1), y(3,4);
   x*=y;
   x.Kiir();                            // 10+5i
}

Felhívjuk a figyelmet, hogy a Komplex osztály három konstruktora egyetlen konstruktorral helyettesíthető, amelyben alapértelmezett argumentumokat használunk:

        Komplex(double a=0, double b=0) : re(a), im(b) {}

III.2.3.3. Az osztályok bővítése input/output műveletekkel

A C++ nyelv lehetővé teszi, hogy az osztályokon alapuló I/O adatfolyamoknak „megtanítsuk” a saját készítésű osztályok objektumainak kezelését. Az adatfolyam osztályok közül az istream az adatbevitelért, míg az ostream az adatkivitelért felelős.

Az input/output műveletek végzéséhez a >> és a << operátorok túlterhelt változatait használjuk. A szükséges működés eléréséhez friend operátorfüggvényként kell elkészítenünk a fenti műveletek saját változatait, mint ahogy ez a Komplex osztály bővített változatában látható:

#include <cmath>
#include <sstream>
#include <iostream>
using namespace std;
 
class Komplex {
  public:
    Komplex(double a=0, double b=0) : re(a), im(b) {}
    operator double() {return sqrt(re*re+im*im);}
 
    Komplex operator *= (const Komplex & k) {
       Komplex t;
       t.re=(k.re*re)-(k.im*im);
       t.im=(k.re*im)+(re*k.im);
       return *this = t;
    }
  private:
    double re, im;
  friend Komplex operator*(const Komplex&, const Komplex&);
  friend istream & operator>>(istream &, Komplex &);
  friend ostream & operator<<(ostream &, const Komplex &);
};
 
Komplex operator*(const Komplex& k1, const Komplex& k2) {
   Komplex k=k1;
   k*= k2;
   return k;
}
 
// Az adatbevitel formátuma: 12.23+7.29i, illetve 12.23-7.29i
istream & operator>>(istream & is, Komplex & c) {
   string s;
   getline(is, s);
   stringstream ss(s);
   if (!(ss>>c.re>>c.im))
      c=Komplex(0);
   return is;
 }
 
// Adatkiviteli formátum: 12.23+7.29i, illetve 12.23-7.29i
ostream & operator<<(ostream & os, const Komplex & c) {
   os<<c.re<<(c.im<0? '-' : '+')<<fabs(c.im)<<'i';
   return os;
 }
 
int main() {
   Komplex a, b;
   cout<<"Kerek egy komlex szamot: ";  cin >> a;
   cout<<"Kerek egy komlex szamot: ";  cin >> b;
   cout<<"A komplex szamok szorzata: " << a*b <<endl;
}

III.3. Öröklés (származtatás)

Az előző fejezetekben megismertük, hogyan tudunk feladatokat egymástól független osztályokkal megoldani. Az objektum-orientált programozás több lehetőséget kínál. A problémák objektum-orientált feldolgozása során általában egy új programépítési módszert, a származtatást (öröklés, inheritance) alkalmazzuk. A származtatás lehetővé teszi, hogy már meglévő osztályok adatait és műveleteit új megközelítésben alkalmazzuk, illetve a feladat igényeinek megfelelően módosítsuk, bővítsük. A problémákat így nem egyetlen (nagy) osztállyal, hanem osztályok egymásra épülő rendszerével (általában hierarchiájával) oldjuk meg.

Az öröklés az objektum-orientált C++ nyelv egyik legfőbb sajátossága. Ez a mechanizmus lehetővé teszi, hogy meglévő osztály(ok)ból kiindulva, új osztályt hozzunk létre (származtassunk). A származtatás során az új osztály örökli a meglévő osztály(ok) nyilvános (public) és védett (protected) tulajdonságait (adattagjait) és viselkedését (tagfüggvényeit), amelyeket aztán a annak sajátjaként használhatunk. Azonban az új osztállyal bővíthetjük is a meglévő osztály(oka)t, új adattagokat és tagfüggvényeket definiálhatunk, illetve újraértelmezhetjük (lecserélhetjük) az öröklött, de működésükben elavult tagfüggvényeket (polimorfizmus, polymorphism).

A szakirodalom örökléssel kapcsolatos szóhasználata igen változatos, ezért röviden összefoglaljuk az magyar és angol nyelvű kifejezéseket, aláhúzással kiemelve a C++-ban alkalmazottakat.

A osztály, amiből származtatunk:

alaposztály, ősosztály, szülőosztály ( base class , ancestor class , parent class, superclass)

a művelet:

öröklés, származtatás, bővítés ( inheritance , derivation , extending, subclassing)

B osztály, a származtatás eredménye:

utódosztály, származtatott osztály, bővített osztály, gyermekosztály, alosztály ( descendant class , derived class , extended class, child class, subclass)

A fenti kapcsolatot megvalósító C++ programrészlet:

class AOsztaly {
    // ...
};
 
class BOsztaly : public  AOsztaly {
    // ...
};

A szakirodalomban nem egyértelmű a fogalmak használata, például egy adott osztály alaposztálya vagy ősosztálya tetszőleges elődöt jelölhet, illetve az utódosztály vagy származtatott osztály minősítés bármely örökléssel létrehozott osztály esetén alkalmazható. Könyvünkben ezeket a fogalmakat a közvetlen ős, illetve közvetlen utód értelemben használjuk.

A C++ többszörös örölésű I/O osztályai
III.5. ábra - A C++ többszörös örölésű I/O osztályai


A C++ támogatja a többszörös öröklődést (multiple inheritance), melynek során valamely új osztályt több alaposztályból (közvetlen őstől) származtatunk (III.5. ábra). A többszörös örökléssel kialakított osztályszerkezet hálós szerkezetű, melynek értelmezése és kezelése is nehézségekbe ütközik. Ezért ezt a megoldást igen korlátozott módon használjuk, helyette - az esetek nagy többségében - az egyszeres öröklést (single inheritance) alkalmazzuk. Ebben az esetben valamely osztálynak legfeljebb egy közvetlen őse, és tetszőleges számú utódja lehet. Az öröklés több lépésben való alkalmazásával igazi fastruktúra (osztály-hierarchia) alakítható ki (III.6. ábra).

Geometriai osztályok hierarchiája
III.6. ábra - Geometriai osztályok hierarchiája


III.3.1. Osztályok származtatása

A származtatott osztály (utódosztály) olyan osztály, amely az adattagjait és a tagfüggvényeit egy vagy több előzőleg definiált osztálytól örökli. Azt az osztályt, amelytől a származtatott osztály örököl, alaposztálynak (ősosztály) nevezzük. A származtatott osztály szintén lehet alaposztálya további osztályoknak, lehetővé téve ezzel osztályhierarchia kialakítását.

A származtatott osztály az alaposztály minden tagját örökli, azonban az alaposztályból csak a public és protected (védett) tagokat éri el sajátjaként. A védett elérés kettős viselkedést takar. Privát hozzáférést jelent az adott osztály felhasználójának, aki objektumokat hoz létre vele, azonban nyilvános elérést biztosít az osztály továbbfejlesztőjének, aki új osztályt származtat belőle. A tagfüggvényeket általában public vagy protected hozzáféréssel adjuk meg, míg az adattagok esetén a protected vagy a private elérést alkalmazzuk. (A privát hozzáféréssel a származtatott osztály tagfüggvényei elől is elrejtjük a tagokat.) A származtatott osztályban az öröklött tagokat saját adattagokkal és tagfüggvényekkel is kiegészíthetjük.

A származtatás kijelölésére az osztály fejét használjuk, ahol az alaposztályok előtt megadhatjuk a származtatás módját (public, protected, private):

class Szarmaztatott : public Alap1, ...private AlapN
{
 // az osztály törzse
};

Az alaposztálybeli elérhetőségüktől függetlenül nem öröklődnek a konstruktorok, a destruktor, az értékadó operátor valamint a friend viszonyok. Az esetek többségében nyilvános (public) öröklést használunk, mivel ekkor az utódobjektum minden kontextusban helyettesítheti az ősobjektumot.

Az alábbi – síkbeli és térbeli pontokat definiáló – példában bemutatjuk a származtatás alkalmazását. A kialakított osztály-hierarchia igen egyszerű (a piros nyíl a közvetlen alaposztályra mutat):

class Pont2D {
   protected:
      int x,y;
   public:
      Pont2D(int a = 0, int b = 0) { x = a; y = b; }
      void GetPont2D(int& a, int& b) const { a=x; b=y;}
      void Mozgat(int a=0, int b=0) { x = a; y = b; }
      void Mozgat(const Pont2D& p) { x = p.x; y = p.y; }
      void Kiir() const { cout<<'('<<x<<','<<y<<')'<<endl; }
};
 
class Pont3D : public Pont2D {
   protected:
      int z;
   public:
      Pont3D(int a=0, int b=0, int c=0):Pont2D(a,b),z(c) {}
      Pont3D(Pont3D & p):Pont2D(p.x, p.y),z(p.z) {}
      void GetPont3D(int& a, int& b, int& c) const { 
           a = x; b = y; c = z; }
      void Mozgat(int a=0, int b=0, int c=0) { 
           x = a; y = b; z = c; }
      void Mozgat(const Pont3D& p) { 
           Pont2D::x = p.x; y = p.y; z = p.z;}
      void Kiir() const {
           cout<<'('<<x<<','<<y<<','<<z<<')'<<endl;}
};
 
void Megjelenit(const Pont2D & p) {
   p.Kiir();
}
 
int main() {
  Pont2D p1(12,23), p2(p1), p3;
  Pont3D q1(7,29,80), q2(q1), q3;
  p1.Kiir();            // (12,23)
  q1.Kiir();            // (7,29,80)
  // q1 = p1;            // ↯ - hiba!
  q2.Mozgat(10,2,4);
  p2 = q2;
  p2.Kiir();            // (10,2)
  q2.Kiir();            // (10,2,4)
  q2.Pont2D::Kiir();        // (10,2)
  Megjelenit(p2);        // (10,2)
  Megjelenit(q2);        // (10,2)
}

A példában kék színnel kiemeltük az öröklés következtében alkalmazott programelemeket, melyekkel a fejezet további részeiben foglalkozunk.

Láthatjuk, hogy a public származtatással létrehozott osztály objektuma minden esetben (értékadás, függvényargumentum,...) helyettesítheti az alaposztály objektumát:

  p2 = q2;
  Megjelenit(q2);

Ennek oka, hogy az öröklés során a származtatott osztály teljesen magában foglalja az alaposztályt. Fordítva azonban ez nem igaz, így az alábbi értékadás fordítási hibához vezet:

  q1 = p1;    // ↯ 

A származtatási listában megadott public, protected és private kulcsszavak az öröklött (nyilvános és védett) tagok új osztálybeli elérhetőségét szabályozzák, az alábbi táblázatban összefoglalt módon.

Az öröklés módja

Alaposztálybeli elérés

Hozzáférés a származtatott osztályban

public

public

protected

public

protected

protected

public

protected

protected

protected

private

public

protected

private

private

A public származtatás során az öröklött tagok megtartják az alaposztálybeli elérhetőségüket, míg a private származtatás során az öröklött tagok a származtatott osztály privát tagjaivá válnak, így elzárjuk azokat mind az új osztály felhasználói, mind pedig a továbbfejlesztői elől. Védett (protected) öröklés esetén az öröklött tagok védettek lesznek az új osztályban, így további öröklésük biztosított marad. (A class típusú alaposztályok esetén a privát, míg a struct típust használva a public az alapértelmezés szerinti származtatási mód.)

Ez az automatizmus az esetek nagy többségében megfelelő eredményt szolgáltat, és a származtatott osztályaink öröklött tagjaihoz megfelelő elérést biztosít. Szükség esetén azonban közvetlenül is beállíthatjuk bármely öröklött (az alaposztályban védett és nyilvános hozzáférésű) tag elérését. Ehhez a tagok alaposztállyal minősített nevét egyszerűen bele kell helyeznünk a megfelelő hozzáférésű csoportba. Arra azonban ügyelni kell, hogy az új elérhetőség nem adhat több hozzáférést, mint amilyen az ősosztályban volt. Például, ha egy ősbeli protected tagot privát módon öröklünk, az automatikusan private elérésű lesz a származtatott osztályban, mi azonban a védett csoportba is áthelyezhetjük (de a nyilvánosba nem!).

Példaként származtassuk privát örökléssel a Pont3D osztályt, azonban ennek ellenére alakítsunk ki hasonló elérhetőséget, mint amilyen a nyilvános származtatás esetén volt!

class Pont3D : private Pont2D {
   protected:
      int z;
      Pont2D::x;
      Pont2D::y;
   public:
      Pont3D(int a=0, int b=0, int c=0):Pont2D(a,b),z(c) {}
      Pont3D(Pont3D & p):Pont2D(p.x, p.y),z(p.z) {}
      void GetPont3D(int& a, int& b, int& c) const {
           a = x; b = y; c = z; }
      void Mozgat(int a=0, int b=0, int c=0) {
           x = a; y = b; z = c; }
      void Mozgat(const Pont3D& p) {
           x = p.x; y = p.y; z = p.z;}
      void Kiir() const {
           cout<<'('<<x<<','<<y<<','<<z<<')'<<endl;}
      Pont2D:: GetPont2D;
};

III.3.2. Az alaposztály(ok) inicializálása

Az alaposztály(ok) inicializálására a taginicializáló lista kibővített változatát használjuk, amelyben a tagok mellett a közvetlen ősök konstruktorhívásait is felsoroljuk.

class Pont3D : public Pont2D {
   protected:
      int z;
   public:
      Pont3D(int a=0, int b=0, int c=0):Pont2D(a,b),z(c) {}
      Pont3D(Pont3D & p):Pont2D(p.x, p.y),z(p.z) {}
      // …
};

A származtatott osztály példányosítása során a fordító az alábbi sorrendben hívja a konstruktorokat:

  • Végrehajtódnak az alaposztályok konstruktorai az származtatási lista szerinti sorrendben.

  • Meghívódnak a származtatott osztály tagobjektumainak konstruktorai, az objektumtagok megadásának sorrendjében (a példában nem szerepelnek).

  • Lefut a származtatott osztály konstruktora.

Az ősosztály konstruktorhívása elhagyható, amennyiben az alaposztály rendelkezik paraméter nélküli konstruktorral, amit ebben az esetben a fordító automatikusan meghív. Mivel példánkban ez a feltétel teljesül, a második konstruktort az alábbi formában is megvalósíthatjuk:

      Pont3D(Pont3D & p) { *this = p;}

(A megoldás további feltétele a nyilvános öröklés, ami szintén teljesül.)

Osztályhierarchia fejlesztése során elegendő, ha minden osztály csupán a közvetlen őse(i) inicializálásáról gondoskodik. Ezáltal egy magasabb szinten (a gyökértől távolabb) található osztály példányának minden része automatikusan kezdőértéket kap, amikor az objektum létrejön.

A származtatott objektumpéldány megszűnésekor a destruktorok a fentiekkel ellentétes sorrendben hajtódnak végre.

  • Lefut a származtatott osztály destruktora.

  • Meghívódnak a származtatott osztály tagobjektumainak destruktorai, az objektumtagok megadásával ellentétes sorrendben

  • Végrehajtódnak az alaposztályok destruktorai, a származtatási lista osztálysorrendjével ellentétes sorrendben.

III.3.3. Az osztálytagok elérése öröklés esetén

Az III.2. szakasz két csoportba soroltuk az osztályok tagjait az elérhetőség szempontjából: elérhető, nem érhető el. Ezt a két csoportot az öröklött és nem öröklött kategóriák csak tovább árnyalják. Az osztályok származtatását bemutató fejezetben (III.3.1. szakasz) megismerkedtünk az alapvető elérési mechanizmusok működésével. Most csupán áttekintünk néhány további megoldást, amelyek pontosítják az eddigi képünket az öröklésről.

III.3.3.1. Az öröklött tagok elérése

A származtatott osztály öröklött tagjai általában ugyanúgy érhetők el, mint a saját tagok. Ha azonban a származtatott osztályban az öröklött tag nevével azonos néven hozunk létre adattagot vagy tagfüggvényt, akkor az elfedi az ősosztály tagját. Ilyen esetekben a hatókör operátort kell használnunk a hivatkozáshoz:

        Osztálynév::tagnév

A fordítóprogram a tagneveket az osztály hatókörrel együtt azonosítja, így minden tagnév felírható a fenti formában. A III.3.1. szakasz példaprogramjában látunk példákat az elmondottakra.

class Pont3D : public Pont2D {
   protected:
      int z;
   public:
      Pont3D(int a=0, int b=0, int c=0):Pont2D(a,b),z(c) {}
      Pont3D(Pont3D & p):Pont2D(p.x, p.y),z(p.z) {}
      // …
      void Mozgat(const Pont3D& p) { 
           Pont2D::x = p.x; y = p.y; z = p.z;}
      // …
};
int main() {
  Pont2D p1(12,23), p2(p1), p3;
  Pont3D q1(7,29,80), q2(q1), q3;
  q2.Pont2D::Kiir();
  q2.Pont2D::Mozgat(1,2); // Mozgatás a x-y síkban
  q2.Pont2D::Mozgat(p1);
  // …
}

Az alábbi táblázatban összefoglaltuk, hogy az említett példaprogram osztályai, milyen (általuk elérhető) tagokkal rendelkeznek. Elfedés esetén a tagok osztálynévvel minősített változatát adtuk meg:

A Pont2D alaposztály tagjai:

A Pont3D származtatott osztály tagjai

protected: x, y

public: Pont2D(),

GetPont2D(), Mozgat(int…), Mozgat(const…), Kiir()

protected: x, y, z

public: Pont3D(int…), Pont3D(Pont3D&…),

GetPont2D(),Pont2D()::Mozgat(int…), Pont2D()::Mozgat(const…), Pont2D()::Kiir(), GetPont3D(), Mozgat(int…), Mozgat(const…), Kiir()

III.3.3.2. A friend viszony az öröklés során

Az alaposztály „barátja” (friend) a származtatott osztályban csak az alaposztályból öröklött tagokat érheti el. A származtatott osztály „barátja” az alaposztályból csak a nyilvános és a védett tagokat érheti el.

III.3.4. Virtuális alaposztályok a többszörös öröklésnél

A többszörös öröklés során problémát jelenthet, ha ugyanazon alaposztály több példányban jelenik meg a származtatott osztályban. A virtuális alaposztályok használatával az ilyen jellegű problémák kiküszöbölhetők (III.7. ábra).

class Alap {
    int q;
  public:
     Alap(int v=0) : q(v) {};
     int GetQ() { return q;}
     void SetQ(int q) { this->q = q;}
};
 
// az Alap virtuális alaposztály
class Alap1 : virtual public Alap {
    int x;
 public:
    Alap1(int i): x(i) {}
};
 
// az Alap virtuális alaposztály
class Alap2: public virtual Alap {
    int y;
  public:
    Alap2(int i): y(i) {}
};
 
class Utod:  public Alap1,  public Alap2 {
    int a,b;
  public:
    Utod(int i=0,int j=0): Alap1(i+j),Alap2(j*i),a(i),b(j) {}
};
int main() {
   Utod utod;
   utod.Alap1::SetQ(100);
   cout << utod.GetQ()<<endl;          // 100
   cout << utod.Alap1::GetQ()<<endl;   // 100
   cout << utod.Alap2::GetQ()<<endl;   // 100
   utod.Alap1::SetQ(200);
   cout << utod.GetQ()<<endl;          // 200
   cout << utod.Alap1::GetQ()<<endl;   // 200
   cout << utod.Alap2::GetQ()<<endl;   // 200
}

Virtuális alaposztályok alkalmazása
III.7. ábra - Virtuális alaposztályok alkalmazása


A virtuális alaposztály az öröklés során csak egyetlen példányban lesz jelen a származtatott osztályokban, függetlenül attól, hogy hányszor fordul elő az öröklődési láncban. A példában a virtuális alaposztály q adattagját egyaránt öröklik az Alap1 és az Alap2 alaposztályok. A virtualitás miatt az Alap osztály egyetlen példányban szerepel, így az Alap1::q és az Alap2::q ugyanarra az adattagra hivatkoznak. A virtual szó használata nélkül az Alap1::q és az Alap2::q különböző adattagokat jelölnek, ami fordítási hibához vezet, mivel fordító számára nem lesz egyértelmű az utod.GetQ() hivatkozás feloldása.

III.3.5. Öröklés és/vagy kompozíció?

A C++ programozási nyelv egyik nagy előnye a programkód újrafelhasználásának támogatása. Az újrafelhasználás azt jelenti, hogy az eredeti programkód módosítása nélkül készítünk új programkódot. C++ nyelv objektum-orientált eszközeit használva három megközelítés közül választhatunk:

  1. Egy adott osztályban tárolt kód legegyszerűbb és leggyakoribb újrahasznosítása, amikor objektumpéldányt hozunk létre, vagy már létező objektumokat ( cin , cout , string , STL stb.) használunk a programunkban.

    class X {
       // …
    };
          
    int main() {
       X a, *pb;
       pb = new X();
       cout<<"C++"<<endl;
       // …
       delete pb;
    }
    
  2. További lehetőséghez jutunk, ha a saját osztályunkban más osztályok objektumait helyezzük el tagobjektumként. Mivel ekkor az új osztályt már meglévő osztályok felhasználásával állítjuk össze, ezt a módszert kompozíciónak nevezzük. Kompozíció során az új és a beépített objektumok között egy tartalmazás (has-a) kapcsolat alakul ki. Amennyiben az új objektumba csupán más objektumok mutatója vagy hivatkozása kerül aggregációról beszélünk.

    class X {
       // …
    };
     
    class Y {
      X x;    // kompozíció
    };    
     
    class Z {
      X& x;  // aggregáció
      X *px;
    };
    
  3. Harmadik megoldás a fejezetünk témájával kapcsolatos. Amikor az új osztályunkat nyilvános származtatással hozzuk létre más osztályokból, akkor egy általánosítás/pontosítás (is-a) kapcsolat jön létre. Ez a kapcsolat azt jelenti, hogy a származtatott objektum minden szempontból úgy viselkedik, mint egy ősobjektum (azaz a leszármazott objektum egyben mindig ősobjektum is) - ez azonban fordítva nem áll fenn.

    // alaposztály
    class X {
       // …
    };
     
    // származtatott osztály
    class Y : public X {
       // …
    };
    

Valamely probléma objektum-orientált megoldása során mérlegelni kell, hogy az öröklés vagy a kompozíció segítségével jutunk-e pontosabb modellhez. A döntés általában nem egyszerű, de néhány szempont megfogalmazható:

  • Kezdetben ajánlott kiindulni a kompozícióból, majd ha kiderül, hogy az új osztály valójában egy speciális típusa egy másik osztálynak, akkor jöhet az öröklés.

  • Származtatásnak elsődlegesen akkor van létjogosultsága, ha szükséges az ősosztályra való típus-átalakítás. Ilyen eset például, ha egy geometriai rendszer összes elemét láncolt listában szeretnénk tárolni.

  • A túl sok származtatás, a mély osztály-hierarchia nehezen karbantartható kódot eredményezhet. Ezért egy nagyobb feladat megoldása során ajánlott az kompozíciót és az öröklést szerves egységben alkalmazni.

Az alábbi két példaprogramból jól láthatók a kompozíció és a származtatás közötti különbségek, illetve azonosságok. Minkét esetben a Pont.h állományban tárolt Pont osztályt hasznosítjuk újra.

#ifndef __PONT_H__
#define __PONT_H__
class Pont {
   protected:
      int x,y;
   public:
      Pont(int a = 0, int b = 0) : x(a), y(b) {}
      Pont(const Pont& p) : x(p.x), y(p.y) {}
      int GetX() const { return x; }
      int GetY() const { return y; }
      void SetX(int a) { x = a; }
      void SetY(int a) { y = a; }
      void Mozgat(int a, int b) { x = a; y = b; }
      void Mozgat(const Pont& p) { x = p.x; y = p.y; }
      void Kiir() const { cout<<'('<<x<<','<<y<<')'<< endl; }
};
#endif

III.3.5.1. Újrahasznosítás kompozícióval

A való életben az összetett objektumok gyakran kisebb, egyszerűbb objektumokból épülnek fel. Például, egy gépkocsi fémvázból, motorból, néhány abroncsból, sebességváltóból, kormányozható kerekekből és nagyszámú más részegységből állítható össze. Elvégezve az összeállítást mondhatjuk, hogy a kocsinak van egy (has-a) motorja, van egy sebességváltója stb.

A Kor példánkban a körnek van egy középpontja, amit a Pont típusú p tagobjektum tárol. Fontos megjegyeznünk, hogy a Kor osztály tagfüggvényiből csak a Pont osztály nyilvános tagjait érhetjük el.

#include "Pont.h"
 
class Kor {
  protected:
    int r;
  public:
    Pont p; // kompozció
    Kor(int x=0, int y=0, int r=0)
        : p(x, y), r(r) {}
    Kor(const Pont& p, int r=0)
        : p(p), r(r) {}
    int GetR() {return r;}
    void SetR(int a) { r = a; }
};
 
int main() {
    Kor k1(100, 200, 10);
    k1.p.Kiir();
    cout<<k1.p.GetX()<<endl;
    cout<<k1.GetR()<<endl;
}

III.3.5.2. Újrahasznosítás nyilvános örökléssel

A kompozícióhoz hasonlóan az öröklésre is számos példát találunk a való életben. Mindenki örököl géneket a szüleitől, a C++ nyelv is sok mindent örökölt a C nyelvtől, amely szintén örökölte a jellegzetességeinek egy részét az elődeitől. Származtatás során közvetlenül megkapjuk az ősobjektum(ok) attribútumait és viselkedését, és kibővítjük vagy pontosítjuk azokat.

A példánkban a Kor típusú objektum szerves részévé válik a Pont objektum, mint a kör középpontja, ami teljes mértékben megfelel a kör geometriai definíciójának. Ellentétben a kompozícióval a Kor osztály tagfüggvényiből a Pont osztály védett és nyilvános tagjait egyaránt elérhetjük.

#include "Pont.h"
 
class Kor : public Pont { // öröklés
  protected:
    int r;
  public:
    Kor(int x=0, int y=0, int r=0)
        : Pont(x, y), r(r) {}
    Kor(const Pont & p, int r=0)
        : Pont(p), r(r) {}
    int GetR() {return r;}
    void SetR(int a) { r = a; }
};
 
int main() {
    Kor k1(100, 200, 10);
    k1.Kiir();
    cout<<k1.GetX()<<endl;
    cout<<k1.GetR()<<endl;
}

III.4. Polimorfizmus (többalakúság)

Amikor a C++ nyelvvel kapcsolatban szó esik a többalakúságról, általában arra gondolunk, amikor a származtatott osztály objektumát az alaposztály mutatóján, illetve hivatkozásán keresztül érjük el. Valójában ez az alosztály (subtype), illetve futásidejű (run-time) polimorfizmusnak vagy egyszerűen csak felüldefiniálásnak (overriding) hívott megoldás képezi fejezetünk tárgyát, mégis tekintsük át a C++ további „többalakú” megoldásait is!

  • A kényszerítés (coercion) polimorfizmus alatt az implicit és az explicit típus-átalakításokat értjük. Ekkor egy adott művelet többalakúságát a különböző típusok biztosítják, amelyek szükség esetén konvertálunk.

  • A kényszerítéssel ellentétes az ún. ad-hoc („erre a célra készült”) polimorfizmus, ismertebb nevén a függvénynevek túlterhelése (overloading). Ekkor a fordítóprogram a típusok alapján választja ki az elkészített függvényváltozatok közül a megfelelőt.

  • Ez utóbbi kiterjesztése a parametrikus vagy fordításidejű (compile-time) polimorfizmus, ami lehetővé teszi, hogy ugyanazt a kódot bármilyen típussal végre tudjuk hajtani. C++-ban a függvény- és az osztálysablonok (templates) segítségével valósul meg a parametrikus többalakúság. Sablonok használatával valójában újrahasznosítjuk a C++ forráskódot.

Korábban már láttuk, hogy az öröklés során a leszármazott osztály örökli az őse minden tulajdonságát és viselkedését (műveletét). Ezek az öröklött tagfüggvények minden további nélkül használhatók a származtatott osztály objektumaival is, hiszen magukban foglalják az őseiket. Mivel öröklés során gyakran specializáljuk az leszármazott osztályt, szükséges lehet, hogy bizonyos örökölt műveletek másképp működjenek. Ezt az igényt a virtuális (virtual) tagfüggvények bevezetésével teljesíthetjük. A futásidejű polimorfizmusnak köszönhetően egy objektum attól függően, hogy az osztály-hierarchia mely szintjén lévő osztály példánya, ugyanarra az üzenetre másképp reagál. Az pedig, hogy az üzenet hatására melyik tagfüggvény hívódik meg az öröklési láncból, csak a program futása közben derül ki (késői kötés).

III.4.1. Virtuális tagfüggvények

A virtuális függvény olyan public vagy protected tagfüggvénye az alaposztálynak, amelyet a származtatott osztályban újradefiniálhatunk az osztály „viselkedésének” megváltoztatása érdekében. A virtuális függvény általában a nyilvános alaposztály referenciáján vagy mutatóján keresztül hívódik meg, melynek aktuális értéke a program futása során alakul ki (dinamikus összerendelés, késői kötés).

Ahhoz, hogy egy tagfüggvény virtuális legyen, a virtual kulcsszót kell megadnunk az osztályban a függvény deklarációja előtt:

class Pelda {
  public:
    virtual int vf();
};

Nem szükséges, hogy az alaposztályban a virtuális függvénynek a definíciója is szerepeljen – helyette a függvény prototípusát az =0; kifejezéssel is lezárhatjuk Ebben az esetben ún. tisztán virtuális függvénnyel (pure virtual function) van dolgunk:

class Pelda {
  public:
    virtual int tvf() = 0;
};

Egy vagy több tisztán virtuális függvényt tartalmazó osztállyal (absztrakt osztállyal) nem készíthetünk objektumpéldányt. Az absztrakt osztály csak az öröklés kiinduló pontjaként, alaposztályaként használható.

Amennyiben egy tagfüggvény az osztály-hierarchia valamely pontján virtuálissá válik, akkor lecserélhetővé válik az öröklési lánc későbbi osztályaiban.

III.4.2. A virtuális függvények felüldefiniálása (redefine)

Ha egy függvényt az alaposztályban virtuálisként deklarálunk, akkor ezt a tulajdonságát megőrzi az öröklődés során. A származtatott osztályban a virtuális függvényt saját változattal újradefiniálhatjuk, de az öröklött verziót is használhatjuk. Saját új verzió definiálásakor nem szükséges a virtual szót megadnunk.

Ha egy származtatott osztály tiszta virtuális függvényt örököl, akkor ezt mindenképpen saját verzióval kell újradefiniálni, különben az új osztály is absztrakt osztály lesz. A származtatott osztály tartalmazhat olyan virtuális függvényeket is, amelyeket nem a közvetlen alaposztálytól örökölt.

A származtatott osztályban az újradefiniált virtuális függvény prototípusának pontosan (név, típus, paraméterlista) meg kell egyeznie az alaposztályban definiálttal. Ha a két deklaráció paraméterezése nem pontosan egyezik, akkor az újradefiniálás helyett a túlterhelés (overloading) mechanizmusa érvényesül.

Az alábbi példaprogramban mindegyik alakzat saját maga számolja ki a területét és a kerületét, azonban a megjelenítést az absztrakt alaposztály (Alakzat) végzi. Az osztályok hierarchiája az ábrán látható:

// Absztrakt alaposztály
class Alakzat {
  protected:
     int x, y;
  public:
     Alakzat(int x=0, int y=0) : x(x), y(y) {}
     virtual double Terulet()=0;
     virtual double Kerulet()=0;
     void Megjelenit() {
          cout<<'('<<x<<','<<y<<")\t";
          cout<<"\tTerulet: "<< Terulet();
          cout<<"\tKerulet: "<< Kerulet() <<endl;
     }
};
 
class Negyzet : public Alakzat {
  protected:
     double a;
  public:
     Negyzet(int x=0, int y=0, double a=0) 
             : Alakzat(x,y), a(a) {}
     double Terulet() {return a*a;}
     double Kerulet() {return 4*a;}
};
 
class Teglalap : public Negyzet {
  protected:
     double b;
  public:
     Teglalap(int x=0, int y=0, double a=0,  double b=0)
             : Negyzet(x,y,a), b(b) {}
     double Terulet() {return a*b;}
     double Kerulet() {return 2*(a+b);}
};
 
class Kor : public Negyzet {
  const double pi;
  public:
     Kor(int x=0, int y=0, double r=0)
             : Negyzet(x,y,r), pi(3.14159265) {}
     double Terulet() {return a*a*pi;}
     double Kerulet() {return 2*a*pi;}
};
 
int main() {
     Negyzet n(12,23,10);
     cout<<"Negyzet: ";
     n.Megjelenit();
 
     Kor k(23,12,10);
     cout<<"Kor: ";
     k.Megjelenit();
 
     Teglalap t(12,7,10,20);
     cout<<"Teglalap: ";
     t.Megjelenit();
 
     Alakzat* alakzatok[3] = {&n, &k, &t} ;
     for (int i=0; i<3; i++)
       alakzatok[i]->Megjelenit();
}

A virtuális függvények használata és a nyilvános öröklés lehetővé teszi, hogy az osztály-hierarchia minden objektumával hívható külső függvényeket hozzunk létre:

void MindentMegjelenit(Alakzat& a) {
     cout<<"Terulet: "<<a.Terulet()<<endl;
     cout<<"Kerulet: "<<a.Kerulet()<<endl;
}

III.4.3. A korai és a késői kötés

A futásidejű többalakúság jobb megértése céljából, példák segítségével megvizsgáljuk a tagfüggvény-hívások fordítási időben (korai kötés – early binding) és a futási időben (késői kötés – late binding) történő feloldását.

A példaprogramokban két azonos prototípusú tagfüggvényt (GetNev(), GetErtek()) definiálunk az alaposztályban és a leszármazott osztályban. A main() függvényben pedig alaposztály típusú mutatóval (pA) és referenciával (rA) hivatkozunk a származtatott osztály példányára.

III.4.3.1. A statikus korai kötés

Korai kötés során a fordító a statikusan befordítja a kódba a közvetlen tagfüggvény-hívásokat. Az osztályok esetén ez az alapértelmezett működési mód, ami jól látható az alábbi példaprogram futásának eredményéből.

Korai kötés példa
III.8. ábra - Korai kötés példa


class Alap {
 protected:
     int ertek;
 public:
     Alap(int a=0) : ertek(a) { }
     const char* GetNev() const { return "Alap"; }
     int GetErtek() const { return ertek; }
 };
 
class Szarmaztatott: public Alap {
 protected:
     int ertek;
 public:
     Szarmaztatott(int a=0, int b=0) : Alap(a), ertek(b) { }
     const char* GetNev() const { return "Szarmaztatott"; }
     int GetErtek() const { return ertek; }
 };
 
int main() {
  Alap a;
  Szarmaztatott b(12, 23);
 
  a = b;
  Alap &rA = b;
  Alap *pA = &b;
 
  cout<<"a \t" <<  a.GetNev()<<"\t"<<  a.GetErtek()<<endl;
  cout<<"b \t" <<  b.GetNev()<<"\t"<<  b.GetErtek()<<endl;
  cout<<"rA\t" << rA.GetNev()<<"\t"<< rA.GetErtek()<<endl;
  cout<<"pA\t" <<pA->GetNev()<<"\t"<<pA->GetErtek()<<endl;
}

A programban a GetErtek() tagfüggvény hívásait a III.8. ábra szemlélteti. A program futásának eredménye:

a       Alap    12
b       Szarmaztatott   23
rA      Alap    12
pA      Alap    12

III.4.3.2. A dinamikus késői kötés

Alapvetően változik a helyzet (III.8. ábra), ha az Alap osztályban a GetNev(), GetErtek() tagfüggvényeket virtuálissá tesszük.

class Alap {
 protected:
     int ertek;
 public:
     Alap(int a=0) : ertek(a) { }
     virtual const char* GetNev() const { return "Alap"; }
     virtual int GetErtek() const { return ertek; }
 };

A példaprogram futásának eredménye is módosult:

a       Alap    12
b       Szarmaztatott   23
rA      Szarmaztatott   23
pA      Szarmaztatott   23

Késői kötés példa
III.9. ábra - Késői kötés példa


A virtuális függvények hívását közvetett módon, memóriában tárolt címre történő ugrással helyezi el a kódban a fordító. A címek tárolására használt virtuális metódustábla (VMT) a program futása során osztályonként, az osztály első példányosításakor jön létre. A VMT az aktuális, újradefiniált virtuális függvények címét tartalmazza. Az osztályhierarchiában található azonos nevű virtuális függvények azonos indexszel szerepelnek ezekben a táblákban, ami lehetővé teszi a virtuális tagfüggvények teljes lecserélését.

III.4.3.3. A virtuális metódustábla

Amennyiben egy osztály egy vagy több virtuális tagfüggvénnyel rendelkezik, a fordító kiegészíti az objektumot egy „virtuális mutatóval”, amely egy virtuális metódustáblának (VMT – Virtual Method Table) vagy virtuális függvénytáblának (VFTable – Virtual Function Table) hívott globális adattáblára mutat. A VMT függvénypointereket tartalmaz, amelyek az adott osztály, illetve az ősosztályok legutoljára újradefiniált virtuális tagfüggvényeire mutatnak (III.10. ábra). Az azonos nevű virtuális függvények címe azonos indexszel szerepel ezekben a táblákban.

A példaprogram virtuális metódustáblái
III.10. ábra - A példaprogram virtuális metódustáblái


Az osztályonkénti VMT futás közben, az első konstruktorhíváskor jön létre. Ennek következtében a hívó és hívott tagfüggvény közötti kapcsolat szintén futás közben realizálódik. A fordító mindössze egy olyan hívást helyez a kódba, amely a VMT i. elemének felhasználásával megy végbe (call VMT[i]).

III.4.4. Virtuális destruktorok

A destruktort virtuális függvényként is definiálhatjuk. Ha az alaposztály destruktora virtuális, akkor minden ebből származtatott osztály destruktora is virtuális lesz. Ezáltal biztosak lehetünk abban, hogy a megfelelő destruktor hívódik meg, amikor az objektum megszűnik, még akkor is, ha valamelyik alaposztály típusú mutatóval vagy referenciával hivatkozunk a leszármazott osztály példányára.

A mechanizmus kiváltásához elegendő valahol az öröklési lánc kezdetén egy virtuális, üres destruktort, vagy egy tisztán virtuális destruktort elhelyeznünk egy osztályban:

class Alap {
 protected:
     int ertek;
 public:
     Alap(int a=0) : ertek(a) { }
     virtual const char* GetNev() const { return "Alap"; }
     virtual int GetErtek() const { return ertek; }
     virtual ~Alap() {}
 };

III.4.5. Absztrakt osztályok és interfészek

Mint korábban láttuk, az absztrakt osztályok jó kiinduló pontjául szolgálnak az öröklési láncoknak. C++-ban az absztrakt osztályok jelzésére semmilyen külön kulcsszót nem használunk, egyetlen ismérvük, hogy tartalmaznak-e tisztán virtuális függvényt, vagy sem. Amiért külön részben ismét foglalkozunk velük, az a más nyelvekben követett programozási gyakorlat, ami C++ nyelven is megvalósítható.

A Java, a C# és az Object Pascal programozási nyelvek csak az egyszeres öröklést támogatják, azonban lehetővé teszik tetszőleges számú interfész implementálását. C++ környezetben az interfész olyan absztrakt osztály, amely csak tisztán virtuális függvényeket tartalmaz. Az interfész egyetlen célja, hogy a benne nyilvánosan deklarált tagfüggvények létrehozására kényszerítse a fejlesztőt a származtatás során.

A többszörös öröklés buktatóit elkerülhetjük, ha az alaposztályaink között egy, az adattagokat is tartalmazó, „igazi” osztály, míg a többi interfész osztály. (Az interfész osztályok nevét általában nagy „I” betűvel kezdjük.)

Egy korábbi Pont osztályunk esetén különválasztjuk a geometriai adatok tárolására szolgáló osztályt és a mozgás képességét definiáló interfészt, hisz ez utóbbira nem mindig van szükség.

// a geometriai Pont osztály
class Pont {
   protected:
      int x, y;
   public:
      Pont(int a = 0, int b = 0) : x(a), y(b) {}
      Pont(const Pont& p) : x(p.x), y(p.y) {}
      int GetX() const { return x; }
      int GetY() const { return y; }
      void SetX(int a) { x = a; }
      void SetY(int a) { y = a; }
      void Kiir() const { cout<<'('<<x<<','<<y<<')'<< endl; }
};
 
// absztrakt osztály a mozgatáshoz - interfész
class IMozgat {
   public:
      virtual void Mozgat(int a, int b) = 0;
      virtual void Mozgat(const Pont& p) = 0;
 };
 
// Pont, amely képes mozogni
class MozgoPont : public Pont, public IMozgat {
    public:
      MozgoPont(int a=0, int b=0) : Pont(a,b) {}
      void Mozgat(int a, int b) { x = a; y = b; }
      void Mozgat(const Pont& p) {
          x = p.GetX();
          y = p.GetY();
      }
};
 
int main() {
    Pont fixPont(12, 23);
    fixPont.Kiir();         // (12, 23)
 
    MozgoPont mozgoPont;
    mozgoPont.Kiir();       // (0, 0)
    mozgoPont.Mozgat(fixPont);
    mozgoPont.Kiir();       // (12, 23)
}

III.4.6. Futás közbeni típusinformációk osztályok esetén

A különböző vizuális fejlesztőrendszerek futás közbeni típusinformációkat (RunTime Type Information, RTTI) tárolnak az objektumpéldányok mellett. Ennek segítségével a futtató rendszerre bízhatjuk az objektumok típusának azonosítását, így nem kell nekünk erre a célra adattagokat bevezetnünk.

Az RTTI mechanizmus helyes működéséhez polimorf alaposztályt kell kialakítanunk, vagyis legalább egy virtuális tagfüggvényt el kell helyeznünk benne, és engedélyeznünk kell az RTTI tárolását. (Az engedélyezési lehetőséget általában a fordító beállításai között találjuk meg.) A mutatók és referenciák típusának azonosítására a dynamic_cast és a typeid műveleteket, míg a megfelelő típus-átalakítás elvégzésére a dynamic_cast operátort használjuk.

A typeid operátor egy const type_info típusú objektummal tér vissza, melynek tagjai információt szolgáltatnak az operandus típusáról. Az objektum name() tagfüggvénye által visszaadott karaktersorozat típusonként különböző tartalma fordítónként eltérő lehet. Az operátor használatához a typeinfo fejállományt kell a programunkba beépíteni.

#include <typeinfo>
#include <iostream>
using namespace std;
 
class Os {
   public:
      virtual void Vf(){}  // e nélkül nem tárolódik RTTI
      void FvOs() {cout<<"Os"<<endl;}
  };
 
class Utod : public Os {
   public:
      void FvUtod() {cout<<"Utod"<<endl;}
 };
 
int main() {
   Utod * pUtod = new Utod;
   Os * pOs = pUtod;
 
    // a mutató type_info-ja:
   const type_info& tiOs = typeid(pOs); 
   cout<< tiOs.name() <<endl;
 
   // az Utod type_info-ja
   const type_info& tiUtod = typeid(*pOs); 
   cout<< tiUtod.name() <<endl;
 
   // az Utod-ra mutat?
   if (typeid(*pOs) == typeid(Utod)) 
      dynamic_cast<Utod *>(pOs)->FvUtod();
 
   // az Utod-ra mutat?
   if (dynamic_cast<Utod*>(pOs)) 
      dynamic_cast<Utod*>(pOs)->FvUtod();
   delete pUtod;
} 

A következő példaprogramban a futás közbeni típusinformációkra akkor van szükségünk, amikor osztályonként különböző tagokat szeretnénk elérni.

#include <iostream>
#include <string>
#include <typeinfo>
using namespace std;
 
class Allat {
  protected:
     int labak;
  public:
     virtual const string Fajta() = 0;
     Allat(int n) {labak=n;}
     void Info() {
       cout<<"A(z) "<<Fajta()<<"nak "
           <<labak<<" laba van."<<endl;
      }
};
 
class Hal : public Allat {
  protected:
     const string Fajta() {return "hal";}
  public:
     Hal(int n=0) : Allat(n) {}
     void Uszik() {cout<<"uszik"<<endl;}
};
 
class Madar : public Allat {
  protected:
     const string Fajta() {return "madar";}
  public:
     Madar(int n=2) : Allat(n) {}
     void Repul() {cout<<"repul"<<endl;}
};
 
class Emlos : public Allat {
  protected:
     const string Fajta() {return "emlos";}
  public:
     Emlos(int n=4) : Allat(n) {}
     void Fut() {cout<<"fut"<<endl;}
};
 
int main() {
    const int db=3;
    Allat* p[db] = {new Madar, new Hal, new Emlos};
 
    // RTTI nélkül is működő lekérdezés
    for (int i=0; i<db; i++)
      p[i]->Info();
 
    // RTTI alapú feldolgozás
    for (int i=0; i<db; i++)
       if (dynamic_cast<Hal*>(p[i]))        // Hal?
             dynamic_cast<Hal*>(p[i])->Uszik();
       else
       if (typeid(*p[i])==typeid(Madar))      // Madár?
             dynamic_cast<Madar*>(p[i])->Repul();
       else
       if (typeid(*p[i])==typeid(Emlos))      // Emlős?
             dynamic_cast<Emlos*>(p[i])->Fut();
 
    for (int i=0; i<db; i++)
        delete p[i];
}

Az összehasonlítás kedvéért szerepeljen itt a fenti feladat futás közbeni típusinformációkat nem alkalmazó változata! Ekkor az Allat osztály Fajta() virtuális tagfüggvényének értékével azonosítjuk az osztályt, a típus-átalakításhoz pedig a static_cast operátort használjuk. Csupán a main() függvény tartalma módosult:

int main() {
    const int db=3;
    Allat* p[db] = {new Madar, new Hal, new Emlos};
  
    for (int i=0; i<db; i++)
      p[i]->Info();
  
    for (int i=0; i<db; i++)
       if (p[i]->Fajta()=="hal")
             static_cast<Hal*>(p[i])->Uszik();
       else
       if (p[i]->Fajta()=="madar")
             static_cast<Madar*>(p[i])->Repul();
       else
       if (p[i]->Fajta()=="emlos")
             static_cast<Emlos*>(p[i])->Fut();
  
    for (int i=0; i<db; i++)
        delete p[i];
}

Mindkét programváltozat futásának eredménye:

A(z) madarnak 2 laba van.
A(z) halnak 0 laba van.
A(z) emlosnak 4 laba van.
repul
uszik
fut

III.5. Osztálysablonok (class templates)

A legtöbb típusos nyelv megoldásai típusfüggők, vagyis ha elkészítünk egy hasznos függvényt vagy osztályt, az csak a benne rögzített típusú adatokkal működik helyesen. Amennyiben egy másik típussal is szükségünk van a megoldásra, újra meg kell írnunk azt, a típusok lecserélésével.

A C++ nyelv a függvény- és osztálysablonok (templates) bevezetésével megkíméli a fejlesztőket a „típuscserélgetős” programozási módszer alkalmazásától. A programozó egyetlen feladata elkészíteni a szükséges függvényt vagy osztályt, megjelölve a lecserélendő típusokat, és a többi már a C++ fordító dolga.

III.5.1. Osztálysablon lépésről-lépésre

A fejezet bevezetőjeként először lépésről-lépésre áttekintjük a sablonkészítés menetét és a sablonok felhasználását. Eközben azokra az ismeretekre építünk, melyeket az Olvasó elsajátíthatott a könyv korábbi fejezeteinek feldolgozása során.

Példaként tekintsük az egydimenziós, 32-elemű, egész tömbök egyszerűsített, indexhatár ellenőrzése mellett működő IntTomb osztályát!

#include <iostream>
#include <cassert>
#include <cstring>
using namespace std;
 
class IntTomb {
  public:
     IntTomb(bool nullaz = true) : meret(32) {
        if (nullaz) memset(tar, 0, 32*sizeof(int));
     }
     int& operator [](int index);
     const int meret;
  private:
     int tar[32];
};
 
int & IntTomb::operator [](int index) {
  if (index<0 || index>=32) assert(0);     // indexhiba
  return tar[index];                  // sikerült
}

Az osztály objektumai a 32 egész elem tárolása mellett az elemek elérésekor ellenőrzik az indexhatárokat. Az egyszerűség kedvéért hibás index esetén a program futása megszakad. A tömb (objektum) létrehozásakor minden elem lenullázódik,

IntTomb a;

kivéve, ha a konstruktort false argumentummal hívjuk.

IntTomb a(false);

A tömb a konstans meret adattagban tárolja az elemek számát, illetve átdefiniálja az indexelés operátorát. Ezek alapján a tömb elemeinek elérése:

int main() {
    IntTomb a;
    a[ 7] = 12;
    a[29] = 23;
    for (int i=0; i<a.meret; i++)
        cout<<a[i]<<'\t';
}

Mi tegyünk, ha nem 32 elemet szeretnénk tárolni, vagy ha double típusú adatokra van szükségünk? A megoldást az osztálysablon adja, amelyet a fenti IntTomb osztály piros színnel kiemelt int típusainak általánosításával, és az elemszám (32) paraméterként való megadásával készítünk el.

#include <iostream>
#include <cassert>
#include <cstring>
using namespace std;
 
template <class tipus, int elemszam>
class Tomb {
  public:
     Tomb(bool nullaz=true): meret(elemszam) { 
        if (nullaz) memset(tar, 0, elemszam*sizeof(tipus)); }
     tipus& operator [](int index);  
     const int meret;
  private:
     tipus tar[elemszam];
};

A külső tagfüggvény esetén az osztály nevét az általánosított típussal és a paraméterrel együtt kell szerepeltetni Tomb<tipus, elemszam>:

template <class tipus, int elemszam>
tipus & Tomb<tipus, elemszam>::operator [](int index) { 
  if (index<0 || index>=elemszam) assert(0);     // indexhiba
  return tar[index];                      // sikerült
}

Felhívjuk a figyelmet, hogy az osztálysablon nem implicit inline tagfüggvényeit minden olyan forrásállományba be kell építenünk, amelyből azokat hívjuk. E nélkül a fordító nem tudja a függvény forráskódját előállítani. Több forrásmodulból álló projekt esetén az osztálysablon elemeit - az osztályon kívül definiált tagfüggvényekkel együtt - ajánlott fejállományba helyezni, melyet aztán minden forrásmodulba beilleszthetünk, anélkül hogy „többszörösen definiált szimbólum” hibajelzést kapnánk.

Az osztálysablon (általánosított osztály) lényege - a már bemutatott függvénysablonokhoz hasonlóan -, hogy a sablon alapján a fordító állítja elő a valóságos, típusfüggő osztályt, annak minden összetevőjével együtt. Az osztálysablont mindig paraméterezve használjuk az objektumok létrehozásakor:

Tomb<int, 32> av, bv(false);

Típus definiálásával

typedef Tomb<int, 32> IntTomb;

egyszerűbbé válik az objektumok előállítása:

IntTomb av, bv(false);

A sablondefinícióban szereplő meret egy konstans paraméter, melynek értékét a fordítás során használja fel a fordító. A sablon feldolgozása során, a paraméter helyén egy konstans értéket, vagy C++ konstanst (const) szerepeltethetünk. Természetesen konstans paraméter nélküli sablonokat is készíthetünk, mint ahogy ezt a fejezet további részeiben tesszük.

Mielőtt tovább mennénk, nézzük meg mit is nyújt számunkra az új osztálysablon! A legegyszerűbb alkalmazását már láttuk, így csak a teljesség kedvéért szerepeltetjük újra:

int main() {
    Tomb<int, 32> a;
    a[ 7] = 12;
    a[29] = 23;
    for (int i=0; i<a.meret; i++)
        cout<<a[i]<<'\t';
}

Az elkészült sablon numerikus adatok mellett karaktersorozatok és objektumok tárolására is felhasználható.

const int ameret=8;
Tomb<char *, ameret> s1;
s1[2] = (char*)"C++";
s1[4] = (char*)"java";
s1[7] = (char*)"C#";
for (int i=0; i<s1.meret; i++)
    if (s1[i]) cout<<s1[i]<<'\t';

Felhívjuk a figyelmet arra, hogy osztálytípusú tömbelemek esetén az általunk alkalmazott nullázási megoldás túl drasztikus, így ezt kerülnünk kell a Tomb osztály konstruktorának false értékkel való hívásával. (Ekkor először lefutnak a tömbelemek konstruktorai, majd pedig a Tomb<string, 8> osztály konstruktora következik, ami alaphelyzetben törli a már inicializált elemobjektumok területét.)

const int ameret=8;
Tomb<string, ameret> s2 (false);
s2[2] = "C++";
s2[4] = "java";
s2[7] = "C#";
for (int i=0; i<s2.meret; i++)
    cout<<s2[i]<<'\t';

Természetesen dinamikusan is létrehozhatjuk a tömbobjektumot, ekkor azonban figyelnünk kell az indexelés helyes használatára és a meret adattag elérésre. A tömbobjektumot a (*dt), illetve dt[0] kifejezéssel érjük el, mely után következhet az indexelés operátorának megadása:

Tomb<double, 3> *dt;
dt = new Tomb<double, 3>;
(*dt)[0] =12.23;
 dt[0][1]=34.45;
 for (int i=0; i<dt->meret; i++) 
      cout<<(*dt)[i]<<'\t';  
 delete dt; 

Dinamikus helyfoglalással egyszerűen létrehozhatjuk a Tomb<double, 3> típusú tömbobjektumok ötelemű vektorát. A double típusú adatelemek eléréséhez válasszuk a dupla indexelést! Az első index a dinamikus tömbön belül jelöli ki az elemet, míg a második Tomb<double, 3> típusú objektumon belül. Ezzel a megoldással – látszólag – egy kétdimenziós, double típusú tömböt kaptunk.

Tomb<double, 3> *dm;
dm = new Tomb<double, 3> [5];
dm[0][1] =12.23;
dm[4][2]=34.45;
for (int i=0; i<5; i++) {
  for (int j=0; j<dm[0].meret; j++)
      cout<<dm[i][j]<<'\t';
  cout<<endl;
}
delete []dm;

Amennyiben az ötelemű, dinamikus helyfoglalás vektor helyett statikus tömböt készítünk, hasonló megoldáshoz jutunk:

Tomb<int, 3> m[5]; 
m[0][1] = 12;
m[4][2] = 23;
for (int i=0; i<5; i++) {
  for (int j=0; j<m[0].meret; j++)
      cout<<m[i][j]<<'\t';
  cout<<endl;
} 

Végezetül tekintsük az alábbi példányosítást, ahol olyan ötelemű objektumvektort hozunk létre, melynek mindegyik eleme Tomb<int,3> típusú! Az eredmény most is egyfajta, int típusú elemeket tartalmazó, kétdimenziós tömb.  

Tomb< Tomb<int,3>, 5> p(false);
p[0][1] = 12;
p[4][2] = 23;
for (int i=0; i<p.meret; i++) {
  for (int j=0; j<p[0].meret; j++)
      cout<<p[i][j]<<'\t';
  cout<<endl;
} 

A példákból jól látható, hogy az osztálysablon hatékony programozási eszköz, melynek alkalmazása során azonban – sok esetben – a fordító „fejével” kell gondolkodni, és ismernünk kell a C++ nyelv teljes, objektum-orientált eszköztárának működését és lehetőségeit. Különösen igaz ez a megállapítás, ha osztálysablonok egymásra épülő rendszerét (hierarchiáját) kívánjuk megvalósítani. A következőkben rendszerezett, tematikus formában áttekintjük a sablonkészítés és a használat során felmerülő fogalmakat és technikákat.

Sajnos az elmondottak a C++ nyelv legnehezebb részeinek tekinthetők, mivel a nyelv tejes ismeretét feltételezik. Amennyiben az Olvasó egyelőre nem kíván saját osztálysablonokat fejleszteni, elegendő a fejezetet záró III.5.6. részét áttanulmányoznia, amely a Szabványos sablonkönyvtár használatához nyújt hathatós segítséget.  

III.5.2. Általánosított osztály definiálása

A paraméterezett (általánosított) osztály (generic class), lehetővé teszi, hogy más osztályok definiálásához a paraméterezett osztályt, mint mintát használjuk. Ezáltal egy adott osztálydefiníció minden típus esetén alkalmazható.

Nézzük meg az osztálysablonok definiálásának általános formáját, ahol típus1,..típusN a típusparamétereket jelölik! A sablonfejben (template<>)a típusparaméterek kijelölésére a class és typename kulcsszavakat egyaránt használhatjuk:

template <class típus1, … class típusN> 
class Osztálynév {    
   …
};

vagy

template <typename típus1, … typename típusN> 
class Osztálynév {
   …
};

Az osztály nem inline tagfüggvényeinek definícióját az alábbiak szerint kell megadni:

template <class típ1, … class típN > 
fvtípus Osztálynév< típ1, … típN> :: FvNév(paraméterlista) {
   …
}

vagy

template <typename típ1, … typename típN > 
fvtípus Osztálynév< típ1, … típN> :: FvNév(paraméterlista) {
   …
}

Példaként tekintsük a Pont osztályból készített általánosított osztályt, implicit inline tagfüggvényekkel!

template <typename tipus>
class Pont {
   protected:
      tipus x, y;
   public:
      Pont(tipus a = 0, tipus b = 0) : x(a), y(b) {}
      Pont(const Pont& p) : x(p.x), y(p.y) {}
      tipus GetX() const { return x; }
      tipus GetY() const { return y; }
      void SetX(tipus a) { x = a; }
      void SetY(tipus a) { y = a; }
      void Kiir() const { cout<<'('<<x<<','<<y<<')'<< endl; }
};

Ugyanez a Pont osztály jóval bonyolultabb formát ölt, ha a tagfüggvényeinek egy részét az osztályon kívül definiáljuk:

template <typename tipus>
class Pont {
   protected:
      tipus x, y;
   public:
      Pont(tipus a = 0, tipus b = 0) : x(a), y(b) {}
      Pont(const Pont& p) : x(p.x), y(p.y) {}
      tipus GetX() const;
      tipus GetY() const { return y; }
      void SetX(tipus a);
      void SetY(tipus a) { y = a; }
      void Kiir() const;
};
template <typename tipus>
tipus Pont<tipus>::GetX() const { return x; }
 
template <typename tipus>
void Pont<tipus>::SetX(tipus a) { x = a; }
 
template <typename tipus>
void Pont<tipus>::Kiir() const { 
     cout<<'('<<x<<','<<y<<')'<< endl; 
}

A Pont minkét formájában egy általánosított osztály, vagy osztálysablon, amely csupán egy forrás nyelven rendelkezésre álló deklaráció, és amelyen a fordító csak a szintaxist ellenőrzi. A gépi kódra való fordítás akkor megy végbe, amikor a sablont konkrét típusargumentumokkal példányosítjuk, vagyis sablonosztályokat hozunk létre.

III.5.3. Példányosítás és specializáció

Az osztálysablon és a belőle létrehozott egyedi osztályok között hasonló a kapcsolat, mint egy normál osztály és az objektumai között. A normál osztály meghatározza, miként lehet objektumok csoportját létrehozni, míg a sablonosztály az egyedi osztályok csoportjának generálásához ad információkat.

A sablonokat különböző módon használhatjuk. Az implicit példányosítás során (instantiation) a típusparamétereket konkrét típusokkal helyettesítjük. Ekkor először létrejön az osztály adott típusú változata (ha még nem létezett), majd pedig az objektumpéldány:

Pont<double> p1(1.2, 2.3), p2(p1);
Pont<int> *pp;  // a Pont<int> osztály nem jön létre 

Explicit példányosítás során arra kérjük a fordítót, hogy hozza létre az osztály példányát a megadott típusok felhasználásával, így az objektum készítésekor már kész osztállyal dolgozhatunk:

template class Pont<double>;
…
Pont<double> p1(1.2, 2.3), p2(p1);

Vannak esetek, amikor a sablon felhasználását megkönnyíti, ha az általános változatot valamilyen szempont szerint specializáljuk (explicit specialization). Az alábbi deklarációk közül az első az általános sablont, a második a mutatókhoz készített változatot, a harmadik pedig a void* mutatókra specializált változatot tartalmazza.

template <class tipus> class Pont {
  // a fenti osztálysablon
};
 
template <class tipus> class Pont <tipus *> {
  // el kell készíteni!
};
 
template <> class Pont <void *> {
  // el kell készíteni!
};

A specializált változatokat az alábbi példányosítások során használhatjuk:

Pont<double> pa;
Pont<int *> pp;
Pont<void *> pv;

Vizsgáljuk meg a példányosítás és a specializáció működését kétparaméteres sablonok esetén! Ekkor az egyik sablonparaméter elhagyásával részleges specializációt is készíthetünk:

template <typename T1, typename T2>
class Adatfolyam {
   public:
      Adatfolyam() { cout << "Adatfolyam<T1,T2>"<<endl;}
      // …
};
 
template < typename T1, typename T2>     // Specializáció
class Adatfolyam<T1*, T2*> {
   public:
      Adatfolyam() { cout << "Adatfolyam<T1*,T2*>"<<endl;}
      // …
};
 
template < typename T1>             // Részleges specializáció
class Adatfolyam<T1, int> {
   public:
      Adatfolyam() { cout << "Adatfolyam<T1, int>"<<endl;}
      // …
};
 
template <>                    // Teljes specializáció
class Adatfolyam<char, int> {
   public:
      Adatfolyam() { cout << "Adatfolyam<char, int>"<<endl;}
      // …
} ;
 
int main() {
   Adatfolyam<char, int> s4 ;        // Teljes specializáció
   Adatfolyam<int, double> s1 ;
   Adatfolyam<double*, int*> s2;     // Specializáció
   Adatfolyam<double, int> s3 ;     // Részleges specializáció
}

III.5.4. Érték- és alapértelmezett sablonparaméterek

A fejezet bevezető példájában az osztálysablont a típus paraméter mellett egy egész típusú értékparaméterrel is elláttuk. Ennek segítségével egy konstans értéket adtunk át a fordítónak a példányosítás során.

A C++ támogatja az alapértelmezett sablonparaméterek használatát. Lássuk el a Tomb osztálysablon paramétereit alapértelmezés szerinti értékekkel!

#include <iostream>
#include <cassert>
#include <cstring>
using namespace std;
 
template <typename tipus=int, int elemszam=32>
class Tomb {
  public:
    Tomb(bool nullaz=true): meret(elemszam) {
       if (nullaz) memset(tar, 0, elemszam*sizeof(tipus)); }
    tipus& operator [](int index) {
       if (index<0 || index>=elemszam) assert(0); 
       return tar[index]; 
     }
     const int meret;
  private:
     tipus tar[elemszam];
};

Ebben az esetben az IntTomb típus létrehozásához argumentumok nélkül is specializálhatjuk az általánosított osztályunkat:

        typedef Tomb<> IntTomb;

Az alábbi egyszerű példa bemutatja a verem (Stack) adatstruktúra osztálysablonként történő megvalósítását. A veremsablon paramétereit szintén alapértelmezett értékekkel láttuk el.

#include <iostream>
#include <string>
using namespace std;
 
template<typename Tipus=int, int MaxMeret=100>
class Stack {
   Tipus tomb[MaxMeret];
   int sp;
  public:
   Stack(void) { sp = 0; };
   void Push(Tipus adat) {
      if (sp < MaxMeret) tomb[sp++] = adat;
   }
   Tipus Pop(void) {
      return tomb[sp > 0 ? --sp : sp];
   }
   bool Ures(void) const { return sp == 0; };
};
 
int main(void) {
  Stack<double,1000> dVerem; // 1000 elemű double verem
  Stack<string> sVerem;      // 100 elemű string verem
  Stack<> iVerem;            // 100 elemű int verem
 
  int a=102, b=729;
  iVerem.Push(a);
  iVerem.Push(b);
  a=iVerem.Pop();
  b=iVerem.Pop();
 
  sVerem.Push("nyelv");
  sVerem.Push("C++");
  do {
     cout << sVerem.Pop()<<endl;;
  } while (!sVerem.Ures());
}

III.5.5. Az osztálysablon „barátai” és statikus adattagjai

Az osztálysablonnak is lehetnek barátai, melyek többféleképpen viselkedhetnek. Azok, amelyek nem tartalmaznak sablonelőírást, minden specializált osztály közös „barátai” lesznek. Ellenkező esetben a külső függvény csak az adott példányosított osztályváltozat friend függvényeként használható.

Az első példában a BOsztaly osztálysablon minden példányosítása barátosztálya lesz az AOsztaly-nak:

#include <iostream>
using namespace std;
 
class AOsztaly {
   void Muvelet() { cout << "A muvelet elvegezve."<< endl; };
   template<typename T> friend class BOsztaly;
};
 
template<class T> class BOsztaly {
   public:
      void Vegrehajt(AOsztaly& a) { a.Muvelet(); }
};
 
int main() {
   BOsztaly<int> b;
   BOsztaly<double> c;
   AOsztaly a;
   b.Vegrehajt(a);
   c.Vegrehajt(a);
}

A második példánkban a definiált barátfüggvény (Fv) maga is sablon:

#include <iostream>
using namespace std;
 
// Előrevetett deklarációk
template <typename T> class Osztaly;
template <typename T> void Fv(Osztaly<T>&);
 
template <typename T> class Osztaly {
    friend void Fv<T>(Osztaly<T>&);
   public:
    T GetAdat(){return adat;}
    void SetAdat(T a){adat=a;}
   private:
    T adat;
};
 
template<typename T> void Fv(Osztaly<T>& x) {
    cout<<"Eredmeny: "<<x.GetAdat()<<endl;
 }
 
int main() {
    Osztaly<int> obj1;
    obj1.SetAdat(7);
    Fv(obj1);
    Osztaly<double> obj2;
    obj2.SetAdat(7.29);
    Fv(obj2);
}

Az általánosított osztályban definiált statikus adattagokat sablonosztályonként kell létrehoznunk:

#include <iostream>
using namespace std;
 
template<typename tipus> class Osztaly {
  public:
    static int ID;
    static tipus adat;
    Osztaly() { ID = adat = 0; }
};
 
// A statikus adattagok definíciói
template <typename tipus> int Osztaly<tipus>::ID = 23;
template <typename tipus> tipus Osztaly<tipus>::adat = 12.34;
 
int main() {
    Osztaly <double> dObj1, dObj2;
 
    cout << dObj1.ID++   << endl;           // 23
    cout << dObj1.adat-- << endl;           // 12.34
 
    cout << dObj2.ID   << endl;             // 24
    cout << dObj2.adat << endl;             // 11.34
 
    cout <<Osztaly<double>::ID << endl;         // 24
    cout <<Osztaly<double>::adat << endl;       // 11.34
}

III.5.6. A C++ nyelv szabványos sablonkönyvtára (STL)

A Standard Template Library (Szabványos Sablonkönyvtár - röviden STL) szoftverkönyvtár a C++ nyelv Szabványos Könyvtárának szerves részét képezi. Az STL konténerek (tárolók), algoritmusok és iterátorok gyűjteménye, valamint számos alapvető informatikai algoritmust és adatszerkezetet tartalmaz. Az STL elemei paraméterezhető osztályok és függvények, melyek használatához ajánlott megérteni a C++ nyelv sablonkezelésének alapjait.

Az alábbi áttekintés nem helyettesíti egy teljes STL leírás áttanulmányozást, azonban ahhoz elegendő információt tartalmaz, hogy az Olvasó bátran használja a programjaiban a könyvtár alapvető elemeit.

III.5.6.1. Az STL felépítése

A könyvtár elemei öt csoportba sorolhatók:

  • tárolók, konténerek (containers) – az adatok memóriában való tárolását biztosító adatstruktúrák (vector, list, map, set, deque, …)

  • adaptációk (adaptors) – a tárolókra épülő magasabb szintű adatstruktúrák (stack, queue, priority_queue)

  • algoritmusok (algorithm) - a konténerekben tárolt adatokon elvégezhető műveletek (sort, copy, search, min, max, …)

  • iterátorok (iterators) – általánosított mutatók, amely biztosítják a tárolók adatainak különböző módon történő elérését (iterator, const_iterator, ostream_iterator<>, … )

  • műveletobjektumok (function objects) – a műveleteket osztályok fedik le, más komponensek számára (divides, greater_equal, logical_and, …).

Az sablonkezelésnek megfelelően az egyes lehetőségek fejállományok beépítésével érhetők el. Az alábbi táblázatban összefoglaltuk az STL leggyakrabban használt deklarációs fájljait:

Rövid leírás

Fejállomány

Adatsorok kezelése, rendezése, keresés stb.

<algorithm>

Asszociatív tároló: bithalmaz

<bitset>

Asszociatív tároló: halmazok (elemismétlődéssel – multiset, illetve ismétlődés nélkül - set)

<set>

Asszociatív tároló: kulcs/érték adatpárok tárolása 1:1 (map), illetve 1:n (multiset) kapcsolatban (leképzések)

<map>

Előre definiált iterátorok, adatfolyam iterátorok

<iterator>

Tároló: dinamikus tömb

<vector>

Tároló: kettősvégű sor

<deque>

Tároló: lineáris lista

<list>

Tároló adaptáció: sor

<queue>

Tároló adaptáció: verem

<stack>

III.5.6.2. Az STL és C++ tömbök

Az STL algoritmusai valamint az adatfolyam iterátorai egydimenziós C++ tömbök esetén is használhatók. Ezt az teszi lehetővé, hogy a C++ mutatókhoz és az iterátorokhoz ugyanazokat a műveleteket használhatjuk: a mutatott adat (*), léptetés (++) stb. Továbbá, az algoritmus függvénysablonok többsége az adatsor kezdetét (begin) és az utolsó adat utáni pozícióját (end) kijelölő általánosított mutatókat vár argumentumként.

Az alábbi példában egy hételemű egész tömb elemein különböző műveleteket hajtunk végre az STL algoritmusainak segítségével. A bemutatottak alapján a több mint 60 algoritmus többségét eredményesen használhatjuk a hagyományos C++ programjainkban is.

#include <iostream>
#include <iterator>
#include <algorithm>
using namespace std;
 
void Kiir(const int x[], int n) {
    static ostream_iterator<int> out(cout,"\t");
    cout<< "\t";
    copy(x, x+n, out);
    cout<<endl;
}
 
void IntKiir(int a) {
    cout << "\t" << a << endl;
}
 
int main() {
    const int db = 7;
    int adat[db]={2, 7, 10, 12, 23, 29, 80};
 
    cout << "Eredeti tömb: " << endl;
    Kiir(adat, db);
 
    cout << "Következő permutació: " << endl;
    next_permutation(adat,adat+db);
    Kiir(adat, db);
 
    cout << "Sorrend megfordítása: " << endl;
    reverse(adat,adat+db);
    Kiir(adat, db);
 
    cout << "Véletlen keverés: " << endl;
    for (int i=0; i<db; i++) {
       random_shuffle(adat,adat+db);
       Kiir(adat, db);
    }
 
    cout << "A legnagyobb elem: ";
    cout << *max_element(adat,adat+db) << endl;
 
    cout << "Elem keresése:";
    int *p=find(adat,adat+db, 7);
    if (p != adat+db)
      cout << "\ttalált" <<endl;
     else
      cout << "\tnem talált" <<endl;
 
    cout << "Rendezés: " << endl;
    sort(adat,adat+db);
    Kiir(adat, db);
 
    cout << "Elemek kiírása egymás alá:"<< endl;
    for_each(adat, adat+db, IntKiir);
    Kiir(adat, db);
 
    cout << "Csere: " << endl;
    swap(adat[2],adat[4]);
    Kiir(adat, db);
 
    cout << "Feltöltés: " << endl;
    fill(adat,adat+db, 123);
    Kiir(adat, db);
}

Ugyancsak beszédes a program futásának eredménye:

Eredeti tömb:
        2       7       10      12      23      29      80
Következő permutáció:
        2       7       10      12      23      80      29
Sorrend megfordítása:
        29      80      23      12      10      7       2
Véletlen keverés:
        10      80      2       23      29      7       12
        2       10      23      80      29      12      7
        7       12      2       10      80      29      23
        2       12      29      10      80      7       23
        12      23      7       29      10      2       80
        7       23      12      2       80      10      29
        7       12      23      2       29      10      80
A legnagyobb elem: 80
Elem keresése:  talált
Rendezés:
        2       7       10      12      23      29      80
Elemek kiírása egymás alá:
        2
        7
        10
        12
        23
        29
        80
        2       7       10      12      23      29      80
Csere:
        2       7       23      12      10      29      80
Feltöltes:
        123     123     123     123     123     123     123

III.5.6.3. Az STL tárolók használata

A konténereket két fő csoportba sorolhatjuk: adatsorok (soros) és asszociatív tárolók. A soros tárolókra (vektor - vector, lista - list, kettősvégű sor - deque) jellemző, hogy elemek sorrendjét a programozó határozza meg. Az asszociatív tárolók (leképzés - map, halmaz – set, bithalmaz - bitset stb.) közös tulajdonsága, hogy az elemek sorrendét maga a konténer szabja meg, valamint az elemek egy kulcs alapján érhetők el. Mindegyik tároló dinamikusan kezeli a memóriát, tehát az adatok száma szabadon változtatható.

A tároló objektumok tagfüggvényei segítik az adatok kezelését és elérését. Mivel ez a függvénykészlet függ a konténer típusától, felhasználás előtt mindenképpen a szakirodalomban (Interneten) kell megnézni, hogy egy adott taroló esetén mik a lehetőségeink. Most csak néhány általános művelet áttekintésére vállalkozunk:

  • Elemet szúrhatunk be (insert()) vagy törölhetünk (erase()) egy iterátorral kijelölt pozcíóba/ból.

  • Elemet adhatunk (push) a soros tárolók elejére (front) vagy végére (back), illetve levehetünk (pop) egy elemet: push_back(), pop_front() stb.

  • Bizonyos konténereket indexelhetjük is a tömböknél használt módon ([]).

  • A begin() és az end() függvények az algoritmusoknál felhasználható iterátorokat adnak vissza, amelyek segítik az adatstruktúrák bejárását.

A következőkben egy vector tárolót használó programmal szemléltetjük az elmondottakat:

#include <vector>
#include <algorithm>
#include <iterator>
#include <iostream>
using namespace std;
 
double Osszeg(const vector<double>& dv) {
   vector<double>::const_iterator p; // konstans iterátor
   double s = 0;
   for (p = dv.begin(); p != dv.end(); p++)
      s += *p;
   return s;
}
 
bool Paratlan (int n) {
    return (n % 2) == 1;
}
 
int main() {
   // kimeneti iterátor
   ostream_iterator<double>out(cout, " ");
   double adatok[]  = {1.2, 2.3, 3.4, 4.5, 5.6};
 
   // A vektor létrehozása az adatok tömb elemivel
   vector<double> v(adatok, adatok+5);
 
   // A vektor kiírása
   copy(v.begin(), v.end(), out);  cout << endl;
   cout<<"Elemösszeg: "<<Osszeg(v)<<endl;
 
   // A vektor bővítése elemekkel
   for (int i=1; i<=5; i++)
       v.push_back(i-i/10.0);
   copy(v.begin(), v.end(), out);  cout << endl;
 
   // Minden elem növelése 4.5 értékkel
   for (int i=0; i<v.size(); i++)
       v[i] += 4.5;
   copy(v.begin(), v.end(), out);  cout << endl;
 
   // Minden elem egésszé alakítása
   vector<double>::iterator p;
   for (p=v.begin(); p!=v.end(); p++)
      *p = int(*p);
   copy(v.begin(), v.end(), out);  cout << endl;
 
   // Minden második elem törlése
   int index = v.size()-1;
   for (p=v.end(); p!=v.begin(); p--)
      if (index-- % 2 ==0)
         v.erase(p);
   copy(v.begin(), v.end(), out);  cout << endl;
 
   // A vektor elemeinek rendezése
   sort(v.begin(), v.end() );
   copy(v.begin(), v.end(), out);  cout << endl;
 
   // 7 keresése a vektorban
   p = find(v.begin(), v.end(), 7);
   if (p != v.end() )
       cout << "talált"<< endl;
   else
       cout << "nem talált"<< endl;
 
   // A páratlan elemek száma
   cout<< count_if(v.begin(), v.end(), Paratlan)<< endl;
}

A program futásának eredménye:

1.2 2.3 3.4 4.5 5.6
Elemösszeg: 17
1.2 2.3 3.4 4.5 5.6 0.9 1.8 2.7 3.6 4.5
5.7 6.8 7.9 9 10.1 5.4 6.3 7.2 8.1 9
5 6 7 9 10 5 6 7 8 9
5 7 10 6 8
5 6 7 8 10
talált
2

III.5.6.4. Az STL tároló adaptációk alkalmazása

A konténer-adaptációk olyan tárolók, amelyek módosítják a fenti tároló osztályokat az alapműködéstől eltérő viselkedés biztosítása érdekében. A támogatott adaptációk a verem (stack), a sor (queue) és a prioritásos sor (priority_queue).

Az adaptációk viszonylag kevés tagfüggvénnyel rendelkeznek, és mögöttük különböző tárolók állhatnak. Példaként tekintsük a stack osztálysablont!

A „last-in, first-out” működésű verem egyaránt adaptálható a vector, a list és a deque tárolókból. Az adaptált stack-függvényeket táblázatban foglaltuk össze:

void push(const érték_típus& a)

a bevitele a verembe,

void pop()

a verem felső elemének levétele,

érték_típus& top()

a verem felső elemének elérése,

const érték_típus& top() const

a verem felső elemének lekérdezése,

bool empty() const

true érték jelzi, ha a verem üres,

size_type size()const

a veremben lévő elemek száma,

operator== és operator<

az egyenlőség és a kisebb művelet.

Az alábbi példaprogramban adott számrendszerbe való átváltásra használjuk a vermet:

#include <iostream>
#include <stack>
#include <vector>
using namespace std;
 
int main() {
   int szam=2013, alap=16;
   stack<int, vector<int> > iverem;
 
   do {
     iverem.push(szam % alap);
     szam /= alap;
   } while (szam>0);
 
   while (!iverem.empty()) {
      szam = iverem.top();
      iverem.pop();
      cout<<(szam<10 ? char(szam+'0'): char(szam+'A'-10));
   }
}