Objektum-orientált C++ VII.
A sorozat (igazi) záró epizódjában a bejárókról lesz szó.
Annak idején, amikor a sorozat hatodik részét megírtam, még azt hittem, hogy vége a sorozatnak. De hatalmas hibát követtem el. Megfeledkeztem a bejárókról. Ezt a hatalmas hiányosságot igyekszem most hirtelen pótolni ennek a bejegyzésnek a keretein belül. Kezdjük is talán a legelején, hogy mi is az a bejáró (iterator)? A Nagy Könyv keveset mondó ám annál hangzatosabb definíciója szerint minden bejáró, ami úgy viselkedik, mint egy bejáró. Viselkedését - és operátorait - tekintve a bejáró viszont a mutatókhoz hasonlatos. Hasonlóan lehet növelni illetve csökkenteni (a ++
és a --
operátorokkal) vagy az éppen aktuális elem értékét megkapni a *
operátor segítségével. Ellenben amíg van nullpointer addig "semmire" mutató bejáró nincsen.
A bejárók lényegében egy egységes felületet kívánnak nyújtani a különböző adatszerkezetekhez, hogy egyes algoritmusoknak ne kelljen tudnia, hogy például egy zsák adatszerkezet az most tömbösen vagy láncolt listásan lett-e vajon megvalósítva. Ugyebár miért is kéne tudnia? Bizonyos algoritmusoknál ez mindegy is.
A bejárókat meghatározott műveleteik alapján csoportosítani lehet (azt pedig, hogy milyen operátorok vannak definiálva, azt az határozza meg, hogy az egyes operátorok mennyire műveletigényesek - általában csak az alacsony műveletigényű funkciókhoz szokás operátort rendelni, a nagyobbakhoz pedig függvényt). Ez a standard template library-ban szépen meg is van téve, ám én erre a részre nem kanyarodnék rá, mivel egyrészt maga megérne jópár bejegyzést (az STL és az STL bejárókkal kapcsolatos része is), másrészt pedig nem szeretném ennyire elbonyolítani a helyzetet. Ezért a korábban már megírt láncolt lista adatszerkezetünkhöz fogok egy bejárót írni. Természetesen amennyire lehetséges tartom magam az STL-ben használt elnevezésekhez és ugyanazok az operátorok itt is ugyanazt a funkciót fogják betölteni (tehát visszakanyarodva az első "bejáró" meghatározásunkhoz, ez egy bejáró lesz, mert bejáróként fog viselkedni :) ).
Szerencsére az eredeti osztályunkat csak két függvénnyel kell kiegészíteni. Az egyik a begin()
, ami egy, a lista elején álló bejáróval fog visszatérni. Az end()
pedig a lista végén áll.
typedef cList_const_iterator<T> const_iterator;
typedef cList_iterator<T> iterator;
iterator begin(void)
{
return *(new iterator(*(list->next)));
}
iterator end(void)
{
return *(new iterator(*list));
}
A mutatók mintájára bejáróból is két fajtánk lesz, egy sima és egy konstans, ahogy az a fenti kódból is látszik. A typedef
rész azért egy lényeges dolog (azon kívül, hogy a két függvényt egy csöppnyit szebbé és rövidebbé teszi), mert így cList<T>::iterator it;
formában létre tudunk hozni egy cList<T>
-beli bejárót (az STL is így viselkedik, ezért tartom magam ehhez). Na, jöhet az izgalmasabb része a dolognak, első körben a cList_iterator
osztály:
template<class T> class cList_const_iterator;
template<class T> class cList_iterator
{
private:
listNode<T>* current_item;
friend class cList_const_iterator<T>;
public:
cList_iterator(listNode<T>& value)
{
current_item = &value;
}
cList_iterator(const cList_iterator<T>& other)
{
current_item = other.current_item;
}
cList_iterator<T>& operator++()
{
current_item = current_item->next;
return *this;
}
cList_iterator<T>& operator=(cList_iterator<T>& other)
{
this->current_item = other.current_item;
return *this;
}
T& operator*()
{
return current_item->data;
}
bool operator==(const cList_iterator<T>& other)
{
return (current_item == other.current_item);
}
bool operator!=(const cList_iterator<T>& other)
{
return (current_item != other.current_item);
}
};
Bár a cList_iterator
-ral kezdünk, mégis előbb a cList_const_iterator
-t kell egy sorban definiálni, az osztályban szereplő friend sor miatt (ami azért kell, hogy majd a const_iterator
el tudja érni az iterator
private adattagját (ami kell majd a copy konstruktorához, hogy lehessen cList_iterator
-ból cList_const_iterator
-t csinálni)). A dolog teljesen egyszerű egyébként, a current_item
tárolja, hogy épp hogy vagyunk, lépdesni ugyanúgy lépdesünk vele, mint ahogy a láconlt lista kódjában is tettük. Akár a --
operátort is meg lehetne írni, mivel a listánk kétirányú volt. Érdemes még odafigyelni arra, hogy a *
operátor referenciát ad vissza, tehát (ha minden igaz :D ) szerepelhet egyenlőség bal oldalán (a bejárón keresztül megváltoztatható az aktuális elem értéke). Második kör, a cList_const_iterator
osztály. Csak azokat a funkciókat írom le, amivel bővült, illetve amik változtak (azt nem tekintem változásnak, hogy cList_iterator
helyett csak cList_const_iterator
-t kell írni):
template<class T> class cList_const_iterator
{
private:
const listNode<T>* current_item;
public:
cList_const_iterator(const cList_iterator<T>& other)
{
current_item = (const listNode<T>*)other.current_item;
}
T operator*()
{
return current_item->data;
}
};
Az utolsó ami még hiányzik, az egy kis példakód, hogy mit is tudunk kezdeni ezzel a bejáróval, amit épp megírtunk. A lent látható kód a listán való végigmenés rövid, fájdalommentes, átlátható, bejárókat használó kódja:
cList<int> int_list;
int_list << 0 << 1 << 2 << 3 << 4 << 5 << 6 << 7 << 8 << 9;
for (cList<int>::const_iterator it = int_list.begin(); it != int_list.end(); ++it) {
std::cout << *it << std::endl;
}
std::cout << "Lista vége" << std::endl;
Ennyit a saját készítésű bejárókról (az STL-ről meg talán a későbbiekben még egy nagyobb lélegzetvételű dolog születhet, ha úgy alakul - addig is ajánlott olvasmány a dologgal kapcsolatban a fentebb említett Nagy Könyv 19. fejezete). További jó iterálást mindenkinek.