2016. szeptember 9., péntek

Osztálysablonok

      Az osztálysablont paraméteres típusnak is lehet nevezni, ugyanis paraméterekre van szükség, hogy egy osztálysablont általános osztállyá alakítsunk. A példákban a verem adatstruktúra lesz a központban, amelynek LIFO (Last In First Out) alapú működése érvényes lesz bármilyen adattípusra. Az osztálysablonok lehetővé teszik egy olyan általános jellegű verem létrehozását, amelyet egy bizonyos adattípusra lehet szabni. Mindez elősegíti a kódújrahasznosítást, hiszen nem kell a hasonló függvényeket mindenik adattípusra külön-külön megírni. Amikor a programozó egy adattípust szeretne a sablonjába illeszteni, akkor egyszerűen csak azzal használja, a többit a fordító megoldja. Ilyen módon a Stack osztálysablon használható double, int, char vagy akár Employee osztálytípusra is.

tstack.h
#ifndef TSTACK_H
#define TSTACK_H

template <class T>
class Stack
{
     public:
           Stack(int = 10);                //konstruktor: 10 elem
           ~Stack() { delete[] stackPtr; } //destruktor
           bool push(const T&);            //beszúrás
           bool pop(T&);                   //kivétel
     private:
           int size;                       //max elemek száma
           int top;                        //jelenlegi elemek száma
           T* stackPtr;                    //T osztálysablon típusú mutató
           bool isEmpty() const {return top == -1;}    //Üres-e
           bool isFull() const {return top == size-1;} //Megtelt-e
};

template<class T>
Stack<T>::Stack(int s)
{
     size = s > 0 ? s : 10;
     top = -1;               //kezdetben üres
     stackPtr = new T[size]; //memóriahely a verem számára
}

template<class T>
bool Stack<T>::push(const T& pushValue)
{
     if(!isFull())                      //ha nincs teli, akkor
     {
           stackPtr[++top] = pushValue; //berak egy elemet és növeli az elemszámot
           return true;                 //sikeres beszúrás
     }
     return false;                      //ha ideér, akkor a beszúrás sikertelen
}

template<class T>
bool Stack<T>::pop(T& popValue)
{
     if(!isEmpty())                    //ha nem üres, akkor
     {
           popValue = stackPtr[top--]; //kivesz egy elemet és csökkenti az elemszámot
           return true;                //sikeres kivétel
     }
     return false;                     //ha ideér, akkor a kivétel sikertelen
}
#endif

test_stack.cpp
#include <iostream>
using std::cout;
using std::cin;
using std::endl;
#include "tstack.h"

int main()
{
     Stack<double> doubleStack(5);
     double f = 1.1;

     cout << "Elemek beszúrása a doubleStack verembe:\n";
     while(doubleStack.push(f))
     {
           cout << f << ' ';
           f += 1.1;
     }
     cout << "\nA verem megtelt. Nem szúrható be a " << f << " elem.";
      
     cout << "\n\nElemek kivétele a doubleStack veremből:\n";
     while(doubleStack.pop(f)) cout << f << ' ';
     cout << "\nA verem kiürült. Nem lehet több elemet kivenni.\n";

     Stack<int> intStack;
     int i = 1;

     cout << "\nElemek beszúrása az intStack verembe:\n";
     while(intStack.push(i))
     {
           cout << i << ' ';
           ++i;
     }
     cout << "\nA verem megtelt. Nem szúrható be a " << i << " elem.";
    
     cout << "\n\nElemek kivétele az intStack veremből:\n";
     while(intStack.pop(i)) cout << i << ' ';
     cout << "\nA verem kiürült. Nem lehet több elemet kivenni.\n";

     return 0;
}


A kimenet:
Elemek beszúrása a doubleStack verembe:
1.1 2.2 3.3 4.4 5.5
A verem megtelt. Nem szúrható be a 6.6 elem.

Elemek kivétele a doubleStack veremből:
5.5 4.4 3.3 2.2 1.1
A verem kiürült. Nem lehet több elemet kivenni.

Elemek beszúrása az intStack verembe:
1 2 3 4 5 6 7 8 9 10
A verem megtelt. Nem szúrható be a 11 elem.

Elemek kivétele az intStack veremből:
10 9 8 7 6 5 4 3 2 1
A verem kiürült. Nem lehet több elemet kivenni.

A Stack osztály definíciója hasonló az általános osztálydefinícióhoz, csakhogy előtte van a template<class T> fejléc. Ez azt jelenti, hogy az osztály egy osztálysablont használ, a T paraméterrel. Amikor létrehozunk egy Stack típusú objektumot, akkor a T paramétert az adott adattípus fogja helyettesíteni. Bármilyen adattípussal alkalmazható a T paraméter. A T szerepében alkalmazott adattípusok esetén két dolgnak kell teljesülnie:
- a konstruktor legyen alapértelmezve (int = 10)
- támogassa a hozzárendelő operátort (=)
Ha a Stack objektum dinamikusan lefoglalt memóriahelyekre hivatkozik, akkor a hozzárendelő operátor túlterhelt kell legyen (át kell legyen definiálva) az adott adattípusnak.
      A main függvény első utasítása egy 5 elemű, double típusú vermet hoz létre. A fordító a double típust rendeli hozzá az osztálysablonT paraméteréhez, így az Stack osztály double verziójú forráskódja fog legenerálódni és működni. Bár a programozó nem látja ezt a forráskódot, az automatikusan része lesz a programnak és lefordítódik. A program ezt követően double típusú adatokkal kezdi feltölteni a vermet, addig amíg a push függvény hamisat nem térít vissza (ekkor a verem megtelt). Ezek után az adatokat a pop függvény elkezdi kivenni, amíg a visszatérített érték hamis nem lesz (ekkor a verem kiürült). Ugyanígy történik a beszúrás és a kivétel az int típusú verem esetén is. Ennek deklarálásásnál nincs megadva a verem mérete, így az alapértelmezett konstruktor 10 elemű vermet hoz létre.
      A template<class T>  fejlécet a tagfüggvények definícióinál is alkalmazni kell, sőt a :: bináris operátor is a <T> paraméterrel van alkalmazva, hogy hozzárendelje a tagfüggvény nevét az osztálysablon tartományához. Például, amikor a doubleStack objektum Stack<double>-ként van értelmezve, akkor a konstruktor a new operátort használja a double típusú verem létrehozására: stackPtr = new T[size] átalakul stackPtr = new double[size]  paranccsá.
      A következő példában a testStack függvény ugyanazt hajtja végre mint az előző példa. A T paramétert használja a veremben lévő adattípusok képviselésére.

test1_stack.cpp
#include<iostream>
using std::cout;
using std::cin;
using std::endl;
#include "tstack.h"

template<class T>
void testStack(Stack<T> &theStack, T value, T increment, const char* stackName)
{
     cout << "\n\nElemek beszúrása a " << stackName << "verembe:\n";
     while(theStack.push(value))
     {
           cout << value << ' ';
           value += increment;
     }
     cout << "\nA verem megtelt. Nem szúrható be a " << value << " elem.";
    
     cout << "\n\nElemek kivétele a " << stackName << "veremből:\n";
     while(theStack.pop(value)) cout << value << ' ';
     cout << "\nA verem kiürült. Nem lehet több elemet kivenni.\n";
}

int main()
{
     Stack<double> doubleStack(5);
     Stack<int> intStack;

     testStack(doubleStack, 1.1, 1.1, "doubleStack");
     testStack(intStack, 1, 1, "intStack");

     return 0;
}
Ebben az esetben is a főprogram létrehoz két Stack típusú objektumot. Az egyik double, a másik int típusú elemekkel kompatibilis.

Értékparaméterek
      A Stack osztálysablon csupán a T típusparamétert használta a sablon fejlécében. Emellett lehet használni még értékparamétereket is, például: template<class T, int elements>. Az objektum deklarálása is megváltozik: Stack<double, 100> doubleStack. Ez az utasítás egy 100 elemű double vermet deklarál. Az osztály ebben az esetben tartalmazhatna egy tömböt, mint privát tagváltozó: T stackHolder[elements];

Statikus tagok
      A hagyományos osztályok esetén static típusú adattagok az összes objektum számára globálisak, ugyanis osztály (és nem objektum) szinten vannak tárolva. Az osztálysablonokból generált osztályoknak külön-külön saját statikus adattagjai lesznek, de továbbra is osztályszinten lesznek tárolva.

Dinamikus memórialefoglalás kezelése
      Előfordulhat, hogy például a stackPtr = new T[size]  művelet sikertelen lesz. A konstruktor nem téríthet vissza értéket, de valahogy mégis el kellene küldeni a hibaüzenetet a programnak anélkül, hogy az megálljon (amit például az assert okoz). Egyik megoldás, hogy visszatérítjük az objektumot akkor is, ha nem volt megfelelően felépítve annak reményében, hogy az osztály felhasználója majd leellenőrzi azt mielőtt használná. Egy másik megoldás, hogy beállítunk egy tagváltozót a konstruktoron kívül, amelyet ellenőrizve jelezhető a hiba. A harmadik megoldás egy kivételkezelés (try-catch) megvalósítása, amellyel még a konstruktorban jelezni tudjuk a hibát. A konstruktorban keletkezett kivételek (try) automatikusan meghívják az objektum destruktorát még mielőtt elindulna a kezelés (catch). Ha az objektumnak a tagjai is objektumok, és kivétel keletkezik a gazda objektum létrehozása során, akkor a tag-objektumok destruktorai is lefutnak még a hiba megjelenése előtt. A destruktorban fellépő kivételek is kezelhetőek, ha a destruktort meghívó függvényt egy try blokkba helyezzük, melyet a kivétel catch-je kezel.
      A standard C++ fordító egy bad_alloc kivételt generál, amikor hiba lép fel a new operátor alkalmazásánál. Ez a <new> fejléc-állományában van definiálva. Egyes fordítók viszont nem kompatibilisek a C++ standard könyvtáraival, és 0 értéket térítenek vissza a kivétel esetén:

test_new.cpp
#include <iostream>
using std::cout;

int main()
{
     double *ptr[50];
    
     for(int i = 0; i < 50; i++)
     {
           ptr[i] = new double[300000000];
           if(ptr[i] == 0)
           {
                cout << "Sikertelen memóriafoglalás: ptr[" << i << "]\n";
                break;
           }
           else
                cout << "Sikeres memóriafoglalás: ptr[" << i << "]\n";
     }

     return 0;
}

A fenti program egy 50 elemű double mutatókból álló tömböt deklarál. A tömb minden elemének elkezd lefoglalni 300 ezer double méretű memóriahelyet, és leellenőrzi, hogy a tömb adott eleme nulla értéket kapott-e vissza a lefoglalás során. Annak függvényében, hogy mennyi memória áll rendelkezésre (ebben az esetben 8GB) és mekkora a double mérete (ebben az esetben 8 byte), a program előbb utóbb meg fog állni. A tömb első eleme rögtön lefoglal 8x300000000 Byte = 2400000000 Byte = 2.4GB memóriahelyet. Belátható, hogy a harmadik lefoglalás után be fog telni a memória és a new hibát generál:

Sikeres memóriafoglalás: ptr[0]
Sikeres memóriafoglalás: ptr[1]
Sikeres memóriafoglalás: ptr[2]
Sikeres memóriafoglalás: ptr[3]
Sikertelen memóriafoglalás: ptr[4]

A következő példában olyan fordító dolgozik, amely kezelni tudja a bad_alloc kivételt:

test1_new.cpp
#include <iostream>
using std::cout;
using std::endl;
#include <new>
using std::bad_alloc;

int main()
{
     double *ptr[50];
    
     try
     {
           for(int i = 0; i < 50; i++)
           {
                ptr[i] = new double[300000000];
                cout << "Sikeres memóriafoglalás: ptr[" << i << "]\n";
           }
     }
     catch(bad_alloc e)
     {
           cout << "A következő kivétel keletkezett: " << e.what() << endl;
     }
    
     return 0;
}

A kimenet:
Sikeres memóriafoglalás: ptr[0]
Sikeres memóriafoglalás: ptr[1]
Sikeres memóriafoglalás: ptr[2]
Sikeres memóriafoglalás: ptr[3]
A következő kivétel keletkezett: std::bad_alloc

A következő program azt igazolja, hogy az objektumok destruktorai még azelőtt lebontják az objektumot, mielőtt a hiba bekövetkezne:

test.h
#ifndef TEST_H
#define TEST_H
#include <iostream>
using std::cout;

class Test
{
     public:
           Test(double = 0);                   
           ~Test() { cout << "\nDestruktor: " << value;}
     private:
           double value;
           double *TestPtr;
};

Test::Test(double s)
{
     value = s > 0 ? s : 0;
     TestPtr = new double[300000000];
     cout << "\nKonstruktor: " << value;
}

#endif

test.cpp
#include <iostream>
using std::cout;
using std::endl;
#include <new>
using std::bad_alloc;
#include "test.h"

int main()
{   
     try
     {
           for(int i = 0; i < 50; i++)
           {
                Test obj(i);
           }
     }
     catch(bad_alloc e)
     {
           cout << "\nA kovetkező kivétel keletkezett: " << e.what() << endl;
     }
    
     return 0;
}

A kimenet:
Konstruktor: 0
Destruktor: 0
Konstruktor: 1
Destruktor: 1
Konstruktor: 2
Destruktor: 2
Konstruktor: 3
Destruktor: 3
A kovetkező kivétel keletkezett: std::bad_alloc

Amikor a dinamikus memórialefoglalás már nem lehetséges, a catch kiírja a hibaüzenetet, amit a fordító generál és ez különbözhet minden fordítónál. Lehetséges, hogy a generált hibaüzenetet saját kezűleg módosítsuk, a set_new_handler segítségével, amely szintén a C++ standard <new> fejlécében található. Ez egy függvény, amely kifejezetten a hibás memórialefoglalás esetében hívódhat meg, alapértelmezett argumentumként egy mutatót kap egy argumentum nélküli függvényre és void típust térít vissza. A programozó megírhatja a függvény törzsét, és amikor kivétel keletkezik, akkor nem a bad_alloc üzenetet kapja, hanem ez a függvény fut le.

test2_new.cpp
#include <iostream>
using std::cout;
using std::cerr;
#include <new>
#include <cstdlib>
using std::set_new_handler;

void MyHandler()
{
     cerr << "Meghívódott a saját hibafüggvény!";
     abort();
}

int main()
{
     double *ptr[50];
     set_new_handler(MyHandler); //a MyHandler függvény lesz a hibafüggvény
    
     for(int i = 0; i < 50; i++)
     {
           ptr[i] = new double[300000000];
           cout << "Sikeres memóriafoglalás: ptr[" << i << "]\n";
     }
               
     return 0;
}

A kimenet:
Sikeres memóriafoglalás: ptr[0]
Sikeres memóriafoglalás: ptr[1]
Sikeres memóriafoglalás: ptr[2]
Sikeres memóriafoglalás: ptr[3]
Meghívódott a saját hibafüggveny!

Az auto_ptr osztály
      Amikor a memórialefoglalás és a felszabadítás között keletkezik egy kivétel, az lehetetlenné teheti a memóriafelszabadítást a program befejezéséig. A standard C++ az auto_ptr osztályt kínálja fel erre a problémára, a <memory> fejlécben. Az auto_ptr objektuma egy mutató, amely a dinamikusan lefoglalt memóriaterületre mutat. Amikor az objektum elhagyja a definíciós tartományát (az értelmét), akkor automatikusan meghívódik a delete arra a területre, amelyre mutatott.
      Az auto_ptr osztály egy osztálysablon és lehetővé teszi a * és a -> operátorok használatát, hogy az objektumait általános mutatóként lehessen kezelni.

test_auto_ptr.cpp
#include <iostream>
using std::cout;
using std::endl;
#include <memory>
using std::auto_ptr;

class Integer
{
     public:
           Integer(int i = 0) : value(i){
                cout << "Konstruktor: " << value << endl;
           }
           ~Integer(){
                cout << "Destruktor: " << value << endl;
           }
           void setInteger(int i) {value = i;}
           int getInteger() {return value;}
     private:
           int value;
};

int main()
{
     auto_ptr<Integer> ptrToInteger(new Integer(7)); //Integer objektumra mutató auto_ptr objektum
     ptrToInteger->setInteger(99); //az auto_ptr objektummal módosítjuk az Integer objektumot
     cout << "getInteger: " << (*ptrToInteger).getInteger() << endl;

     return 0;
}

A kimenet:
Konstruktor: 7
getInteger: 99
Destruktor: 99

Az auto_ptr<Integer> ptrToInteger(new Integer(7)) utasítás létrehoz egy auto_ptr<Integer> típusú objektum mutatót, melynek neve ptrToInteger, és amely egy Integer típusú objektumra mutat. Ez az objektum 7-tel inicializálja a privát tagváltozóját, majd a mutató segítségével megváltoztatja és kiírja ezt a változót. Az auto_ptr osztálysablont főleg a dinamikusan lefoglalt memória felszabadításnál fellépő problémák (memory leaks) megoldására használják és „smart pointer”-ként hivatkoznak rá.

Nincsenek megjegyzések:

Megjegyzés küldése