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:
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.
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.