Iterátorok

Czirkos Zoltán · 2019.02.27.

Mik azok az iterátorok? Hogy működnek?

1. Az iterátorok használata

A tároló osztályok általában adnak egy iterátor típust, amely segítségével az elemeik bejárhatóak. Ezt az iterátort a pointerekhez hasonlóan lehet használni, főként a ++ és * operátorokkal. Valahogy így:

std::list<int> l = { 4, 5, 6 };

for (std::list<int>::iterator it = l.begin(); it != l.end(); ++it) {
    std::cout << *it << std::endl;
}
4
5
6

Figyeljük meg, hogy itt lényegében operátorok lettek úgy definiálva, hogy az iterátorok úgy viselkedjenek, mintha pointerek lennének. Csak közben a háttérben a láncolt lista elemein lépkedünk. Kifejtve a kódot függvényhívások formájában (és közben jobban sorokra bontva):

std::list<int>::iterator it;
for (it = l.begin(); it.operator!=(l.end()); it.operator++()) {
    std::cout << it.operator*() << std::endl;
}

Látszik, hogy a tároló .begin() és .end() függvényei azok, amelyek iterátort adnak annak elejére és végére (az első elemre és az utolsó utánira, azaz balról zárt, jobbról nyílt intervallumról van szó). A következő elemre lépést, az elem elérését, és a tároló végének elérését pedig rendre az iterátor .operator++(), .operator*() és .operator!=() tagfüggvényei végzik. A ++ és a * a jellegzetes pointerművelet.

Az iterátor egy jellegzetes OOP tervezési minta. Enélkül a tárolók lényegében használhatatlanok lennének, vagy nem tudnák elrejteni a belső reprezentációjukat. Gondoljunk csak bele: a tároló felépítését, privát adattagjait el szeretnénk rejteni (hogy a tároló használóinak ne kelljen foglalkozni vele, és elrontani se tudják). Ugyanakkor ennek ellenére a tárolóban tárolt adatokhoz mégis hozzáférést kell adni. Ezt az ellentmondást oldják fel az iterátorok olyan módon, hogy az ő tagfüggvényeik ismerik a tároló felépítését, elemeit. Mindez egyébként működhetne operátorok nélkül is:

Ha nem lehetne
operátorokat
definiálni...
int_list l = { 1, 2, 3 };
int_list::iterator it;
for (it = l.begin(); it.not_equals(l.end()); it.next()) {
    std::cout << it.get_current() << std::endl;
}

C++-ban azért használunk operátorokat, mert itt ez a természetes. Más nyelvekben, pl. Javaban külön neveket kaptak ezek a függvények.

2. Konstans iterátorok

Az iterátorok * operátora a tárolóbeli elemhez referencia szerinti elérést biztosít. Így azok akár módosíthatóak is. Ahogy a pointerekből, az iterátorokból is szokás const változatot csinálni, amelynek segítségével a tárolóban lévő elemek olvashatóak, de nem írhatóak.

for (std::list<int>::const_iterator it = l.begin(); it != l.end(); ++it) {
    *it += 1;                        /* FORDÍTÁSI HIBA */
    std::cout << *it << std::endl;   /* OK */
}

Gyakori félreértés szokott lenni, hogy a const_iterator ugyanaz, mint az iterator const, pedig ez egyáltalán nem igaz. Ha így lenne, akkor felesleges lenne külön nevet adni a típusnak. A const_iterator-on keresztül az elem nem módosítható, ezzel szemben az iterator const (vagy const iterator) pedig azt jelenti, hogy maga az iterátor a változtathatatlan! Vegyük észre, hogy a fenti kódban az iterátor értéke változik; a ++it kifejezés miatt végiglépdel a listán. A fenti lista iterator-a leginkább egy int*-nak felel meg, a const_iterator-a pedig egy int const *-nak (tehát az int a konstans), nem pedig egy int * const-nak (ahol a *, azaz a pointer a konstans). A kódban egyébként egy iterátorok közötti konverzió is történik; a v.begin() egy iterator-t ad vissza, amely const_iterator-rá konvertálódik, mert a ciklus fejében definiált it változó típusa ilyen.