Die Standard Template Library (STL)
Willemers Informatik-Ecke
Die STL ist eine hochflexible Sammlung von Template-Klassen. Leider bringt diese Flexibilität eine Komplexität mit sich, die es dem Anfänger schwer macht, die STL zu verstehen. Das wird vermutlich der Grund sein, warum sie von manchen C++-Programmierern nicht benutzt wird und von einigen C++-Büchern vollständig ignoriert wird.

Herangehensweise

Damit Sie von den Möglichkeiten der STL profitieren können, wähle ich hier einen praxisorientierten, etwas vereinfachten Ansatz. Zuerst werden Sie anhand von Arrays sehen, welche Algorithmen die STL bietet. Danach erst werden Sie die Container kennen lernen. Das sind als Templates definierte Datenbehälter, in denen Sie eigene Daten ablegen können. Die Algorithmen sind eigentlich in erster Linie für diese Container geschrieben worden. Im dritten Schritt werden die Iteratoren näher betrachtet.

Algorithmen und Arrays

Die STL bietet eine Reihe von Algorithmen an, die auf Datencontainern arbeiten. Als einfachsten Datencontainer könnte man ein Array ansprechen, an dem die Algorithmen zunächst betrachtet werden. Um die Algorithmen nutzen zu können, müssen Sie die passende Header-Datei einbinden und den Namensraum auf std setzen:

#include <algorithm>
using namespace std;

Algorithmen und Iteratoren

Die Algorithmen-Bibliothek der STL enthält Funktionen, die den Bereich, auf dem sie arbeiten sollen, als Parameter übergeben bekommen. Die Bereichsgrenzen werden durch Iteratoren bestimmt. Dabei zeigt der Anfangsiterator auf das erste Element und der Enditerator auf das Element hinter dem Ende. Bei einem Array kann ein einfacher Zeiger als Iteratorersatz verwendet werden. Soll der Algorithmus auf dem ganzen Array arbeiten, wird als Anfangsiterator die Adresse des Arrays übergeben. Als Ende-Iterator wird zu dem Zeiger einfach die Anzahl der Elemente des Arrays hinzuaddiert. Damit zeigt der Ende-Iterator hinter das letzte Element.

Suchen

Die Funktion find() sucht in dem Container, dessen Grenzen durch die ersten beiden Parameter angegeben werden, nach einem Element, das im dritten Parameter spezifiziert wird. Der Rückgabewert ist ein Iterator auf das erste gefundene Element. In diesem einfachen Fall, in dem ein Array als Container dient, ist dieser Iterator einfach ein Zeiger. Sollen alle Elemente gefunden werden, kann der zurückgegebene Zeiger inkrementiert und als Anfang der nächsten Suche verwendet werden.

Beispiel

Das folgende Beispielprogramm sucht in dem Integer-Array nach der Zahl 4. Das Ergebnis ist hier ein Zeiger auf einen Integer.

[Suche nach einer Zahl in einem Array (stl/find.cpp)]

#include <iostream>
#include <algorithm>
using namespace std;
int main()
{
    int zahlen[9] = {1,2,3,4,5,6,7,8,9};
    int *found;
    found = find(zahlen, zahlen+9, 4);
    if (found!=zahlen+9)
    {
        cout << "Gefunden: " << *found << endl;
    }
    else
    {
        cout << "Nichts gefunden!" << endl;
    }
}

Fehlschlag

Wurde in dem Bereich kein Wert gefunden, dann ist der Ergebniszeiger identisch zu dem Zeiger des Bereichsendes. Dieser zeigt ja auf das erste Element hinter dem zu suchenden Bereich.

Sortieren

Die Funktion sort() sortiert einen Bereich eines Containers. Wieder wird der Bereich durch die Iteratoren angegeben. Als Container wird der Einfachheit halber wieder ein Array verwendet. Als Iteratoren dienen Zeiger auf int.

[Sortieren eines Arrays (stl/sort.cpp)]

#include <iostream>
#include <algorithm>
using namespace std;

int main()
{
    int zahlen[9] = {3,8,1,9,5,6,4,2,9};
    sort(zahlen, zahlen+9);
    for (int i=0; i<9; i++)
    {
        cout << zahlen[i]<< endl;
    }
}

Voraussetzung

Der Algorithmus sort() arbeitet auf Arrays und Vektoren, da er einen Direktzugriff über eckige Klammern auf die Elemente benötigt. [1] Ferner kann die Funktion sort() nur für Typen eingesetzt werden, für die der Kleiner-Operator definiert ist.

Kopieren: copy()

Die Funktion copy() kopiert einen Bereich eines Arrays in ein anderes Array.

[Kopieren]

int main()
{
    int zahlen[9] = {1,2,4,9,5,6,7,8,9};
    int ziel[5];
    copy(zahlen, zahlen+5, ziel);
}

Hier werden die ersten fünf Elemente des Arrays zahlen in das Array ziel kopiert. Dabei kontrolliert die Funktion copy() nicht, ob die Kapazität des Ziels überschritten wird. Die Funktion copy() kommt auch mit unterschiedlichen Containern zurecht.

Umdrehen: reverse

Die Funktion reverse() vertauscht die Elemente des Containers in der Art, dass sie nach dem Aufruf in umgekehrter Reihenfolge vorliegen. Das folgende Beispiel verwendet gleich zwei Algorithmen der STL. Zunächst wird das Argument des Programmaufrufs mit dem Aufruf von copy() in ein lokales char-Array namens wort kopiert. Anschließend wird auf dieses Array die Funktion reverse() angewandt, die die Zeichenkette umdreht.

[Übergabeparameter verdrehen (stl/reverse.cpp)]

#include <iostream>
#include <algorithm>
#include <string.h>
using namespace std;

int main(int argc, char **argv)
{
char wort[80];

    copy(argv[1], argv[1]+strlen(argv[1])+1, wort);
    reverse(wort, wort+strlen(wort));
    cout << wort << endl;
}

Beim Programmaufruf ist als Parameter ein beliebiger Text zu übergeben. Das Programm gibt als Ergebnis diesen Text in umgekehrter Reihenfolge aus.

Füllen: fill

Die Funktion fill() füllt ein Array mit Werten. Als Parameter werden der Anfang und das Ende des zu füllenden Bereichs angegeben und als dritter Parameter der Wert, mit dem der Container zu füllen ist.

[Füllen]

int main()
{
    int zahlen[9] = {1,2,4,9,5,6,7,8,9};
    fill(zahlen, zahlen+9, 4);
}

Jedes Element des Arrays zahlen wird mit dem Wert 4 gefüllt.

equal()

Mit der Funktion equal() können zwei Bereiche auf Gleichheit geprüft werden. Als Parameter werden zunächst Anfang und Ende des Ausgangsbereichs angegeben. Als dritter Parameter wird der Anfang des Bereichs angegeben, mit dem verglichen werden soll:

if (equal(zahlen, zahlen+3, vgl))

Die Anweisung hinter dem if wird ausgeführt, wenn die ersten drei Werte des Arrays zahlen gleich denen des Arrays vgl sind.

Funktionen als Parameter

Einige der STL-Funktionen rufen Funktionen auf, die ihnen als Parameter übergeben werden. Ein einfacher Vertreter ist die Funktion find_if(). Ihr dritter Parameter ist eine Funktion, deren Rückgabetyp bool sein muss und deren Parameter dem Typ entsprechen muss, den der Container aufnimmt.

Prädikat

In der Aussagenlogik wird ein boolescher Term als Prädikat bezeichnet. Diese Bezeichnung wird in der STL für Funktionen mit einem Rückgabetyp bool verwendet. Die Funktion find_if() durchsucht wie die Funktion find() einen Bereich eines Containers. Dabei wird als dritter Parameter allerdings nicht ein konstanter Wert übergeben, sondern eine Funktion, die von find_if() für jedes Element des zu durchsuchenden Bereichs aufgerufen wird. Liefert diese Funktion den Wert true, endet die Funktion find_if() und liefert den Iterator (in diesem einfachen Fall einen Zeiger) auf den gefundenen Wert als Rückgabewert. Damit find_if() das zu prüfende Element an das Prädikat übergeben kann, muss es einen Parameter des Typs akzeptieren, deren Variablen der Container als Elemente aufnimmt.

Flexibel

Mit Hilfe der Funktion find_if() kann nach Elementen gesucht werden, die nur in bestimmten Teileigenschaften gleich sind oder die aufgrund von Eigenschaften gesucht werden, die nicht durch die einfache Kombination logische Operatoren auszudrücken sind.

Beispiel

Das folgende Programm verwendet die Funktion istneun() als Prädikat. Die Funktion muss einen Übergabeparameter vom Typ int haben und bool zurückgeben. Wie ihr Name schon andeutet, wird sie wahr, wenn der übergebene Parameter 9 ist.

[Suche nach einer Zahl in einem Array]

#include <iostream>
#include <algorithm>
using namespace std;

bool istneun(int parameter)
{
    return (parameter==9);
}

int main()
{
int zahlen[9] = {1,2,3,9,5,6,7,8,9};
int *found;

    found = find_if(zahlen, zahlen+9, istneun);
    if (found!=zahlen+9)
    {
        cout << *found << " - " << endl;
    }
    else
    {
        cout << "nix" << endl;
    }
}

Da eine Funktion extrem flexibel ist, kann nahezu nach allen Kriterien gesucht werden. Beispielsweise könnte das Prädikat ermitteln, ob die Zahl eine Primzahl ist. In manchen Fällen ist eine Funktion aber immer noch nicht ausreichend flexibel. Betrachten wir den Fall, dass die erste Zahl gesucht wird, die kleiner ist als der Vorgänger im Container. Im vorhergehenden Beispiel wäre dies die 5. Als Funktion könnten Sie das so formulieren:

bool KleinerAlsVorgaenger(int parameter)
{
    static letzter = INT_MIN;
    bool rueckgabe;
    rueckgabe = parameter < letzter;
    letzter = parameter;
    return rueckgabe;
}

Wiederholungsfalle

Tatsächlich funktioniert dies beim ersten Einsatz wunderbar. Allerdings wird die Funktion direkt anschließend unbrauchbar, da sie sich in ihrer statischen Variablen letzter den Wert 5 gemerkt hat. Würde sie erneut für das gleiche Array verwendet, würde bereits die 1 als gesuchte Zahl gemeldet, da sie kleiner als 5 ist. Um diese Problematik zu lösen, könnten Sie die Variable letzter als globale Variable verwenden, die vor dem Aufruf von find_if() zurückgesetzt werden muss. Abgesehen davon, dass globale Variablen vermieden werden sollten, hinterlässt diese Lösung einen faden Nachgeschmack.

Funktionsobjekt

Wenn es um Initialisierungsprobleme geht, bietet sich der Einsatz einer Klasse an. Hier kann im Konstruktor eine Variable leicht zurückgesetzt werden. Um den Anforderungen der Funktion find_if() gerecht zu werden, kann der Funktionsoperator überladen werden. Das vollständige Programm sieht folgendermaßen aus:

[Einsatz eines Funktionsobjekts (stl/findif.cpp)]

#include <iostream>
#include <algorithm>
#include <limits.h>
using namespace std;

class tKleinerAlsVorgaenger
{
public:
    tKleinerAlsVorgaenger() { letzter = INT_MIN; }
    bool operator()(int a);
private:
    int letzter;
};

bool tKleinerAlsVorgaenger::operator()(int a)
{
    bool rueckgabe;
    rueckgabe = a < letzter;
    letzter = a;
    return rueckgabe;
}

int main()
{
int zahlen[9] = {1,2,3,9,5,6,7,8,9};
int *found;
tKleinerAlsVorgaenger rueckfall;

    found = find_if(zahlen, zahlen+9, rueckfall);
    if (found!=zahlen+9)
    {
        cout << *found << endl;
    }
    else
    {
        cout << "nix" << endl;
    }
}

Randbedingungen

Es sollte allerdings nicht unerwähnt bleiben, dass für jeden Aufruf von find_if() ein neues Objekt der Klasse tKleinerAlsVorgaenger verwendet werden muss, damit der Konstruktor das Datenelement letzter zurücksetzen kann. Alternativ könnte natürlich eine Elementfunktion Reset() in die Klasse eingebaut werden, die das übernimmt. Sie können aber auch die folgende Variante einsetzen:

found = find_if(zahlen, zahlen+9, tKleinerAlsVorgaenger());

Hier wird der Konstruktor der Klasse tKleinerAlsVorgaenger aufgerufen, der ein temporäres Objekt an die Funktion übergibt, die dann wiederum als Prädikat aufgerufen wird. Dann könnte auch die Zeile mit der Definition der Variablen rueckfall weggelassen werden.

for_each

Funktionalität

Die Funktion for_each() führt die Funktion, die sie als dritten Parameter übergeben bekommt, für alle Elemente eines Containers aus, dessen Grenzen sie in den ersten beiden Parametern übergeben bekommt. Das folgende Beispiel ruft für jedes Element die Funktion show() auf, die das Element auf dem Bildschirm ausgibt. Das Ergebnis des Programms ist also eine Ausgabe des Arrays auf dem Bildschirm.

[Ausgabe per for_each (stl/foreach.cpp)]

#include <iostream>
#include <algorithm>
using namespace std;

int show(int a)
{
    cout << a << endl;
}

int main()
{
int zahlen[9] = {1,2,4,9,5,6,7,8,9};

    for_each(zahlen, zahlen+9, show);
}

Der Parameter der Funktion show() muss ein Element des Containers akzeptieren. Der Rückgabewert sollte vom gleichen Typ sein. Die Funktion for_each() reicht den Rückgabewert der letzten Funktion an seinen Aufrufer durch.

Container und Iteratoren

Template-Container

Die Algorithmen der STL sind nicht speziell für Arrays entwickelt worden, sondern für die verschiedenen Container, die die STL dem Programmierer anbietet. Ein Container ist eine Datenstruktur, die andere Daten aufnimmt und organisiert. Ein Beispiel für solch einen Container kann eine Liste oder ein Baum sein. Da die Container auf Templates basieren, können beliebige Datentypen in den Containern abgelegt werden. Wie ein Datenbehälter mit Hilfe von Templates für beliebige Datentypen angelegt werden kann, wurde schon mit der Klasse Stack gezeigt.

Spezialisierung

Die STL bietet sehr unterschiedliche Container. Sie sind für bestimmte Einsätze spezialisiert. So sind Vektoren optimal, wenn auf die einzelnen Elemente wahlfrei zugegriffen werden muss. Eine Liste ist ideal, wenn die Anzahl der Elemente extrem schwankt, und Bäume eignen sich für Daten, die sortiert abgelegt und schnell aufgefunden werden sollen.

Iterator

Für den Zugriff auf die Elemente der Container verwendet die STL Iteratoren. Sie sind Zeigern sehr ähnlich, sind aber an ihre Container angepasst. Sie bilden das Glied zwischen Container und Anwendung.

Die Container-Klasse vector

Flexibles Array

Der Vektor ist wohl der Container, der am einfachsten zu verstehen ist. Er ähnelt dem Array, ist aber wesentlich flexibler. Wie bei einem Array befinden sich alle Elemente direkt nebeneinander im Speicher, und man kann jeweils über ihre Positionsnummer direkt auf sie zugreifen. Sogar die rechteckigen Klammern arbeiten, wie man es vom Array her kennt.

Im folgenden Beispiel werden die Lottozahlen wie bekannt mit einem Array realisiert:

int main() {
    const int MAX = 6;
    int lotto[MAX];

    for (int i=0; i<MAX; i++)
    {
        lotto[i] = 1+i*i;
    }
}
Das Programm kann leicht vom Array auf einen vector umgesetzt werden:
#include <vector>

int main() {
    const int MAX = 6;
    std::vector<int> lotto(MAX);

    for (int i=0; i<MAX; i++)
    {
        lotto[i] = 1+i*i;
    }
}

Dynamik

Im Falle von Lotto wissen wir, dass es sechs Werte gibt und dass da es auch nicht überraschend mehr werden. In vielen Fällen weiß man aber zuvor nicht, wie viele Werte zu erwarten sind.

Im folgenden Beispiel wird der Vektor ohne Angabe einer Größe definiert. Die Vektor-Funktion push_back fügt am Ende jeweils einen Wert zum Vektor hinzu. Die Größe des Vektors ändert sich dabei ständig. Die aktuelle Größe des Vektors liefert dessen Funktion size.

#include <iostream>
#include <vector>

int main() {
    const int MAX = 6;
    std::vector<int> lotto;
    for (int i=0; i<MAX; i++)
    {
        lotto.push_back(1+i*i);
    }
    for (int i=0; i<lotto.size(); i++)
    {
        std::cout << lotto[i] << " ";
    }
    std::cout << std::endl;
    std::cout << "Größe: " << lotto.size() << std::endl;
}

Kapazität und Größe

Damit nicht bei jedem Hinzufügen erneut Speicher angefordert werden muss, werden bei Größenänderungen vorausschauend bereits ein paar Felder mehr reserviert. Diese Ausdehnung nennt man Kapazität. Dies ist die Reserve, die gefüllt werden kann, bevor eine Vergrößerung dazu führt, dass ein neuer Speicherbereich angelegt und der bisherige Vektor umkopiert werden muss.

Grenzüberschreitung

Der Zugriff außerhalb der Kapazität führt durchaus nicht zum Abbruch des Programms. Die Zahl 12 wird außerhalb des reservierten Bereichs abgelegt. Sie können den Wert zwar anschließend auch wieder ausgeben. Er befindet sich aber nicht im Kontrollbereich des Vektors. Sie wissen also nicht, wohin der Wert gerät und müssen damit rechnen, dass der Wert in eine andere Variable hineinreicht und unkalkulierbare Fehler erzeugt.

#include <vector>

int main()
{
    std::vector<int> v(16);
    v[2000] = 12; // Unkontrollierte Speicherveränderung

Der Zugriff über die rechteckigen Klammern soll möglichst effizient laufen. Also verzichtet C++ auf eine Prüfung. Wenn aber die Überprüfung gewünscht wird, können Sie den Zugriff über die Elementfunktion at ausführen.

#include 

int main()
{
    std::vector v(16);
    v.at(2000) = 12; // Zugriff wird geprüft.

Im Gegensatz zu dem Zugriff über die eckigen Klammern wird der Zugriff über die Elementfunktion at() geprüft und löst bei Fehlern eine Ausnahme out_of_range aus.

Algorithmen

Die Algorithmen-Bibliothek, die oben anhand von Arrays vorgeführt wurden, arbeitet ebenso auf einem Vektor. Eigentlich ist die Algorithmen-Bibliothek der STL sogar in erster Linie für die Container entwickelt worden. Dass sie auch auf Arrays funktionieren, ist eigentlich ein Nebeneffekt.

Beispiel

Betrachten wir also die Sortierung eines Vektors. Zunächst wird der Vektor mit Zufallszahlen gefüllt. Anschließend wird die Funktion sort() mit dem Anfangs- und Enditerator als Parameter aufgerufen.

[Sortieren eines Vektors]

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

int main()
{
    int MaxVec = 9;
    vector<int> zahlen(MaxVec);

    srand(0);
    for(int i=0; i<MaxVec; i++)
    {
        zahlen[i] = rand() % 9 + 1;
    }

    sort(zahlen.begin(), zahlen.end());

    for (int i=0; i<MaxVec; i++)
    {
        cout << zahlen[i]<< endl;
    }
}

Iteratoren

Beim Aufruf der Funktion sort() werden die Grenzen nicht, wie beim Array, durch die Zeiger auf die erste und hinter die letzte Position bestimmt. Im Gegensatz zu einer Array-Variablen besteht das Objekt zahlen der Klasse vector ja nicht aus den Datenelementen. Ein Zeiger auf zahlen zeigt auf das Objekt, das die Daten verwaltet. Um die Grenzen des Vektors zu beschreiben, gibt es Iteratoren. Alle Container-Klassen haben die Elementfunktionen begin(), um einen Iterator auf das erste Element zu liefern, und die Elementfunktion end(), die den Iterator zurückgibt, der hinter das letzte Element zeigt. Der Iterator eines Vektors ist einem Zeiger recht ähnlich, da die Struktur linear im Speicher liegt. Man kann nicht nur vorwärts und rückwärts durch den Container laufen, sondern auch direkt auf einzelne Elemente zugreifen. Aufgrund der leichten Handhabbarkeit wird dieser Iterator auch »Random Access Iterator« genannt, übersetzt etwa »Iterator mit wahlfreiem Zugriff«.

Binäres Suchen

Liegt ein sortierter Bereich in einem Vektor vor, kann mit Hilfe der Funktion binary_search() darin nach einem Wert gesucht werden. Diese Suche ist sehr schnell, aber abhängig davon, dass direkt auf die Elemente eines Containers zugegriffen werden kann. Dementsprechend kann er für Vektoren und auch für Arrays eingesetzt werden, aber nicht für verkettete Listen.

if (binary_search(zahlen.begin(), zahlen.end(), SuchWert))

Die Funktion liefert einen booleschen Wert zurück, je nachdem, ob der Wert gefunden wurde oder nicht. Die nachfolgende Tabelle zeigt die wichtigsten Elementfunktionen des Containers vector in einer Übersicht: [Die wichtigsten Elementfunktionen von vector]
Funktion Bedeutung
begin() Liefert die erste Position des Vektors
end() Zeigt hinter die letzte Position des Vektors
at(int) Geprüfter Zugriff auf Element des Vektors
size() Liefert die Anzahl der Elemente eines Vektors
capacity() Liefert die Kapazität eines Vektors
resize(int) Verändert die Größe
reserve(int) Verändert die Kapazität

Vektor als Stack

push_back()

Für den Vektor sind Funktionen definiert, die es ermöglichen, mit dem Vektor einen Stack zu realisieren. Durch Aufruf der Elementfunktion push_back() können am Ende des Vektors Elemente hinzugefügt werden. Als Parameter wird der Funktion ein Wert des Typs übergeben, über den der Vektor definiert ist. Der Vektor wächst in seiner Größe um eins.

back()

Um das letzte Element zu lesen, gibt es die Funktion back(). Die Funktion hat keine Parameter und liefert nur den Wert des letzten Elements. Der Stack, respektive der Vektor, wird nicht verkleinert.

pop_back()

Mit der Funktion pop_back() wird ein Element vom Stack entfernt. Auch diese Funktion hat keine Parameter. Sie hat den Rückgabetyp void, liefert also weder den Wert des letzten Elements, noch gibt sie an, ob die Operation erfolgreich war. Das ist besonders deswegen bedauerlich, weil beispielsweise der GNU-Compiler bei einem überzähligen Pop die Vektorgröße auf 4.294.967.295 setzt. Der Wert muss also vor dem Aufruf von pop_back() mit der Funktion back() ausgelesen werden. Die Applikation muss sicherstellen, dass kein Pop auf einen leeren Stack ausgeführt wird.

[Stack-Funktionen des Vektors]

#include <iostream>
#include <vector>
using namespace std;

int main()
{
    vector<int> Zahlen;
    Zahlen.push_back(8);
    Zahlen.push_back(7);
    Zahlen.push_back(6);
    do
    {
        cout << ": " << Zahlen.back();
        Zahlen.pop_back();
    } while (Zahlen.size());
    cout << endl;
}

Die Container-Klasse deque

Der Container deque (Das Wort wird wie »Deck« ausgesprochen. Vermutlich wird man Sie besser verstehen, wenn Sie »double ended queue« sagen.) verhält sich weitgehend wie ein Vektor. Beispielsweise ist der Indexoperator verfügbar. Sie können also über die eckigen Klammern auf ein Element zugreifen. Allerdings gibt es keine Möglichkeit, die Kapazität festzulegen oder zu erfragen. Der Name »double ended queue« (übersetzt etwa: Schlange mit zwei Enden) deutet schon an, dass der Container nicht nur am Ende wachsen kann, sondern auch am Anfang. Sie können sich eine Deque bildlich wie zwei Vektoren vorstellen. Der eine Vektor realisiert den Anfang, der andere das Ende der Deque. Die Indexnummern werden vom ersten Element bis zum letzten durchgezählt. Kommt ein Element am Anfang hinzu, wird die Nummerierung für jedes Element um eins erhöht, und das neue Element bekommt die Nummer 0. Für den Anwender der Deque entsteht der Eindruck, als wären alle Elemente um eine Position verschoben worden.

Hier steht im Buch die Abbildung "Deque" (picdeque)

Funktionen

Um am Anfang der Liste arbeiten zu können, gibt es Funktionen, die denen für das Ende der Liste entsprechen. Die Funktion front() liefert das erste Element. Mit der Funktion push_front() wird am Anfang einer Deque ein neues Element eingehängt. Die Funktion erwartet als Parameter das neue Element. Schließlich gibt es die Funktion pop_front(), die ein Element vom Anfang der Deque entfernt. Um die Operationen am Ende der Deque auszuführen, werden die vom Vektor bekannten Funktionen back(), push_back() und pop_back() verwendet.

gpBeispiel

Das folgende Beispiel zeigt einen FIFO-Puffer (FIFO: first in first out). Jede Warteschlange ist ein FIFO. Wer sich zuerst angestellt hat, kommt auch als erstes an die Reihe. Die Elemente kommen also in der Reihenfolge zum Vorschein, wie sie hineingestellt wurden. Im Beispiel werden tausend mal tausend Elemente in den Puffer geschoben und gleich anschließend wieder herausgeholt. Die Funktion clock() zählt die CPU-Ticks, wird also zur Messung der Laufzeit herangezogen.

[Deque als FIFO (stl/deque.cpp)]

#include <iostream>
#include <deque>
using namespace std;
#include <time.h>

int main()
{
    clock_t start, end;
    start = clock();

    deque<int> Puffer;
    for(int i=0; i<1000; i++)
    {
        for (int j=1; j<1000; j++)
        {
            Puffer.push_front(j);
        }
        do
        {
            Puffer.pop_back();
        } while (Puffer.size());
    }
    end = clock();
    cout << start << ":" << end << endl;
}

Wenn Sie statt deque den Container list aus dem nächsten Abschnitt verwenden, werden Sie feststellen, dass die Deque wesentlich schneller ist. Der Grund liegt in der internen Struktur. Die Messung zeigt, dass es wichtig ist, zu wissen, welcher Container für die gewünschte Anwendung optimal ist. Die folgende Aufstellung fasst die Funktionen zusammen, mit denen am Anfang und am Ende Elemente ein- und ausgehängt werden können:

push_front(obj)
Am Anfang der Liste wird ein Objekt eingefügt.
push_back(obj)
Am Ende der Liste wird ein Objekt eingefügt.
pop_front(obj)
Am Anfang der Liste wird ein Objekt entnommen.
pop_back(obj)
Am Ende der Liste wird ein Objekt entnommen.
front(obj)
Liefert eine Referenz auf das Element am Anfang der Liste.
back(obj)
Liefert eine Referenz auf das Element am Ende der Liste.
Es ist zwar möglich, Elemente aus der Mitte einzufügen und zu löschen, das ist allerdings weniger effizient. Sind diese Operationen häufiger erforderlich, sollte der Container list verwendet werden. Das Einfügen erfolgt über die Elementfunktion insert(). Elemente werden mit erase() wieder gelöscht.

Die Container-Klasse list

Doppelt verkettet

Während der Vektor einem Array gleicht, ähnelt der Container list eher einer doppelt verketteten Liste und dürfte in den meisten Fällen auch so implementiert sein. Eine verkettete Liste wurde bereits bei der Programmierung des Stacks an verschiedenen Stellen des Buches vorgestellt. Dabei zeigt jedes Element mit einem Zeiger auf das nachfolgende Element. Die Elemente können vereinzelt an beliebiger Stelle im Speicher stehen. Darum muss dieser Container auch nicht umkopiert werden, wenn er wächst. Ein neues Element wird angefordert und in die Kette integriert. Um an das letzte Element zu gelangen, muss sich das Programm von Element zu Element durchhangeln. Eine doppelt verkettete Liste ist so aufgebaut, dass jedes Element zwei Zeiger besitzt. Der eine Zeiger zeigt auf den Nachfolger, der andere auf den Vorgänger in der Liste. Das führt zu einem gewissen Mehraufwand beim Einhängen neuer Elemente. Dafür ist es leichter möglich, an beiden Enden Elemente an- oder abzuhängen und in beiden Richtungen zu navigieren.

Vor- und Nachteile

Gegenüber der Deque oder dem Vektor ist eine Liste vor allem dann im Vorteil, wenn in der Mitte der Liste neue Elemente eingehängt oder gelöscht werden sollen. In einer Liste müssen die anderen Elemente ja nicht verschoben zu werden, um Platz zu schaffen oder eine Lücke zu schließen. Beim Einfügen mit insert() wird Platz für ein Element angefordert. Es wird an der Stelle in die Kette eingehängt, die der Iterator bezeichnet. Die Nachbarelemente müssen nicht nebeneinander angeordnet sein, und darum müssen sie auch nicht verschoben werden. Das Gleiche gilt für das Löschen aus der Mitte der Liste mit erase(). Die Lücke muss nicht aufgefüllt werden. Der Preis dafür ist, dass es nicht sehr effizient ist, auf ein bestimmtes Element der Liste über seine Position zuzugreifen. Um das fünfzigste Element zu erreichen, muss sich das Programm fünfzigmal weiterhangeln. Aus diesem Grund unterstützt die Liste einen Zugriff über die eckigen Klammern nicht.

Listen-Iteratoren

Um eine Stelle in der Liste zu bezeichnen, an der neue Elemente eingefügt oder existierende gelöscht werden sollen, benötigen Sie einen Iterator. Jeder Container enthält die Definition für einen auf ihn passenden Iterator unter dem Namen iterator. Um einen solchen Iterator zu definieren, muss zunächst der Containertyp genannt werden. Dann folgen zwei Doppelpunkte, um anzuzeigen, dass der in dieser Klasse definierte Iteratortyp gemeint ist, und dann natürlich der Name des Iterators. Die folgende Zeile definiert einen Iterator für eine Integer-Liste:

list<int>::iterator IntegerListenIterator;

begin() und end()

Um die Iteratoren zu benutzen, müssen sie initialisiert werden. Sie werden zunächst typischerweise auf den Anfang der Liste gesetzt. Den Anfangsiterator eines Containers liefert immer die Elementfunktion begin(). Um durch den Container zu laufen, wird der Iterator inkrementiert. Die Elementfunktion end() des Containers liefert die Iteratorposition hinter dem letzten Element. Sobald also der Laufparameter gleich dem Rückgabewert von end() ist, ist der Durchlauf beendet. Daraus ergibt sich folgende typische for-Schleife zum Durchlaufen einer Liste:

list<int>::iterator Iter;
for(Iter=Liste.begin(); Iter!=Liste.end(); ++Iter)
Wird innerhalb einer solchen Schleife eine Position gefunden, an der ein neues Element eingefügt werden soll, so wird Iter als erster Parameter für die Elementfunktion insert() verwendet. Der Iterator zeigt auf das Listenelement, vor dem das neue Element eingefügt werden soll.

Liste.insert(Iter, NeuesElement);

Zum Entfernen eines Elements aus der Liste wird die Funktion erase() verwendet. Ihr einziger Parameter ist der Iterator, der auf das Listenelement zeigt, das Sie löschen wollen. Da dieser Iterator durch das Löschen ungültig wird, liefert die Funktion erase() den Iterator, der auf das nun nächste Element zeigt. Dieser Iterator wird dann benötigt, wenn nach dem Löschen die Liste weiter durchlaufen werden soll.

Iter = Liste.erase(Iter);

Das Löschen ist ein wenig kitzlich, da man leicht den Ast absägt, auf dem man sitzt.

for (auto iter=c.begin(); iter!=c.end(); /* nicht inkrementieren */ ) {
  if (kannWeg(iter))
    iter = c.erase(i);
  else
    ++iter;
}

Beispiel

Im folgenden Listing wird ein wenig mit einer Liste herumgespielt, um die verschiedenen Funktionsaufrufe zu demonstrieren.

[Listenspielereien]

#include <iostream>
#include <list>
using namespace std;

int main()
{
    list<int> zahlen;
    list<int>::iterator lIter; // Iterator definieren

    srand(0);
    for(int i=0; i<10; i++)
    {
        zahlen.push_back( rand() % 9 + 1 );
    }
    // Durchlaufen und anzeigen
    for(lIter=zahlen.begin(); lIter!=zahlen.end(); ++lIter)
    {
        cout << *lIter << endl;
    }
    // vor jeder 8 eine -1 einfügen und jede 2 löschen
    for(lIter=zahlen.begin(); lIter!=zahlen.end(); ++lIter)
    {
        if (8==*lIter)
        {
            zahlen.insert(lIter, -1);
        }
        else if (2==*lIter)
        {
            lIter = zahlen.erase(lIter);
        }
    }
    // Durchlaufen und anzeigen
    for(lIter=zahlen.begin(); lIter!=zahlen.end(); ++lIter)
    {
        cout << *lIter << endl;
    }
}

Auch hier wird das Programm Schritt für Schritt erläutert. Das Listing beginnt mit der Definition der Liste:

list<int> zahlen;

Die Liste nimmt int-Werte auf. Sie soll Zahlen enthalten, darum heißt sie etwas phantasielos zahlen.

list<int>::iterator lIter; // Iterator definieren

Als Nächstes wird ein Iterator angelegt. Er soll später durch die Liste laufen und die Basis für das Einfügen und Löschen von Elementen bilden. Vorher soll aber zunächst die Liste gefüllt werden:

for(int i=0; i<10; i++)
{
    zahlen.push_back( rand() % 9 + 1 );
}

In dieser for-Schleife werden einfach zehn Zufallszahlen gebildet und mit Hilfe der Elementfunktion push_back() an das Ende der Liste angehängt.

// Durchlaufen und anzeigen
for(lIter=zahlen.begin(); lIter!=zahlen.end(); ++lIter)
{
    cout << *lIter << endl;
}

Hier sehen Sie die Funktionsweise des Iterators. Als Startanweisung wird der Iterator auf den Anfang des Containers gesetzt. Die Elementfunktion begin() gibt einen Iterator zurück, der auf den Anfang eines Containers zeigt. Die Elementfunktion end() liefert die Position hinter dem letzten Element. Sobald der Iterator das Ende der Liste erreicht hat, wird er den gleichen Wert haben, den die Funktion end() liefert. Solange der Iterator ungleich dem Rückgabewert von end() ist, wird die Schleife nicht verlassen. Der Iterator kann durch den Inkrementierungsoperator erhöht werden. Innerhalb der Schleife zeigt sich eine weitere Eigenschaft des Iterators. Er kann beim Zugriff auf das Objekt wie ein Zeiger verwendet werden. Dem Iterator wird ein Stern vorangestellt, um das Element zu erreichen, auf das er zeigt. Anschließend beginnt wiederum eine Schleife, die durch die Liste läuft. Hier soll zur Demonstration des Einfügens und Löschens vor jede 8 eine -1 eingefügt werden und jede 2 gelöscht werden.

if (8==*lIter)
{
    zahlen.insert(lIter, -1);

Ob das Listenelement eine 8 enthält, wird wieder geprüft, indem mit dem Stern über den Iterator zugegriffen wird. Dann wird die Elementfunktion insert() der Liste aufgerufen. Als ersten Parameter enthält sie den aktuellen Iterator. Das neue Element wird an der Position des Iterators eingefügt - und damit vor dem aktuellen Element. Der zweite Parameter von insert() enthält das neu einzufügende Objekt, hier die -1.

lIter = zahlen.erase(lIter);

Zum Löschen wird die Funktion erase() verwendet. Als Parameter bekommt sie den Iterator auf das Objekt, das gelöscht werden soll. Der Iterator, der als Parameter übergeben wird, wird nach dem Aufruf ungültig. Das Element, auf das er zeigte, wurde ja gelöscht. Der Rückgabewert von erase() liefert aber den nächsten gültigen Iterator. Durch Zuweisung des Rückgabewertes an den bisherigen Iterator kann mit ihm einfach weitergearbeitet werden.

Umhängen von Elementen: splice

Mit der Elementfunktion splice() werden Elemente einer Liste in eine andere Liste verschoben. Derartige Vorgänge lassen sich mit Listen extrem effizient realisieren, weil die eigentlichen Daten nicht bewegt werden müssen, sondern nur ein paar Zeiger umgehängt werden. Je nach ihren Parametern führt die Funktion splice() leicht abgewandelte Operationen aus. So können einzelne Elemente, aber auch ganze Listen umgehängt werden.

Beispiel

Um die verschiedenen Varianten zu demonstrieren, werden zwei Listen vorbereitet. Die Liste Quelle enthält die Elemente 0, 1, 2, 3 und die Liste Ziel die Elemente 100 und 200. Der Iterator Pos zeigt auf das Element 200 der Liste Ziel. Der Iterator QuellAnf zeigt auf 1, der Iterator QuellEnde auf 3.

Liste einfügen

Die Funktion splice() fügt in die aktuelle Liste vor der Position Pos die Elemente der Liste OrgList ein.

void splice(iterator Pos, list<T>& Quelle);

Der Iterator Pos muss ein Iterator der Liste sein, für die splice() ausgeführt wird. Der Aufruf lautet:

Ziel.splice(Pos, Quelle);

Danach enthält die Liste Ziel die Elemente 100, 0, 1, 2, 3, 200. Die Liste Quelle ist anschließend leer.

Einzelelement umhängen

Mit der Funktion splice() kann auch ein einzelnes Element von einer Liste in die andere umgehängt werden.

void splice(iterator Pos, list<T>& Quelle, iterator QuellAnf);

Mit diesen Parametern fügt die Funktion splice() in die aktuelle Liste vor der Position Pos das Element ein, das an der Position QuellAnf der Liste Quelle steht. Dabei wird das Element QuellAnf aus der Liste Quelle entfernt. Der Aufruf lautet:

Ziel.splice(Pos, Quelle, QuellAnf);

Danach enthält die Liste Ziel die Elemente 100, 1, 200. Die Liste Quelle enthält 0, 2, 3.

Teilliste umhängen

In der letzten Variante werden aufeinander folgende Elemente der Liste Quelle umgehängt.

void splice(iterator Pos, list<T>& Quelle, 
            iterator QuellAnf, iterator QuellEnde);

In dieser Variante werden alle Elemente der Liste Quelle von QuellAnf bis QuellEnde an die Position Pos umgehängt. Wie bei den Iteratoren üblich, zeigt auch hier QuellEnde als Endebegrenzer auf die Position hinter dem letzten Element, das mitgenommen wird.

Ziel.splice(Pos, Quelle, QuellAnf, QuellEnde);

Danach enthält die Liste Ziel die Elemente 100, 1, 2, 200. Die Liste Quelle enthält 0, 3.

Mischen sortierter Listen

Die Elementfunktion merge() erlaubt es, in eine sortierte Liste eine andere sortierte Liste einzusortieren. Die Zielliste enthält dann alle Elemente in sortierter Reihenfolge. Die eingefügte Liste ist nach dem Aufruf leer.

Ziel.merge(Quelle);

Container-Adapter

Container-Adapter basieren auf Containern, um spezielle Schnittstellen zu liefern. So kann ein Stack beispielsweise mit Hilfe eines Vektors realisiert werden. Der Stack bietet dem Benutzer nur eine eingeschränkte Schnittstelle an. Ein Adapter besitzt keine Iteratoren. Auf der Basis der sequenziellen Container vector, deque und list werden die Container-Adapter stack, queue und prioritiy_queue definiert.

Container-Adapter stack

Ein Stack speichert seine Elemente in der Reihenfolge ihres Eintreffens und gibt sie in umgekehrter Reihenfolge wieder heraus. Er entspricht also einem Stapel, auf den nur von oben zugegriffen werden kann. Der Container-Adapter stack bietet als Schnittstelle die folgenden drei Funktionen an:
push(obj)
Das Objekt wird auf den Stack gelegt.
top()
Das oberste Objekt des Stapels wird als Rückgabewert geliefert. Es wird jedoch nicht vom Stack entfernt.
pop()
Das oberste Objekt wird vom Stack entfernt. Es gibt keinen Rückgabewert.
Um also ein Objekt vom Stack zu lesen und herunterzunehmen, müssen die Funktionen top() und pop() nacheinander aufgerufen werden. Ein kleines Beispiel zeigt, wie ein solcher Stack verwendet wird.

[Stack-Adapter (stl/stack.cpp)]

#include <iostream>
#include <stack>
using namespace std;

int main()
{
   stack<int> Stapel;
   Stapel.push(3);
   Stapel.push(2);
   Stapel.push(1);
   while (!Stapel.empty())
   {
       cout << Stapel.top() << endl;
       Stapel.pop();
   }
}

Standardmäßig wird ein Stack auf der Basis einer Deque implementiert. Wollen Sie den Stack als Liste repräsentiert wissen, müssen Sie auch list einbinden. Schließlich wird bei der Definition des Stacks angegeben, welcher Container als Basis dient. Sie müssen folgende Änderungen vornehmen:

#include <list>
#include <stack>
...
   stack<int, list<int> > Stapel;

Das Leerzeichen zwischen den beiden Kleiner-Zeichen vor dem Wort »Stapel« sollten Sie nicht vergessen. Es kann Ihnen sonst passieren, dass einer der Compiler das doppelte Größer-Zeichen für einen Umleitungsoperator hält.

Der Container-Adapter queue

Analog zum Stack gibt es einen Adapter, der als FIFO arbeitet: die Queue. Eine Queue besitzt zusätzlich zu den Funktionen des Container-Adapters stack die Funktionen front() und back(), die jeweils ein Element zurückgeben. Dafür gibt es die Funktion top() nicht mehr.
push(obj)
Ein Objekt wird hinten in den Puffer hineingeschoben.
pop()
entnimmt vorne das zuerst eingefügte Objekt.
front()
liefert eine Referenz auf das vorderste Element.
back()
liefert eine Referenz auf das zuletzt eingestellte Element.

[Queue (stl/queue.cpp)]

#include <iostream>
//#include <list>
#include <queue>
using namespace std;

int main()
{
   queue<int> FIFO;
//   queue<int, list<int> > FIFO;
   FIFO.push(4);
   FIFO.push(3);
   FIFO.push(2);
   FIFO.push(1);
   while (!FIFO.empty())
   {
       cout << FIFO.front() << endl;
       FIFO.pop();
   }
}

Die auskommentierten Zeilen können Sie verwenden, um die Queue auf Basis einer Liste einzusetzen. Standardmäßig wird eine Deque verwendet.

Container-Adapter priority_queue

Dieser Adapter unterscheidet sich von einer queue dadurch, dass die eingeschobenen Elemente eine Priorität mitbekommen. Dabei sind die Funktionen mit denen eines Stacks vergleichbar. Die Warteschlange wird als priority_queue definiert. Der Unterschied zeigt sich im internen Verhalten: Die Warteschlange prüft die Elemente mit Hilfe des Kleiner-Operators auf ihre Priorität und liefert das Element mit der höchsten Priorität als Erstes wieder aus. In dem folgenden Beispiel werden ganze Zahlen in die Warteschlange geschoben, die dann sortiert wieder herausgegeben werden. In einem realen Einsatz würden Sie natürlich eine Datenklasse definieren, die den Kleiner-Operator überlädt und damit die Prioritäten liefern kann.

[Priority-Queue (stl/pqueue.cpp)]

#include <iostream>
#include <queue>
using namespace std;

int main()
{
   priority_queue<int> FIFO;
   FIFO.push(4);
   FIFO.push(7);
   FIFO.push(5);
   FIFO.push(1);
   while (!FIFO.empty())
   {
       cout << FIFO.top() << endl;
       FIFO.pop();
   }
}

Die Container-Klassen set und multiset

Der Container set stellt die eingefügten Elemente sofort in einer sortierten Reihenfolge ab. In einem set darf jedes Element nur einmal auftreten. In einem multiset dürfen Elemente dagegen mehrfach auftreten. Der Container set ist ein assoziativer Container. Das heißt, dass er die Elemente nach einem Schlüsselwert in einem Binärbaum sortiert.

Vor- und Nachteile

Wenn Sie Daten nach einem Sortierkriterium in einer Struktur ablegen wollen und schnell wieder auf sie zugreifen wollen, dann ist der Container set die ideale Struktur. In einer Liste erfolgt das unsortierte Einfügen zwar sehr effizient, aber das Auffinden der entsprechenden Position kostet zu viel Zeit, und das Sortieren muss nachträglich durch den Aufruf von sort() erfolgen.

Einfügen und Löschen

Die Operationen für das Einfügen, insert(), und das Löschen, erase(), benötigen beim Container set nun keinen Iterator mehr, sondern erwarten den Wert, der eingefügt oder gelöscht werden soll. Die Funktion insert() liefert einen Iterator zurück, der auf das neu eingefügte Element zeigt. Das folgende Beispiel fügt zufällige Zahlen in ein Set ein. Doppelte Zahlen werden nicht eingefügt, da es sich um ein Set und nicht um ein Multi-Set handelt. Anschließend wird die Zahl 4 aus dem Set wieder gelöscht.

[Einfügen und Löschen in set (stl/set.cpp)]

#include <iostream>
#include <set>
using namespace std;

void show(set<int> &zahlen)
{
    set<int>::iterator iter;
    for(iter=zahlen.begin(); iter!=zahlen.end(); ++iter)
    {
        cout << *iter << " - " ;
    }
    cout << endl;
}

int main()
{
    set<int> zahlen;
    int neuzahl;
    srand(0);
    for(int i=0; i<10; i++)
    {
        neuzahl = rand() % 9 + 1 ;
        zahlen.insert(neuzahl);
        cout << neuzahl << " - " ;
    }
    cout << endl;
    show(zahlen);
    zahlen.erase(4);
    show(zahlen);
}

Suchen

Das Suchen in einem Binärbaum ist sehr effizient. Darum ist die Elementfunktion find() des Containers set auch wesentlich schneller als die allgemeine Funktion find(), die für alle Container gedacht ist. Die Funktion erwartet als Parameter den zu suchenden Wert und liefert als Ergebnis einen Iterator, der auf das Element zeigt. Gibt es das Element im Container set nicht, ist der Iterator identisch zu dem Rückgabewert der Elementfunktion end().

Sortierung

Damit der Container set Elemente in der richtigen Reihenfolge einsortieren kann, muss für den Typ der Elemente das Kleiner-Zeichen definiert sein. Alternativ kann beim Anlegen eines Containers set ein Prädikat für den Typ angegeben werden, das die Kleiner-Relation ersetzt. Dazu wird eine Klasse als Funktionsobjekt gebildet, die den Vergleich liefert. Im folgenden Beispiel wird ein Prädikat erstellt, das statt eines Kleiner-Vergleichs, eine Größer-Relation bildet. Dadurch wird die Reihenfolge des Sets umgedreht.

[Vergleichsklasse (stl/setsort.cpp)]

#include <iostream>
#include <set>
using namespace std;

class Anders
{
public:
    bool operator() (int a, int b)
    {
        return a > b;
    }
};

void show(set<int,Anders> &zahlen)
{
    set<int>::iterator iter;
    for(iter=zahlen.begin(); iter!=zahlen.end(); ++iter)
    {
        cout << *iter << " - " ;
    }
    cout << endl;
}

int main()
{
    set<int,Anders> zahlen;
    srand(0);
    for(int i=0; i<10; i++)
    {
        zahlen.insert( rand() % 9 + 1 );
    }
    show(zahlen);
}

Die Klasse Anders wird als zweiter Template-Parameter verwendet. Dadurch werden die Elemente in diesem Fall absteigend sortiert. Bei der Klasse Anders handelt es sich um ein ganz typisches Funktionsobjekt. Sie sehen, dass der Aufrufoperator definiert wurde. Da der Rückgabetyp bool ist, handelt es sich um ein Prädikat. An der Funktion show() ist auch zu sehen, wie ein Container als Parameter an eine Funktion übergeben wird.

Die Container-Klassen map und multimap

Der Container map nimmt zwei Arten von Elementen auf. Die erste Art ist der Suchschlüssel, der wie bei einem Set in einem Binärbaum abgestellt wird. Die zweite Art ist das eigentliche Datenelement, das durch den Schlüssel gefunden werden soll. So werden bei der Definition eines Map-Containers zwei Typen angegeben: der erste für den Schlüssel, der zweite für den Wert. Die eckigen Klammern werden von der Map so überladen, dass der Schlüssel dazwischen angegeben wird. Der gesamte Ausdruck bezeichnet dann das Datenelement. Auf diese Weise kann sehr anschaulich mit der Map programmiert werden.

[Beispiel für Map (stl/map.cpp)]

#include <iostream>
#include <string>
#include <map>
using namespace std;

int main()
{
    map<string,string> Kennzeichen;
    Kennzeichen["HH"] = "Hansestadt Hamburg";
    cout << Kennzeichen["HH"] << endl;
    cout << "Größe: " << Kennzeichen.size() << endl;
    cout << Kennzeichen["HG"] << endl;
    cout << "Größe: " << Kennzeichen.size() << endl;
}

Wenn Sie das Programm laufen lassen, erhalten Sie die folgende Ausgabe:

Hansestadt Hamburg
Größe: 1

Größe: 2

find()

Ganz offensichtlich wird also für den Schlüssel HG allein durch die Verwendung in den eckigen Klammern ein Element angelegt. Wollen Sie lediglich prüfen, ob es ein solches Element überhaupt gibt, sollten Sie die Elementfunktion find() verwenden:

if (Kennzeichen.find("KI")==Kennzeichen.end())
{
    cout << "Nicht gefunden!" << endl;
}

Iteratortypen

Iteratoren bieten die Möglichkeit, in standardisierter Form auf Elemente eines Containers zuzugreifen. Da auf die verschiedenen Container auf unterschiedliche Weise zugegriffen wird, liefert jeder Container den zu ihm passenden Iteratortyp unter dem Namen iterator mit.

Schnittstelle

Damit bilden die Iteratoren die Verbindung für die STL-Algorithmus-Funktionen, um auf die Container zuzugreifen. Auf diese Weise kann der Container ausgetauscht werden, ohne dass die STL-Funktion davon betroffen ist. Allerdings eignen sich nicht alle Container für jeden Zugriff. So werden verschiedene Arten von Iteratoren unterschieden, je nachdem, welche Zugriffsmöglichkeiten sie anbieten. Die Arten der Iteratoren sind der RandomAccessIterator, der BidirectionalIterator, der ForwardIterator, der InputIterator und der OutputIterator. Tabelle (tabiter1) zeigt die Operationen, die mit den jeweiligen Iteratortypen möglich sind. [Iteratoren]
Output Input Forward Bidirectional RandomAccess
Lesen =*p =*p =*p =*p
Zugriff -> -> -> ->, []
Schreiben *p= *p= *p= *p=
Iteration ++ ++ ++ ++, -- ++, --, +, -, +=, -=
Vergleich ==, != ==, != ==, != ==, !=, <, >, <=, >=

Containerabhängigkeit

Hinter dem Typ iterator, den der Container anbietet, steckt einer der oben genannten Iteratoren. Welchen Iterator der Container anbieten kann, hängt natürlich mit den Möglichkeiten des Zugriffs zusammen, die der Container bietet. Tabelle (tabiter2) zeigt, welcher Container welchen Iterator anbietet.
Container Iterator
vector RandomAccessIterator
deque RandomAccessIterator
string RandomAccessIterator
list BidirectionalIterator

[Container und Iteratoren]
Container Iterator
map BidirectionalIterator
multimap BidirectionalIterator
set BidirectionalIterator
multiset BidirectionalIterator

Die Template-Klasse bitset

Konstruktion

Die Template-Klasse bitset gehört ebenfalls zur STL, ist aber kein Container, denn sie nimmt keine Benutzertypen auf, um sie zu organisieren. Sie bietet vielmehr eine Möglichkeit, Bitstrukturen zu verwalten. Der Parameter, den das Template erhält, ist kein Datentyp, sondern eine ganzzahlige Konstante, die angibt, wie viele Bits das Bitset enthält.

Bitoperationen

Sie können einzelne Bits mit der Elementfunktion set() setzen und mit reset() löschen. Die Elementfunktion flip() schaltet ein Bit um. Als Parameter wird die Position des gewünschten Bits angegeben. Wird kein Parameter angegeben, wird die Funktion auf alle Bits des Bitsets angewandt.

Prüfen

Die Elementfunktion test() liefert einen bool-Wert, der angibt, ob das Bit, dessen Position als Parameter übergeben wird, gesetzt ist. [Bitset-Funktionen]
Wirkung
bool test(size_t pos) gibt true zurück, wenn Bit gesetzt ist
bitset<N> set(size_t pos, int var=1) Setzt das Bit an Position pos
bitset<N> reset(size_t pos, int var=0) Löscht das Bit an Position pos
bitset<N> flip(size_t pos) Schaltet das Bit an Position pos um

Indexoperator

Das Bitset kann mit Hilfe des Indexoperators wie ein Array behandelt werden. Dadurch werden die Zugriffe anschaulicher. Die Elementfunktion count() liefert die Anzahl der gesetzten Bits eines Bitsets, die Funktion size() dessen Größe in Bits. Die Funktion any() liefert true, wenn mindestens ein Bit des Bitsets gesetzt ist. Die Funktion none() wird wahr, wenn keines der Bits gesetzt ist. [Weitere Bitset Funktionen]
Funktion Wirkung
size_t count() Anzahl der gesetzten Bits des Bitsets
size_t size() Anzahl der Bits des Bitsets
bool any() gibt true zurück, wenn eines der Bits gesetzt ist
bool none() gibt true zurück, wenn keines der Bits gesetzt ist
Das Bitset kennt die Funktionen to_ulong() und to_string(), um das komplette Bitset in eine ganze Zahl oder in eine Zeichenkette zu überführen.
[1]
Konkret gesagt: Er benötigt Random-Access-Iteratoren. Was es mit diesen Iteratoren auf sich hat, wird im Zuge des Kapitels noch geklärt.