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