Dieser und noch weitere Artikel wurde von CStoll erstellt.


Folgende Themen werden von diesem Artikel berührt:


Druckversion des Artikels


Inhalt

Teil 1:
  1. Vorbemerkungen
  2. Container
  3. sequenzielle Container
  4. assoziative Container
  5. Container-Adapter
  6. Zusammenfassung


1 Vorbemerkungen

Die Standard Template Library (STL) ist ein wesentlicher Bestandteil von ISO C++, der leider häufig unterschätzt oder falsch eingesetzt wird. [...]

Wie der Name schon andeutet, sind die einzelnen Komponenten der STL als Templates aufgebaut. Das hat den Vorteil, dass sie mit beliebigen Datentypen und Erweiterungen zusammenarbeiten können, ohne auf Vererbungshierarchien Rücksicht nehmen zu müssen. Der Anwender der STL muss lediglich darauf achten, dass die benötigten Operationen von seinen Typen unterstützt werden (z.B. benötigt der Schlüsseltyp einer set<> einen <-Operator oder eine äquivalente Vergleichfunktion).

Die STL kann grob in drei Hauptbestandteile untergliedert werden:
Außerdem gibt es einige weitere Funktionen und Klassen in der Standardbibliothek von C++, die nicht direkt der STL zugeordnet werden.

2 Container

Container werden verwendet, um viele gleichartige Objekte gemeinsam unterzubringen und zu verwalten. Alle Containerklassen der STL haben einige gemeinsame Definitionen und Methoden, sodass sie teilweise gegeneinander ausgetauscht werden können. Die wichtigsten davon sind:

Typdefinitionen:

Memberfunktionen


Zu den Template-Parametern, die ein Container bekommt, gehört neben dem Wertetyp auch immer ein Allokator-Typ. Dessen Methoden werden verwendet, wenn der Container neuen Speicher anfordern, initialisieren und freigeben will. Der Defaulttyp hierfür ist std::allocator<T> und verwendet die Operatoren new und delete, um den Speicher zu verwalten. Experten können die Speicherverwaltung jedoch auch anders implementieren, indem sie eine eigene Allokator-Klasse schreiben und diese dem Container mitgeben (z.B. kann eine Datenbank oder ein Garbage Collector verwendet oder zurückgegebene Speicherbereiche für schnelleren Zugriff zwischengelagert werden).

3 sequenzielle Container

Sequenzielle Container speichern ihre Elemente in der Reihenfolge, in der sie eingefügt wurden. Natürlich ist es auch möglich, diese Reihenfolge manuell anzupassen, indem man ein Element in der Mitte des Containers einfügt oder die vorhandenen Elemente direkt ändert.

3.1 vector

voller Name:
C++:
template<typename T,typename A=allocator<T> >
class std::vector;

Header:
C++:
include <vector>

Ein Vektor verwendet einen zusammenhängenden Speicherbereich, in dem er seine Elemente unterbringen kann. Dadurch kann er sehr schnell über den Index auf ein Element zugreifen und neue Elemente am Ende der Sequenz anhängen und löschen. Einfüge- und Lösch-Operationen in der Mitte oder gar am Anfang des Containers benötigen jedoch deutlich mehr Zeit, weil alle nachfolgenden Elemente entsprechend verschoben werden müssen.

Ein Beispiel zum Umgang mit Vektoren:
C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <vector>
#include <iostream>//für Ausgaben
using namespace std;
 
int main()
{
  vector<int> data;
  data.push_back(1);
  data.push_back(2);
  data.push_back(3);
 
  //Ausgabe aller Elemente:
  for(int j=0;j<data.size();++j)
    cout<<data[j]<<" ";
  cout<<endl;
 
  data[1]=20;//direkter Elementzugriff
  data.front()=11;
  data.pop_back();
 
  data.resize(10,4);
 
  //noch mal Ausgabe:
  for(int j=0;j<data.size();++j)
    cout<<data[j]<<" ";
 
  return 0;
}


Ein Vektor reserviert sich in der Regel mehr Speicherplatz als für seine aktuelle Größe notwendig ist. Solange diese Kapazität noch nicht erreicht wird, können push_back()-Operationen in konstanter Zeit durchgeführt werden. Bei Überschreitung der Kapazität werden alle vorhandenen Elemente in einen neuen (entsprechend größeren) Speicherbereich umkopiert - dadurch werden erstens alle Referenzen, Zeiger und Iteratoren, die auf Elemente des Vektors zeigen, ungültig gemacht und zweitens kostet diese Operation recht viel Zeit. Deshalb ist es günstig, vor aufwendigen Operationen mit dem Vektor per reserve() genug Speicher anzufordern.

Achtung: Ein Vektor gibt die einmal reservierte Kapazität erst im Destruktor wieder frei.

Zusätzlich zu den generellen Methoden, seinen Inhalt zu manipulieren, bietet ein Vektor noch einige spezielle Zugriffsfunktionen:

Die Spezialisierung vector<bool> wurde zur Platz sparenden Speicherung von 1-Bit-Werten vorgesehen und bietet ein paar Zusatzmethoden zur Manipulation einzelner Bits:


3.2 deque

voller Name:
C++:
template<typename T,typename A=allocator<T> >
class std::deque;

Header:
C++:
#include <deque>

Eine Deque (Double Ended QUEue) hat insofern Ähnlichkeit mit dem Vektor, dass sie ihren Inhalt auch in zusammenhängendem Speicher organisiert. In der Regel wird jedoch nicht ein einzelner Speicherblock dafür verwendet, sondern mehrere kleinere Blöcke, die beim Durchlaufen der Deque virtuell zusammengefügt werden. Ihr Vorteil ist, dass sie sowohl am Ende als auch am Anfang problemlos erweitert werden kann. Änderungen in der Mitte des Containers sind wie beim vector langsam und sollten besser vermieden werden.

Wenn eine Deque ihre reservierte Kapazität ausgeschöpft hat, reserviert sie einen neuen Speicherbereich, der vor bzw. hinter den vorhandenen Speicherblöcken in die Kontrollstruktur eingefügt wird. Im Gegensatz zum Vektor werden die einzelnen Speicherblöcke auch wieder freigegeben, wenn sie nicht mehr benötigt werden (wann das geschieht, ist normalerweise von der Implementation abhängig).

Eine Deque bietet - mit Ausnahme von capacity() und reserve() - die gleichen Methoden wie ein Vektor, zusätzlich ermöglicht sie:


3.3 list

voller Name:
C++:
template<typename T,typename A=allocator<T> >
class std::list;

Header:
C++:
#include <list>

Im Gegensatz zu Vektoren wird der Inhalt einer List in Form einer doppelt verketteten Liste gespeichert. Auf diese Weise ist es sehr effizient, Werte an beliebiger Stelle einzufügen und wieder zu entfernen. Im Gegenzug ist es sehr langwierig, ein bestimmtes Element anhand seines Index wiederzufinden.

Eine List bietet keinen direkten Elementzugriff über den Index-Operator bzw. at(), aber unterstützt die meisten anderen Funktionen, die auch von Vektoren und Deques zur Verfügung gestellt werden:


Ein Beispiel zur Anwendung einer Liste:
C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <list>
#include <iostream>
using namespace std;
 
int main()
{
  list<int> data;
  //Liste füllen
  for(int i=1;i<5;++i) data.push_back(i);
  data.push_front(4711);
 
  //Ausgabe (nicht über Index möglich, deshalb benötigt man Listen-Iteratoren)
  for(list<int>::iterator pos=data.begin();pos!=data.end();++pos)
    cout<<*pos<<" ";
  cout<<endl;
 
  list<int>::iterator pos1=data.begin();
  ++pos1;//Iterator auf 2. Element
  data.insert(pos1,0x0815);
 
  //noch mal Ausgabe:
  for(list<int>::iterator pos=data.begin();pos!=data.end();++pos)
    cout<<*pos<<" ";
  cout<<endl;
}


3.4 string

voller Name:
C++:
template<typename C,typename Tr=char_traits<T>,typename A=allocator<C> >
class std::basic_string;
 
typedef basic_string<char> string;
typedef basic_string<wchar_t> wstring;

Header:
C++:
#include <string>

Achtung: Der Header <string.h> existiert ebenfalls, dieser enthält jedoch die char*-Verarbeitungsfunktionen der C-Standardbibliothek.

Strings sind zwar keine offiziellen STL-Container, aber sie bieten genug Unterstützung an, um analog zu den anderen Containern verwendet zu werden. Ihre Hauptverwendung ist die Verwaltung von Zeichenketten, deshalb bieten sie auch Operationen zur Verkettung und zur Zusammenarbeit mit char*-Feldern an.

Mit Strings werde ich mich ausführlicher in einem späteren Artikel beschäftigen.

4 assoziative Container

Assoziative Container sortieren ihre Elemente intern nach ihrer Größe. Dadurch geht es deutlich schneller, einen bestimmten Wert im Container wiederzufinden, allerdings spielt es für die Reihenfolge in der Regel keine Rolle, welches Element zuerst eingefügt wurde.

Im Allgemeinen nutzen alle assoziativen Container die gleiche Grundstruktur (ein balancierter Binärbaum), um ihren Inhalt zu speichern, sie unterscheiden sich lediglich im Typ der eingetragenen Elemente und der Art, wie Duplikate verwaltet werden.

Alle assoziativen Container bieten einige zusätzliche Methoden, um bestimmte Elemente suchen zu können (diese Methoden entsprechen den gleichnamigen Algorithmen, können jedoch in der Baumstruktur effizienter arbeiten als auf einem linear angeordneten Iterator-Bereich):

Jeder assoziative Container besitzt einen eingebauten Vergleichstyp, mit dessen Hilfe die einzelnen Elemente verglichen werden. Dieser wird bei der Definition des Containers als zweiter bzw. dritter Template-Parameter übergeben. Der vorgegebene Typ "less<T>" verwendet < als Vergleichskriterium, sortiert die Elemente also aufsteigend. Aber jeder Funktortyp, der zwei Schlüsselwerte als Parameter akzeptiert und einen bool zurückliefert, kann theoretisch als Vergleichskriterium eingesetzt werden.

Die Vergleichsrelation des Containers wird auch verwendet, um zwei Elemente auf Gleichheit zu testen: x==y ist genau dann wahr, wenn !(x<y)&&!(y<x) gilt.

Wichtig für die Anwendung von sets und maps ist, dass der verwendete Vergleichstyp eine "strict weak ordering" (siehe Wikipedia)) bildet, andernfalls kommt es zur Laufzeit des Programmes zu undefiniertem Verhalten - und kein Compiler kann kontrollieren, ob eine beliebig gegebene Relation diese Anforderungen erfüllt.

4.1 set und multiset

voller Name:
C++:
template<typename K,typename P=less<K>,typename A=allocator<K> >
class std::set;
template<typename K,typename P=less<K>,typename A=allocator<K> >
class std::multiset;

Header:
C++:
#include <set>

Sets und Multisets speichern einzelne Schlüsselwerte. Ihr Unterschied besteht darin, dass eine Set jeden Wert maximal einmal enthält, während eine Multiset auch Duplikate enthalten kann.

4.2 map und multimap

voller Name:
C++:
template<typename K,typename V,typename P=less<K>,typename A=allocator<pair<const K,V> > >
class std::map;
template<typename K,typename V,typename P=less<K>,typename A=allocator<pair<const K,V> > >
class std::multimap;

Maps und Multimaps speichern Paare aus einem Schlüssel und einem zugeordneten Wert. Auch hier besteht der Unterschied darin, dass eine Map eindeutige Schlüssel enthält, während die Schlüssel in der Multimap mehrfach vorkommen können - sogar mit unterschiedlich zugeordneten Werten. Die Elemente in einer (Multi-)Map sind Paare aus konstantem Schlüssel und Wert, wodurch die insert-Operationen etwas komplizierter geschrieben werden müssen:
C++:
1
2
3
4
5
6
7
8
9
10
map<string,int> id_liste;
 
//(1) - exakter Typ:
id_liste.insert(map<string,int>::value_type("Carsten",4711));
 
//(2) - pair:
id_liste.insert(pair<string,int>("CStoll",0x0815));
 
//(3) - make_pair:
id_liste.insert(make_pair("Admin",1));
(im ersten Fall wird der Typ exakt so übergeben, wie die map ihn benötigt, im zweiten und dritten Fall ist eine implizite Typumwandlung von pair<string,int> bzw. pair<const char*,int> nach pair<const string,int> notwendig)

Eine Map bietet als besonderes Extra direkten Elementzugriff über den Index-Operator m[ind]. Im Gegensatz zu einem Array oder Vektor wird hierbei der Wert zurückgegeben, der zum Schlüssel 'ind' zugeordnet wurde. Wenn dieser Schlüssel nicht in der Map existiert, wird er angelegt und mit einem Defaultwert verknüpft (die Multimap bietet dieses Feature nicht, da sie keine eindeutige Zuordnung zwischen einem Schlüssel und seinem Wert herstellen kann).

5 Container-Adapter

Diese Adapter verwenden die Standard-Container, um darauf spezielle Datenstrukturen aufzubauen. Sie bieten ein halbwegs einheitliches Interface zur Verwendung an, unterscheiden sich jedoch in der Definition, welches Element das erste ist.
Diese Methoden verwenden keinerlei Fehlerkontrolle - wenn versucht wird, das erste Element eines leeren Stacks zu lesen, ergibt sich deshalb undefiniertes Verhalten.

Alle Adapter erwarten, dass der verwendete Containertyp die Operationen bereitstellt, die sie verwenden wollen, andernfalls weigert sich der Compiler, sie zu erzeugen.

5.1 stack

voller Name:
C++:
template<typename T,typename C=deque<T> >
class std::stack;

Header:
C++:
#include <stack>

Ein Stack nutzt die Memberfunktionen push_back(), back() und pop_back() des unterliegenden Containertyps, um einen LIFO-Speicher zu realisieren - die Elemente werden in der umgekehrten Reihenfolge herausgegeben, in der sie eingefügt wurden.

Als Grundlage für den Stack kann theoretisch jeder sequenzielle Container eingesetzt werden - der Standardtyp ist die deque, weil sie im Gegensatz zum Vektor auch wieder Speicher freigeben kann.

5.2 queue

voller Name:
C++:
template<typename T,typename C=deque<T> >
class std::queue;

Header:
C++:
#include <queue>

Eine Queue (oder Warteschlange) nutzt die Memberfunktionen push_back(), front(), und pop_front() des unterliegenden Containertyps, um einen FIFO-Speicher zu realisieren - die Elemente werden in derselben Reihenfolge herausgegeben, in der sie eingefügt wurden.

Als Grundlage für die Warteschlange können entweder deques oder Listen verwendet werden, da sie als einzige Standard-Container eine pop_front()-Methode definieren.

5.3 priority_queue

voller Name:
C++:
template<typename T,typename C=vector<T>,typename P=less<typename C::value_type> >
class std::priority_queue;

Header:
C++:
#include <queue>

Eine Priority-Queue implementiert eine Prioritätswarteschlange auf dem unterliegenden Containertyp - die Elemente werden entsprechend ihrer Größe umgeordnet, sodass jeweils das größte aus der Warteschlange entnommen werden kann. Der Adapter nutzt die Methoden push_back(), front() und pop_back() sowie die Heap-Algorithmen make_heap(), push_heap() und pop_heap(), um seine Elemente zu organisieren.
Das Prädikat P dient dabei zur Feststellung, welches Element größer ist. Genau wie bei den assoziativen Containern muss die verwendete Vergleichsfunktion eine Ordnungsrelation definieren, sonst funktioniert die Priority-Queue nicht richtig.

Als Grundlage für eine Prioritätsschlange können Vektoren, Deques oder Strings verwendet werden - Listen bieten keine Random-Access-Iteratoren und können deshalb nicht mit den Heap-Algorithmen genutzt werden.

6 Zusammenfassung

Es gibt keine "perfekte" Containerklasse für alle Anwendungsbereiche. Deshalb ist es von Vorteil, wenn man die unterschiedlichen Container der STL kennt und auswählen kann, welcher gerade am günstigsten geeignet ist. Dabei ist es auch mit geringem Aufwand möglich, die Container gegeneinander auszutauschen, wenn man zum Beispiel während der Entwicklung feststellt, dass eine deque<> doch bessere Eigenschaften hat als eine list<>.

Code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Container         Vektor          Deque           Liste           Set             Multiset        Map             Multimap
----------------------------------------------------------------------------------------------------------------------------------
 
Struktur          dynamisches     Array von       verkettete      Binärbaum       Binärbaum       Binärbaum       Binärbaum
                  Array           Arrays          Liste
 
Elemente          Wert            Wert            Wert            Wert            Wert            Schlüssel+Wert  Schlüssel+Wert
 
Duplikate         Ja              Ja              Ja              Nein            Ja              Wert            Ja
 
Direktzugriff     Ja              Ja              Nein            Nein            Nein            über Schlüssel  Nein
 
Iteratortyp       Random Access   Random Access   Bidirektional   Bidirektional   Bidirektional   Bidirektional   Bidirektional
                                                                  konstant        konstant        Schl. konstant  Schl. konstant
Suche             langsam         langsam         sehr langsam    schnell         schnell         schnell (Schl.) schnell (Schl.)
 
Einf./Entf.       Ende            Anfang, Ende    überall
 
invalidiert       Reallocation    manchmal        nie             nie             nie             nie             nie
Iteratoren etc.   Einf./Löschen
 
Speicherfreigabe  nie             manchmal        immer           immer           immer           immer           immer
 
Reservierung      Ja              Nein
 
Transaktions-     push/pop        push/pop an     alle außer      alle außer      alle außer      alle außer      alle außer
sicher            am Ende         Anfang, Ende    sort            multi-insert    multi-insert    multi-insert    multi-insert


Ausblick

Container sind ein hilfreiches Werkzeug zur Datenorganisation. Aber ihre volle Stärke entwickeln sie erst in Zusammenarbeit mit den Iteratoren und Algorithmen - deshalb werde ich diese Bestandteile der STL im zweiten Teil der Serie ausführlicher behandeln.

Sie können Kommentare zu diesem Artikel im Forum schreiben. (Eine Registrierung ist nicht notwendig.)

Logo-Design: MastaMind Webdesign