Objektum-orientált C++ IV.

Tegyünk pontot a láncolt lista végére.

Abban a szerencsétlen helyzetben hagytuk abba legutóbb, hogy a left_insert() függvény hiányzott és a szegény objektumnak nem volt felhasználói felülete. De talán abban egyet érthetünk, hogy így is elég hosszúra nyúlt az előző bejegyzés, meg nekem se volt túl sok kedvem hajnali nem tudom hány órakor még a maradék részt kifejteni.
Ami késik, gyakran múlik, de most kivételesen nem. Szóval itt az idő, itt a hely, hogy a félbehagyott osztály elnyerje (még messze nem) végleges formáját.

Felhasználói felület

Mint ahogy már az első bejegyzésben is elmeséltem, a private és public blokkok tárgyalásánál, a felhasználói felület jó dolog, mivel megszabhatjuk vele, hogy a felhasználó milyen módon férhet hozzá az objektum adataihoz, milyen módon használhatja magát az osztályt. Kezdjük a sort a már annyiszor emlegetett left_insert()-tel:

void cList::left_insert(listNode* target, listNode* data)
{
    data->prev = target->prev;
    data->next = target;
    target->prev->next = data;
    target->prev = data;
    return;
}

Jaj, de jó. Ismét egy szép hosszú sor mutató állítgatás. Lehet sejteni, hogy mi is fog következni. Igen, rajzolni fogok. Vagyis már rajzoltam, az eredmény itt jobbra látható a zagyvaságom mellett (stílusosan, mivel ez egy balos beillesztés).
Kis képmagyarázat mielőtt belevágnánk: minden két sor egy-egy sort valósít meg a kódból (a függvény megadását, a return; utasítást és a lezáró zárójelet kihagyva). A jobb felső elem, amihez képest balra szúrjuk be a jobb alsó elemet. Tehát, az első két (kód)sorban csak annyit csinálunk, hogy a beszúrandó listaelem két mutatóját (először a hátrafelé, aztán az előrefelé mutatót) ráállítjuk a célelem előtti elemre, illetve magára a célelemre. Aztán jön az izgi rész, amikor is megbontjuk magát a listát, és befűzzük az elemünket. Először a célelem visszafelé mutatója által mutatott objektum előrefelé mutató mutatóját állítjuk rá a befűzendő elemre, majd a célelem visszafelé mutatóját.
Figyelem! A képen ez a két lépés (szándékosan) fel van cserélve, hogy rajzolási szempontból könnyebb legyen ábrázolnom a dolgot (így nem kellett kereszteznem két mutatót, ami még átláthatatlanabbá tenné az ábrát).

Huhh, haladunk ezerre. Következhet a right_insert(), ami nem okoz majd túl nagy meglepetéseket, ezért különösebb kommentárt nem is fűznék hozzá (igen, képet sem, szóval mindenki megkönnyebbülhet), beszéljen inkább a kód, meg talán az előző magyarázat/kép hatására egyértelmű is lesz a dolog:

void cList::right_insert(listNode* target, listNode* data)
{
    data->prev = target;
    data->next = target->next;
    target->next->prev = data;
    target->next = data;
    return;
}

Semmi meglepő, semmi meghökkentő. Jöjjön két függvény, amivel adatokat szedhetünk ki a listából ("név" szerint az első és az utolsó elemet):

listNode *cList::get_first(void)
{
    if (list->next == list) throw E_INVALIDITEM();
    return Get(list->next);
}
listNode* cList::get_last(void)
{
    if (list->prev == list) throw E_INVALIDITEM();
    return Get(list->prev);
}

Ismét látható, hogy mennyire hasznos tud lenni a kétirányú, ciklikus fejelemes lista. Rosszabb esetekben kénytelenek lennénk végigmenni az egész listán, hogy az utolsó elemet ki tudjuk szedni. Vagy kivételként kellene kezelni az utolsó elemet, mert a mutatója (vagy az egyik mutatója) nullpointer. De szerencsére nem kell, így meg is vagyunk a dologgal. Ezekkel az eszközökkel már nagyjából tudunk mit kezdeni a listával, de a felület a megvalósítandó feladattól függően változhat. Minden esetre én még írtam bele egy Search() függvényt is, biztos ami biztos:

listNode* cList::Search(const int& number)
{
    listNode* tmp = list->next;
    while (number != tmp->data && tmp != list) {
        tmp = tmp->next;
    }
    if (tmp == list) throw E_INVALIDITEM();
    return tmp;
}

A keresett elemre mutató mutatóval tér vissza, vagy egy kivételt dob, ha nem találja a megadott elemre. Nem egy lényeges dolog, főleg ha szükség sincs rá, mindenesetre sok vizet nem zavar. Jöjjön valami - relatíve - izgalmasabb dolog: újra operátorokat fogunk írni.

Lista operátorok

Mint azt az általános operátoros leírásnál említettem, mindig törekedjünk arra, hogy az operátor valami rá jellemző dolgot hajtson végre. Extrém példaként felhozhatnám azt, hogy mondjuk a - operátort és a + operátort a komplex számoknál felcseréli valaki. Szegény programozó, aki meg akarja használni az objektumot, csak les, hogy mi a fene van. Persze mindenki azt csinál, amit akar, de nem árt az ilyen íratlannak mondható "szabályokra" is ügyelni.

Elsőként a + operátor segítségével fogunk két listát összefűzni. A kód igazából nem olyan hatékony, mint amilyen lenni tudna. Ha például a másik objektumra nem lenne szükségünk, akkor egyszerűen a mi objektumunk végét megbontanánk és hozzácsatolnánk a másik objektumot, vigyázva arra, hogy a fejelemet ne vegyük bele a játékba. Így se lesz sokkal bonyolultabb, csak végigmegyünk a másik objektumon, és szépen hozzáadogatjuk a listánk végére az elemeket (hasonlít egy picit a dolog a copy konstruktorhoz):

cList& cList::operator+(const cList& other)
{
    listNode* tmp = other.list->next;
    while (tmp != other.list) {
        listNode* node = new listNode(tmp->data);
        if (!node) throw E_OUTOFMEMORY();

        this->left_insert(this->list, node);
        tmp = tmp->next;
    }
}

Következőnek jöhet az egyenlőség operátor. Működését tekintve csak annyit, hogy először kiürítjük a listát, majd a másik lista elemeit átmásoljuk a mi listánkba (copy konstruktorhoz és a + operátorhoz hasonlóan). Az első feltétel meg azért szükséges, hogy ha saját magát adjuk értékül a listának, akkor abból se legyen para.

cList& cList::operator=(const cList& other)
{
    if (this == &other) return *this;
    this->Destroy();

    listNode* tmp = other.list->next;
    while (tmp != other.list) {
        listNode* node = new listNode(tmp->data);
        if (!node) throw E_OUTOFMEMORY();

        this->left_insert(this->list, node);
        tmp = tmp->next;
    }
    return *this;
}

Már csak két operátor van, amit meg fogok valósítani, a << operátor és párocskája a >>. Lényegi haszna nem lesz egyiknek sem, tulajdonképpen csak hasznos álnevekként funkcionálnak majd a végére beszúrás és az utolsó elem kiszedése számára.

cList& cList::operator>>(int& number)
{
    number = get_last()->data;
    return *this;
}
cList& cList::operator>>(listNode* pointer)
{
    pointer = get_last();
    return *this;
}

cList& cList::operator<<(const int& number)
{
    listNode* tmp = new listNode(number);
    if (!tmp) throw E_OUTOFMEMORY();

    left_insert(list, tmp);
    return *this;
}
cList& cList::operator<<(listNode* pointer)
{
    left_insert(list, pointer);
    return *this;
}

A két plusz függvény tényleg csak a kódot teszi szemléletesebbé, átláthatóbbá. Bár ez a két dolog sosem lehet "csak" tényező. Legalább is nekem mindig fontos volt, hogy két-három hónap után is tudjam, hogy mit csinál az általam írt kód, és ezt eddig a Perl kivételével minden nyelvben sikerült is megtennem. :)
Pár példa a használatra:

cList lista;
lista << 6 << 7 << 1 << 12 << 23;
int szam;
lista >> szam;

Amint az látszik is, eléggé szemléletes. Először a 6-ot, 7-et, 1-et, 12-t, 23-at beletesszük a listába, majd kiszedjük az utolsó elemet (vagyis a szam értéke a végén 23 lesz).

Ezzel végére is értünk a mai napra rendelt dolgoknak. A sablonok ismét a csúszás áldozatává válnak, cserében viszont senki mással nem kell megosztaniuk majd a következő - szám szerint az ötödik - bejegyzést. Addig is további jó próbálkozást.

A sorozat további részei

Hozzáfűznél valamit?

Dobj egy emailt a blog kukac deadlime pont hu címre.

Feliratkoznál?

Az RSS feed-et ajánljuk, ha kedveled a régi jó dolgokat.