Iteratori
Iteratori su objekti pomoću kojih iteriramo kroz spremnike. Prisutni su u svim
modernim jezicima, a specifičnost C++-a je što imaju sintaksu pokazivača.
To znači da za kretanje kroz spremnik koristimo operatore ++
i --
, a za
dohvat elementa koristimo operator dereferenciranja; iteratore uspoređujemo
pomoću operatora ==
i !=
.
Svi spremnici imaju metode begin()
i end()
koje vraćaju iteratore za
kretanje kroz spremnik:
-
begin()
je iterator koji pokazuje na prvi element u spremniku, -
end()
je iterator koji pokazuje na jedno mjesto iza zadnjeg elementa u spremniku. -
cbegin()
je konstantan iterator koji pokazuje na prvi element u spremniku, -
cend()
je konstantan iterator koji pokazuje na jedno mjesto iza zadnjeg elementa u spremniku.
Konstantan iterator ne može mijenjati objekte kroz koje iterira. Oni su za njega konstantni.
Iteratori se dijele u kategorije prema svojim sposobnostima.
-
Iteratori nikad ne provjeravaju jesu li izašli izvan granica spremnika kroz koji iteriraju.
Kategorije iteratora
Svi iteratori dozvoljavaju kretanje unaprijed pomoću operatora ++
i dereferenciranje pooću operatora *
. Derferencirani iterator može služiti
samo za čitanje, samo za pisanje ili za oboje, ovisno o kategoriji kojoj pripada.
K tome svi iteratori imaju operatore ==
i !=
za uspoređivanje.
Ulazni iterator (input iterator)
Ulazni iterator može se kretati samo unaprijed (nema operatora --
) i može jedino čitati
vrijednost sa ulaza. Kako služi uglavnom čitanju s ulaznog toka, ne može pročitati istu vrijednost
više puta. Postinkrement operator (it++
) ne mora nužno ništa vratiti.
Izlazni iterator (output iterator)
Izlazni iterator može se kretati samo unaprijed (nema operatora --
) i može jedino pisati
vrijednost na izlaz. Kako služi uglavnom za pisanje na izlazni tok, ne može pisati
više puta u istu lokaciju. Izlazni iterator ne može biti konstantan.
Jednosmjerni iterator (forward iterator)
Jednosmjerni iterator može se kretati samo unaprijed (nema operatora --
) i može pisati u lokaciju (ako nije konstantan)
i čitati iz nje. Nema ograničenja na broj pisanja/čitanja.
Takav iterator daje klasa std::forward_list<>
i (eventualno) neuređeni asocijativni spremnici.
Dvosmjerni iterator (bidirectional iterator)
Dozvoljava kretanje u oba smjera pa ima operatore ++
i --
(prefiks i postfiks verziju).
Može čitati i pisati (ako nije konstantan). Takav iterator daje klasa std::list<>
i asocijativni spremnici.
Iterator izravnog dohvata (random access iterator)
Iteratori u ovoj kategoriji imaju sva svojstva dvosmjernih iteratora i k tome podržavaju
pokazivačku aritmetiku. To znači da ako je it
iterator izravnog dohvata koji pokazuje na neku lokaciju,
onda it + n
pokazuje na n
-ti element od polazne lokacije. Dozvoljena je i sintaksa it[n]
.
Operacije dodavanja i oduzimanja cijelog broja iteratoru su dozvoljene i imaju kao efekt pomicanje
iteratora za toliko mjesta unaprijed/unazad.
Funkcije na iteratorima
U zaglavlju <iterator>
su definirane pomoćne funkcije koje rade s iteratorima:
Operacija | Značenje |
---|---|
Pomiče iterator |
|
Daje novi iterator |
|
Daje novi iterator |
|
Daje novi iterator |
|
Daje novi iterator |
|
Cjelobrojna vrijednost |
|
Vrši zamjenu dva iteratora. |
-
U pozivu
n = distance(it1,it2)
iteratorit1
može pokazivati na element nakon elementa na koji pokazujeit2
u slučaju iteratora izravnog dohvata. U slučaju dvosmjernih iteratora to vodi na nedefinirano ponašanje. -
Funkcija
std::iter_swap
je definirana u zaglavlju<algorithm>
.
Primjer:
std::list<int> li{1,2,3,4,5,6,7,8,9};
std::iter_swap(li.begin(), std::prev(li.end())); // 9,2,3,4,5,6,7,8,1,
Reverzni iteratori
Reverzni iteratori služe za iteriranje kroz spremnik u obrnutom pretku, od kraja prema početku. Na primjer:
#include <deque>
#include <algorithm>
void print(int x){
std::cout << x << ",";
}
int main()
{
std::deque<int> deq{1,2,3,4,5,6,7,8,9};
std::for_each(deq.begin(), deq.end(), print);
std::cout << std::endl; // 1,2,3,4,5,6,7,8,9,
std::for_each(deq.rbegin(), deq.rend(), print);
std::cout << std::endl; // 9,8,7,6,5,4,3,2,1,
return 0;
}
-
Metode
rbegin()
irend()
vraćaju reverzne iteratore tipaspremnik::reverse_iterator
ili, ako je reverzni iterator konstantan,spremnik::const_reverse_iterator
. U našem primjeruspremnik=std::deque<int>
. -
Metode
crbegin()
icrend()
vraćaju konstantne reverzne iteratore tipaspremnik::const_reverse_iterator
.
Konverzija u reverzni iterator i obratno
-
Konstruktor reverznog iteratora može se inicijalizirati običnim iteratorom i tada konstruktor vrši konverziju običnog u reverzni iterator. Dobiveni reverzni iterator pokazuje na jedno mjesto ispred običnog iteratora.
-
Funkcija
base()
na reverznom iteratoru vrši konverziju u obični iterator. Dobiveni obični iterator pokazuje na jedno mjesto iza reverznog iteratora.
std::deque<int> deq{1,2,3,4,5,6,7,8,9};
auto it = deq.begin() + 5;
std::deque<int>::reverse_iterator rit{it}; // konverzija u reverzni iterator
std::cout << *it << "\n"; // ispisuje 6
std::cout << *rit << "\n"; // ispisuje 5
auto rrit = rit.base(); // konverzija u običan iterator
std::cout << *rrit << "\n"; // ispisuje 6
Ovakva konstrukcija čuva raspone pri prijelazu iz običnog u reverzno iteriranje:
auto it1 = deq.begin() + 2;
auto it2 = deq.begin() + 7;
std::for_each(it1, it2, print);
std::cout << std::endl; // 3,4,5,6,7,
std::deque<int>::reverse_iterator rit1{it1}, rit2{it2};
std::for_each(rit2, rit1, print);
std::cout << std::endl; // 7,6,5,4,3,
-
Reverzan iterator se prirodno implementira tako da fizički pokazuje na istu memorijsku lokaciju kao i odgovarajući običan iterator, ali logički pokazuje na lokaciju ispred nje.
Insert iteratori
Insert iteratori su posebni iteratori koji pripadaju kategoriji izlaznih iteratora
koji prilikom dereferenciranja insertiraju element u spremnik. Ta
se operacija može obavljati pomoći metoda:push_back()
,
push_front()
i insert()
, pa imamo tri vrste takvih iteratora:
-
std::back_insert_iterator<Spremnik>
-
std::front_insert_iterator<Spremnik>
-
std::insert_iterator<Spremnik>
Svi se oni nalaze u zaglavlju <iterators>
i uzimaju kao parametar predloška tip spremnika
na kojem rade. Njihovi konstruktori uzimaju spremnik, a insert_iterator
uzima i poziciju
ubacivanja. Na primjer,
std::list<int> lst;
std::back_insert_iterator<std::list<int> > iter_b(lst);
std::front_insert_iterator<std::list<int> > iter_f(lst);
std::insert_iterator<std::list<int> > iter(lst, lst.begin());
Ovi iteratori su konstruirani tako da operator ++
ne radi ništa, operator dereferenciranja
vraća *this
(dakle ne radi ništa) i tek operator pridruživanja poziva odgovarajuću insert
metodu.
//Ubacivanje na kraj spremnika
*iter_b = 1;
iter_b++;
*iter_b = 2;
iter_b++;
*iter_b = 3; // daje 1 2 3
// Ubacivanje ispred prvog elementa
*iter_f = 4; // iter_f++ nije potreban jer ne radi ništa
*iter_f = 5;
*iter_f = 6; // daje 6 5 4 1 2 3
Pokažimo još i insert_iterator
:
auto it = std::find(lst.begin(), lst.end(), 3);
std::insert_iterator<std::list<int> > iter(lst, it);
*iter = 10;
*iter = 11;
*iter = 12; // daje 6 5 4 1 2 10 11 12 3
Uočimo da back_insert_iterator
i insert_iterator
ubacuju elemente u ispravnom poretku
dok ih front_insert_iterator
ubacuje u inverznom poretku.
Pomoćne funkcije - inserteri
Postoje tri pomoćne funkcije koje uzimaju spremnik i kreiraju odgovarajući insert iterator. To su
Definirane su u zaglavlju <iterator>
. Ove funkcje najčešće koristimo zajedno sa algoritmima.
Na primjer:
std::vector<int> vec1;
std::copy(lst.begin(), lst.end(), std::back_inserter(vec1));
// daje 6 5 4 1 2 10 11 12 3
std::deque<int> deq;
std::copy(lst.begin(), lst.end(), std::front_inserter(deq));
// daje 3 12 11 10 2 1 4 5 6
std::list<int> lst1{lst};
std::copy(lst.begin(), lst.end(),
std::inserter(lst1, std::next(lst1.begin(),3)));
// daje 6 5 4 6 5 4 1 2 10 11 12 3 1 2 10 11 12 3
Iostream iteratori (1)
Postoje dva specijalna iteratora (deklarirana u zaglavlju <iterator>
)
koja služe za čitanje s ulaznog streama i za pisanje na izlazni stream. Oba iteratora
promatraju stream kao niz vrijednosti istog tipa;
to su istream_iterator
i ostream_iterator
.
ostream_iterator
Kod ostream_iterator
-a operator dereferenciranja (*
) i operator
inkrementiranja (++
) ne rade ništa. Vrijednost na stream ispisuje
operator pridruživanja. Ovdje nemamo end-iteratora,ali konstruktor
iteratora pored streama mora kao argument dobiti i separator podataka
(kao C-string). Za pisanje se koristi operator <<.
Operacija | Značenje |
---|---|
|
Konstruktor |
|
NoOp |
|
|
|
NoOp |
|
NoOp |
// ostream iteratori
std::ostream_iterator<float> osi(std::cout, "\n");
// std::ostream_iterator<float> osi_end; -- ne postoji !
// ispis vektora
std::copy(fvec2.begin(), fvec2.end(), osi);
// drugi način da se postigne ispis:
for(auto & x : fvec2)
*osi++ = x; // dereferenciranje i inkrementiranje ne rade ništa
// dovoljno je pisati
for(auto & x : fvec2)
osi = x; // ispis radi operator pridruživanja
return 0;
}
-
U konstrukciji
delim
služi kao delimiter između dva ispisa. Može se izostaviti ako se želi ispis bez razgraničenja između članova u ispisu. Tip mu jeconst char *
.
Iostream iteratori (2)
istream_iterator
istream_iterator
služi čitanju podataka sa ulaznog streama.
Kod istream_iterator
-a operator dereferenciranja vraća pročitanu
vrijednost, a operator inkrementiranja (++) čita sljedeću vrijednost.
Čitanje završava kada se dođe do kraja streama ili kada dođe do greške.
Za čitanje se koristi operator >>. Prilikom unosa sa tastature kraj
unosa se daje s Return i Ctrl-D (linux) ili s Return i Ctrl-Z (windows).
Operacija | Značenje |
---|---|
|
Dodijeljeni konstruktor. Predstavlja end iterator. |
|
Konstruktor. Kreira |
|
Vraća pročitanu vrijednost |
|
Čita sljedeću vrijednost. |
|
Čita sljedeću vrijednost. |
// istream iteratori
std::istream_iterator<float> isi(std::cin);
std::istream_iterator<float> isi_end;
std::vector<float> fvec;
// dereferenciranje operatora vraća pročitanu vrijednost. Operator ++
// čita sljedeću vrijednost
while(isi != isi_end){
fvec.push_back(*isi++);
}
// Malo korisniji primjer -- čitanje iz datoteke.
std::ifstream in("brojevi.txt");
std::istream_iterator<float> isi_in(in);
// čitanje pri konstrukciji vektor.
std::vector<float> fvec2(isi_in, isi_end);
Obevrijeđivanje iteratora
Neke operacije mogu obezvrijediti iteratore, pokazivače i reference na elemente. Jedna takva operacija je brisanje elementa. Kod vektora pri brisanju i umetanju elementa gube vrijednost svi iteratori, pokazivači i reference nakon upisanog/obrisanog elementa. Na primjer, sljedeći kod je neispravan:
auto badValue = [](char c){ return c > 'k';};
for(auto it = s1.begin(); it != s1.end(); ++it){
if(badValue(*it)) s1.erase(it); // Neodređeno ponašanje -- it obezvrijeđen
}
Funkcija erase()
kod vektora obezvrijeđuje iterator na obrisani element i kasnije
iteratore. Ispravna verzija bi bila:
for(auto it = s1.begin(); it != s1.end(); ++it){
if(badValue(*it))
s1.erase(it--); // smanji it prije no što staru vrijednost predaš funkciji erase
}
Nije samo brisanje elementa operacija koja može obezvrijediti iterator. Kod ubacivanja novog elementa u vektor mijenja se end iterator, a ako se pri tome vektor mora realocirati zbog nedostatka prostora obezvrijeđuju se svi iteratori.
Kod deque spremnika svi iteratori mogu izgubiti vrijednost kod brisanja i ubacivanja elementa. Jedino brisanje ili ubacivanje elementa na jednom od krajeva spremnika ne obezvrijeđuje iteratore (osim onog obrisanog elementa).
Kod liste i asocijativnih spremnika (map, set, multimap, multiset) brisanje obezvrijeđuje samo iterator na obrisani element, dok ubacivanje ne obezvrijeđuje iteratore.