Von Containern und anderen flexiblen Konzepten
Als Einstieg in die C++-Standardbibliothek soll die Container-Library dienen.
Diese Sammlung von Datenstrukturen bietet für fast jeden Anwendungsfall ein
passendes Klassentemplate. Hier leuchten die Iteratoren nochmal hell auf. Sie
ermöglichen die flexible Verbindung von Containern mit Algorithmen und bieten so
eine hohe Wiederverwendung innerhalb der Bibliothek an. Wir fangen an mit
std::list und std::vector, die mittels Iteratoren mit einem kleinen
Algorithmus auf der Konsole ausgegeben werden können.
Video
Code
#include <iostream>
#include <vector>
#include <list>
template <typename IteratorT> void printValues(IteratorT const start, IteratorT const end)
{
for (IteratorT i=start; i != end; ++i) {
std::cerr << *i << std::endl;
}
}
int main()
{
std::vector<int> v;
v.push_back(2);
v.push_back(7);
v.push_back(4);
printValues(v.begin(), v.end());
std::list<std::string> l;
l.push_back("Eins");
l.push_back("Zwo");
l.push_back("Drei");
printValues(l.begin(), l.end());
}
Erklärung
Nicht viel Quellcode und das ist auch beabsichtigt: wir wollen ja gerade die
Bibliothek verwenden, statt alles zu Fuß zu schreiben. Die beiden Container, die
hier vorgestellt sind, unterscheiden sich doch deutlich in ihrem internen
Aufbau. std::vector ist im Prinzip ein Array auf Steroiden: es vergrößert und
verkleinert sich automatisch bei Bedarf, kann wahlfrei zugegriffen werden und
ist typsicher. Anhängen ans Ende ist amortisiert O(1), in der Mitte allerdings
O(n). Dafür ist der wahlfreie Zugriff O(1). std::list hingegen ist eine
zweifach verkettete Liste. Die Elemente liegen nicht im Speicher hintereinander,
sondern werden durch Zeiger verkettet (davon sieht der Nutzer der Klasse
allerdings nichts). Einfügen und Löschen sind O(1), dafür ist der wahlfreie
Zugriff O(n) (und kann nicht über einen Index erfolgen, wie beim std::vector).
Beide Datenstrukturen bieten geeignete Iteratoren, die sich in die von C++
vorgegebenen Konzepte einfügen. Für std::vector steht ein Random Access
Iterator zur Verfügung, std::list bietet einen Bidirectional Iterator. Beide
lassen sich daher einfach mit unserem Algorithmus zur Ausgabe eines
Datenbereiches verbinden. Die Funktion printValues verlangt zwei Iteratoren
als Parameter: einen Start-Iterator und einen Ende-Iterator. Die Funktion hält
sich dabei an das Design der Standardbibliothek: der Bereich ist immer
[start, ende), d.h. der Ende-Iterator ist immer das erste Element, welches
nicht mehr zum Bereich gehört. Jeder C++-Container stellt diese beiden
Iteratoren zur Verfügung: die Funktion begin() liefert den Anfang eines
Containers, end() liefert das Element hinter dem Ende des Containers.
Unsere Ausgabefunktion hat nun nur noch etwas ganz einfaches zu tun: sie zählt
einen Iterator i solange hoch, wie er ungleich dem Ende-Iterator ist. Dieses
Muster tritt in C++-Programmen sehr häufig auf, weswegen es auch gleich zu
Anfang mal vorgestellt werden soll. Wenn wir uns die Anforderungen an den
Iterator anschauen, so ist klar, dass die Funktion einen Input-Iterator verlangt
(dier Vergleich machts. Ohne den würde es auch ein normaler Iterator tun. Da wir
nicht mehrfach durch den Bereich laufen, brauchen wir die entsprechende
Fähigkeit des Forward Iterator nicht. Es genügt also ein Input-Iterator.) Um an
die Daten ranzukommen, die sie ausgeben muss, muss die Funktion den Iterator nur
dereferenzieren. Ob der nun ursprünglich aus einem std::vector kommt oder eine
std::list die Quelle war oder jemand einen völlig eigenen Iteratortyp
geschrieben hat, ist hier unerheblich.