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