Programmieren in C++: Aufbau


Kapitel 7: Der C++ Standard


Inhaltsverzeichnis

Dieses Buch ist unter einer Creative Commons-Lizenz lizensiert.


7.1 Allgemeines

Standardisierte Werkzeuge auf allen Plattformen

In den 90er Jahren wurde die Programmiersprache C++ standardisiert. Ein internationales Gremium arbeitete an einem langen Dokument, in dem festgehalten wurde, wie standardisierter C++-Code auszusehen hat. Die Standardisierung sollte es ermöglichen, C++-Code problemlos mit Compilern verschiedener Hersteller zu übersetzen, und das idealerweise auch auf unterschiedlichen Plattformen. 1998 wurde die erste Version des C++ Standards schließlich verabschiedet und veröffentlicht.

Im C++ Standard wurde nicht nur festgelegt, aus welchen Schlüsselwörtern C++ besteht und welche Bedeutung sie haben. Es wurden darüberhinaus Klassen und Funktionen definiert, die von jedem Compilerhersteller, der den C++ Standard unterstützen möchte, zur Verfügung gestellt werden müssen. Das erst macht es möglich, auf Klassen wie std::string zugreifen zu können, ohne sich jeweils Gedanken machen zu müssen, wo diese Klasse genau herkommt und wie sie im Detail aussieht. Der C++ Standard verlangt, dass die Klasse std::string in einer Headerdatei string zur Verfügung gestellt wird und sich so verhält, wie es im C++ Standard definiert ist.

Der C++ Standard hat sich soweit durchgesetzt, dass ihn heute kein Compilerhersteller ignorieren kann. Das bedeutet, dass standardisierter C++-Code mit jedem modernen C++-Compiler auf jeder beliebigen Plattform kompiliert werden kann. Dabei kann problemlos auf sämtliche Klassen und Funktionen zugegriffen werden, die im Namensraum std definiert sind. Alle in diesem Namensraum vorhandenen Klassen und Funktionen müssen sich so verhalten, wie es der C++ Standard vorschreibt - egal, mit welchem Compiler und egal, auf welcher Plattform Sie arbeiten.

In diesem Kapitel erhalten Sie einen Überblick über verschiedene Bestandteile des C++ Standards. Neben der Klasse std::string und den Streams std::cin und std::cout, die Sie bereits in vielen Beispielen kennengelernt und eingesetzt haben, werden Ihnen Container, Iteratoren und Algorithmen vorgestellt.


7.2 Strings

Zeichenketten speichern und verarbeiten

Eine der am häufigsten verwendeten im C++ Standard definierten Klassen ist std::string. Diese Klasse ist in der Headerdatei string definiert und bietet rund 100 Methoden, um Zeichenketten zu verarbeiten.

#include <string> 
#include <cstdio> 

int main() 
{ 
  std::string s = "Hello"; 
  s += ", world!"; 
  std::printf("%s\n", s.c_str()); 

  std::string::size_type idx = s.find("world"); 
  if (idx != std::string::npos) 
  { 
    s.replace(idx, 5, "moon"); 
    std::printf("%s\n", s.c_str()); 
  } 
} 

Im obigen Programm werden ein paar Methoden der Klasse std::string beispielhaft verwendet. So wird zuerst die Variable s mit Hilfe eines überladenen Konstruktors mit "Hello" initialisiert und anschließend der überladene Operator += verwendet, um ", world!" an s anzuhängen.

Die Methode c_str() gibt einen Zeiger auf einen Puffer zurück, in dem der String mit einem Nullzeichen abgeschlossen ist. Da Strings in C üblicherweise mit einem Nullzeichen abgeschlossen werden, wird diese Methode immer dann verwendet, wenn ein String an eine C-Funktion übergeben werden muss. std::printf() ist eine derartige C-Funktion, der deswegen die Variable s nicht direkt übergeben wird, sondern mit Hilfe von c_str() ein Zeiger auf einen mit einem Nullzeichen abgeschlossenen String. Auf diese Weise kann der String "Hello, world!" mit std::printf() ausgegeben werden.

Die Klasse std::string bietet verschiedene Methoden an, um einen String zu durchsuchen. So gibt find() die Position einer gefundenen Zeichenkette im String zurück. Der Datentyp dieser Positionsnummer ist std::string::size_type. Sollte die gesuchte Zeichenkette nicht gefunden werden, gibt find() std::string::npos zurück.

Es stehen darüberhinaus Methoden zum Ersetzen und Löschen von Zeichen zur Verfügung. So wird im obigen Beispiel mit replace() das Wort "world" durch "moon" ersetzt.

Beachten Sie, dass std::string keine Methoden anbietet, deren Funktionsweise vom sogenannten Kulturkreis abhängt. So gibt es beispielsweise keine Methode, um einen String in Groß- oder Kleinbuchstaben umzuwandeln. Denn welches Zeichen jeweils wie umgewandelt werden müsste, hängt vom Kulturkreis ab. So sind im Deutschen Umlaute wie ä, ö und ü Buchstaben wie alle anderen. Im Englischen aber zählen ä, ö und ü nicht zu den Buchstaben. Das Ergebnis einer Umwandlung in Groß- oder Kleinbuchstaben hängt demnach vom Kulturkreis ab. Da std::string nur Methoden anbietet, die unabhängig vom Kulturkreis immer gleiche Ergebnisse liefern, stehen keine Methoden zur Umwandlung von Buchstaben zur Verfügung.

Neben std::string ist in der Headerdatei string auch eine Klasse std::wstring definiert. Diese Klasse basiert nicht auf dem Datentyp char, sondern auf wchar_t. Der C++ Standard legt die Größe dieses Datentyps nicht fest. Sie können lediglich davon ausgehen, dass ein wchar_t auf 2 oder mehr Bytes basiert und damit größer als ein char ist. Das ermöglicht eine andere Kodierung von Strings. So wird in der Praxis std::wstring üblicherweise eingesetzt, um Unicode-Strings zu speichern.

#include <string> 
#include <cstdio> 

int main() 
{ 
  std::wstring s = L"Hello"; 
  s += L", world!"; 
  std::printf("%ls\n", s.c_str()); 

  std::string::size_type idx = s.find(L"world"); 
  if (idx != std::string::npos) 
  { 
    s.replace(idx, 5, L"moon"); 
    std::printf("%ls\n", s.c_str()); 
  } 
} 

Obiges Programm verwendet Strings vom Typ std::wstring. Da die Zeichen aller Strings nicht mehr auf einem char, sondern auf einem wchar_t basieren, muss vor Literalen ein L angegeben werden. So muss bei der Initialisierung von s L"Hello" angegeben werden, um dem Compiler zu verstehen zu geben, dass die fünf Zeichen jeweils auf einem wchar_t basieren.

Abgesehen von der Kennzeichnung der Literale mit einem L und dem neuen Formatierungssymbol %ls, das zur Ausgabe von auf wchar_t basierenden Strings mit std::printf() verwendet werden muss, gibt es keine Änderungen im Vergleich zum vorherigen Beispiel. Der einzige Unterschied zwischen std::string und std::wstring ist die Kodierung der Strings.


7.3 Streams

Auf dem Flußkonzept basierende Schnittstellen

Unter Streams können Sie sich vereinfacht Rohre vorstellen, über die Daten von einer Quelle gelesen oder in ein Ziel geschrieben werden. Die Schnittstelle ist dabei so abstrakt, dass sich die unterschiedlichsten Objekte wie Streams verhalten können. Die Bedingungen, die Streams an eine Quelle oder ein Ziel stellen, sind minimal. Streams erwarten lediglich, dass ein paar Methoden zur Verfügung stehen, mit denen Daten gelesen oder geschrieben werden können.

Zwei Streams kennen Sie bereits aus vielen Beispielen: std::cin ist die Standardeingabe und std::cout die Standardausgabe. Es handelt sich dabei um Objekte, die in jedem C++ Programm automatisch vorhanden sind, wenn iostream eingebunden wird.

#include <string> 
#include <iostream> 

int main() 
{ 
  std::string s; 
  int i; 

  std::cin >> s >> i; 
  std::cout << i << s; 
} 

Über den Operator >> können Daten von std::cin gelesen werden. Um Daten auf std::cout auszugeben, wird auf den Operator << zugegriffen. Man bezeichnet diese Lese- und Schreiboperationen als formatierte Ein- und Ausgabe, da Daten automatisch umgewandelt werden. So wird im obigen Programm bei einer Eingabe automatisch die erste Zeichenkette in s gespeichert und die zweite Zeichenkette in einen Wert vom Typ int umgewandelt, um ihn dann in i zu speichern. Bei der Ausgabe über std::cout geschieht ähnliches.

Neben der formatierten Ein- und Ausgabe unterstützen Streams auch nicht-formatierte Lese- und Schreiboperationen.

#include <iostream> 
#include <cstring> 

int main() 
{ 
  char buffer[32]; 
  std::cin.getline(buffer, sizeof(buffer)); 
  std::cout.write(buffer, std::strlen(buffer)); 
} 

Über die Methode getline() kann eine Zeile komplett in einen Puffer geladen werden. Die Methode erkennt das Zeilenende am Enter, das der Anwender eingeben muss, damit der Aufruf von getline() zurückkehrt. Es findet dabei im Gegensatz zur formatierten Dateneingabe keine Interpretation der eingegebenen Zeichen statt - die gesamte Zeile wird in den Puffer kopiert. getline() setzt automatisch ans Ende der gelesenen Zeichen ein Nullzeichen, so dass der String im Puffer zum Beispiel an eine C-Funktion übergeben werden kann.

So wie Daten über getline() unformatiert von der Standardeingabe gelesen werden können, können sie mit write() unformatiert auf die Standardausgabe ausgegeben werden.

Objekte wie std::cin und std::cout sind prominente, aber nicht die einzigen Streams, die in C++-Programmen verwendet werden können. Während diese beiden Objekte automatisch existieren, stellt der C++ Standard zahlreiche Stream-Klassen zur Verfügung, die von Ihnen instantiiert werden können. Dabei wird grundsätzlich zwischen Ein- und Ausgabestreams unterschieden. Eingabestreams sind von std::istream abgeleitet, Ausgabestreams von std::ostream. In diesen beiden Klassen sind auch die verschiedenen Methoden definiert, die für die formatierte und nicht-formatierte Ein- und Ausgabe von Daten verwendet werden können. Weil alle Streams von mindestens einer der beiden Klassen abgeleitet sind, besitzen sie alle die gleiche Schnittstelle.

Der C++ Standard bietet zum Beispiel zwei Klassen std::istringstream und std::ostringstream an, die in der Headerdatei sstream definiert sind. Diese Klassen verwenden als Quelle bzw. Ziel einen String vom Typ std::string. Mit Hilfe dieser beiden Klassen können Daten von einem String so gelesen und in einen String so geschrieben werden wie Sie es von std::cin und std::cout gewohnt sind.

#include <string> 
#include <sstream> 
#include <iostream> 

int main() 
{ 
  std::string input = "abc 10"; 
  std::istringstream is(input); 
  std::ostringstream os; 

  std::string s; 
  int i; 

  is >> s >> i; 
  os << i << s; 

  std::cout << os.str() << std::endl; 
} 

Im obigen Programm wird ein Eingabestream is vom Typ std::istringstream mit dem String input initialisiert. Der Stream is verwendet demnach den String input als Quelle. Es werden dann mit Hilfe des Operators >> Daten formatiert gelesen - zuerst ein String, dann eine Zahl. Diese Daten werden in den Variablen s und i entgegen genommen.

Im nächsten Schritt werden die Daten in s und i in umgekehrter Reihenfolge in den Stream os geschrieben. Dieser Stream ist ein Ausgabestream vom Typ std::ostringstream. Alle Daten, die in einen derartigen Stream geschrieben werden, werden intern in einer Variablen vom Typ std::string gespeichert. Die Methode str() gibt diesen internen String zurück. Im obigen Programm wird str() verwendet, um den String zur Kontrolle auf die Standardausgabe auszugeben.

Der C++ Standard stellt weitere Stream-Klassen zur Verfügung wie beispielsweise std::ifstream und std::ofstream, die in der Headerdatei fstream definiert sind. Diese Klassen können verwendet werden, um Daten aus Dateien zu lesen bzw. in Dateien zu schreiben. Es existiert außerdem eine Klasse std::fstream, die sowohl von std::istream als auch von std::ostream abgeleitet ist und somit Lese- und Schreiboperationen unterstützt.


7.4 Container

Gruppieren von Daten

Ein wichtiger Bestandteil des C++ Standards sind verschiedene Container, die es einfach machen, große Datenmengen zu verwalten. Für größtmögliche Flexibilität sind alle Containerklassen als Templates definiert, so dass sie Daten beliebigen Typs speichern können. So existiert beispielsweise eine Klasse std::vector, die sich grundsätzlich wie ein Array verhält. Der Vorteil dieser Klasse ist jedoch, dass die Größe von Vektoren im Gegensatz zu Arrays dynamisch ist und Elemente beliebig hinzugefügt und entfernt werden können.

#include <vector> 
#include <iostream> 

int main() 
{ 
  std::vector<char> v; 
  v.push_back('a'); 
  v.push_back('b'); 
  v.push_back('c'); 

  std::cout << v[0] << std::endl; 
  std::cout << v.at(2) << std::endl; 
  std::cout.write(&v[0], v.size()) << std::endl; 
} 

Im obigen Programm wird ein Vektor v verwendet, der Elemente vom Typ char speichern kann. Um den Vektor mit drei Buchstaben zu füllen, wird auf die Methode push_back() zugegriffen.

Um auf ein Element an einer bestimmten Position im Vektor zuzugreifen, kann entweder der überladene Operator [] oder die Methode at() verwendet werden. Der Unterschied ist: at() überprüft, ob der übergebene Index gültig ist und wirft im Fehlerfall eine Ausnahme vom Typ std::out_of_range. Der Operator [] hingegen überprüft wie bei herkömlichen Arrays nicht, ob der Index innerhalb der gültigen Bandbreite liegt.

Vektoren garantieren, dass Elemente direkt hintereinander im Speicher liegen - so wie Sie es von herkömlichen Arrays kennen. Deswegen können wie im obigen Programm geschehen mit Hilfe der Methode write() die Elemente im Vektor als Zeichenkette "abc" auf die Standardausgabe ausgegeben werden. Die Anzahl der Elemente im Vektor wird über die Methode size() ermittelt.

Da Vektoren garantieren, dass Elemente direkt hintereinander im Speicher liegen, kann das Hinzufügen eines neuen Elements mit push_back() dazu führen, dass ein Vektor einen neuen Speicherbereich reservieren muss, der für die aktuell gespeicherten Daten und das neu hinzugefügte Elemente groß genug ist. Das bedeutet, dass momentan im Vektor gespeicherte Daten in einen neuen Speicherbereich kopiert werden müssen.

Wenn es für Ihr Programm wichtig ist, dass Daten schnell hinzugefügt werden können, ohne das bereits gespeicherte Daten intern an eine andere Position kopiert werden müssen, bietet sich die Liste an. Die entsprechende Klasse heißt std::list und ist in der Headerdatei list definiert.

#include <list> 
#include <iostream> 

int main() 
{ 
  std::list<char> l; 
  l.push_back('a'); 
  l.push_back('b'); 
  l.push_back('c'); 

  std::cout << l.front() << std::endl; 
  std::cout << l.back() << std::endl; 
  std::cout << l.size() << std::endl; 
} 

Der entscheidende Unterschied zwischen einer Liste und einem Vektor ist, dass in einer Liste ein Element mit Hilfe eines Zeigers auf das nächste Element zeigt, während bei einem Vektor alle Elemente direkt hintereinander in einem Speicherbereich liegen. Weil in einer Liste Elemente über Zeiger gekoppelt sind, kann das Hinzufügen eines neuen Elements nicht dazu führen, dass die Liste bereits gespeicherte Daten verschieben muss. Da Entwickler in unterschiedlichen Situationen unterschiedliche Container benötigen, bietet der C++ Standard entsprechend verschiedene Klassen an, aus denen gewählt werden kann.

Während der Zugriff auf ein beliebiges Element in einem Array und in einem Vektor über Zeigerarithmetik sehr schnell vonstatten geht, muss sich eine Liste anhand der Zeiger über die Elemente entlang hangeln. Deswegen bietet std::list keinen überladenen Operator [] und keine Methode at() an. Sie müssen sich also selbst anhand der Zeiger vorhangeln, um zu einem Element an einer bestimmten Position in der Liste zu kommen. Mit front() und back() stehen aber zwei Methoden zur Verfügung, um direkt auf das erste und letzte Element in einer Liste zugreifen zu können.

Neben std::vector und std::list bietet der C++ Standard weitere Container wie std::deque, std::queue und std::set an. Alle Container sind auf bestimmte Datenstrukturen und Algorithmen zur Verwaltung von Daten spezialisiert, so dass Entwickler den für ihre Situation jeweils besten Container wählen können.

Neben den bisher kennengelernten Containern, die einfach nur Gruppen von Daten speichern, gibt es einen Container, in dem jeweils zwei Daten eineinander zugeordnet sind.

#include <string> 
#include <map> 
#include <utility> 
#include <iostream> 

int main() 
{ 
  std::map<std::string, int> m; 
  m.insert(std::make_pair("Anton", 35)); 
  m.insert(std::make_pair("Boris", 31)); 
  m.insert(std::make_pair("Caesar", 40)); 

  std::cout << m["Boris"] << std::endl; 
} 

Die Klasse std::map ist ein Template, das zwei Parameter erwartet - nämlich die Typen der Daten, die eineinander zugeordnet werden sollen. Wenn wie im obigen Beispiel Name und Alter von Personen zugeordnet werden sollen, kann std::map mit std::string und int instantiiert werden. In diesem Fall ist der String der Schlüssel und die Zahl der dem Schlüssel zugeordnete Wert.

Container vom Typ std::map sortieren Daten anhand der Schlüssel. Dank dieser automatischen Sortierung ist es möglich, Werte über Schlüssel sehr schnell zu finden. So wird im obigen Programm mit Hilfe des überladenen Operators [] nach dem Schlüssel "Boris" gesucht und der dem Schlüssel zugeordnete Wert 31 auf die Standardausgabe ausgegeben.

std::map bietet zwar einen Operator [] an. Das heißt aber nicht, dass Daten intern ähnlich wie bei Arrays oder Vektoren gemeinsam in einem Speicherbereich liegen. Im Gegensatz zu Arrays und Vektoren garantieren Container vom Typ std::map noch nicht einmal, dass Daten intern genau in der Reihenfolge gespeichert werden, in der sie dem Container mit insert() hinzugefügt wurden. Damit Container vom Typ std::map Schlüssel schnell finden können, müssen sie Daten sortieren. Sie sollten demnach std::map nicht verwenden, wenn Ihnen die Reihenfolge der Daten, die Sie im Container speichern, wichtig ist und die Reihenfolge unverändert bleiben muss.


7.5 Iteratoren

Eine Art Zeiger auf Elemente in Containern

Alle Container bieten zwei Methoden begin() und end() an, die sogenannte Iteratoren zurückgeben. Es handelt sich dabei um Objekte, mit denen über Elemente in einem Container iteriert werden kann. Der von begin() zurückgegebene Iterator zeigt dabei auf das erste Element, der von end() zurückgegebene Iterator auf die Position hinter dem letzten gültigen Element in einem Container.

Iteratoren sind daher etwas ähnliches wie Zeiger. Sie bieten tatsächlich einen überladenen Operator * an, so dass wie bei Zeigern über das Sternchen auf ein Element zugegriffen werden kann, auf das ein Iterator verweist.

#include <list> 
#include <iostream> 

int main() 
{ 
  std::list<char> l; 
  l.push_back('a'); 
  l.push_back('b'); 
  l.push_back('c'); 

  for (std::list<char>::iterator it = l.begin(); it != l.end(); ++it) 
    std::cout << *it << std::endl; 
} 

Im obigen Beispiel wird auf die Liste zugegriffen, die bereits in einem Beispiel im vorherigen Abschnitt verwendet wurde. Mit Hilfe der Iteratoren wird nun über die einzelnen Elemente in der Liste iteriert, um dann über das Sternchen auf das jeweilige Element zuzugreifen und es auf die Standardausgabe auszugeben.

Die Schleifenkonstruktion, die Sie im obigen Programm sehen, ist typisch: Mit begin() wird ein Iterator auf das erste Element des Containers erhalten. Bevor über diesen Iterator versucht werden darf, auf ein Element im Container zuzugreifen, wird überprüft, ob er ungleich dem Iterator ist, der von end() zurückgegeben wird. Nach jedem Schleifendurchgang wird der Iterator mit ++ inkrementiert, um ein Element im Container vorzurücken.

Der Datentyp des Iterators hängt mit dem Datentyp des Containers zusammen, über dessen Elemente der Iterator laufen soll. Alle Container definieren dazu einen Typ iterator, so dass es nicht notwendig ist, sich neue Klassennamen zu merken.

Mit Hilfe der Iteratoren ist es nun möglich, auf jedes beliebige Element in einem Container zuzugreifen. Wenn Sie beispielsweise das zweite Element der Liste ausgeben möchten, können Sie das wie folgt machen.

#include <list> 
#include <utility> 
#include <iostream> 

int main() 
{ 
  std::list<char> l; 
  l.push_back('a'); 
  l.push_back('b'); 
  l.push_back('c'); 

  std::list<char>::iterator it = l.begin(); 
  std::advance(it, 1); 
  std::cout << *it << std::endl; 
} 

In der Headerdatei utility ist eine Funktion std::advance() definiert, mit der ein Iterator um eine bestimmte Anzahl an Stellen vor- oder zurückgesetzt werden kann. Im obigen Beispiel wird der Iterator, der auf das erste Element im Container zeigt, um eine Stelle vorgerückt und zeigt somit auf das zweite Element im Container. Genausogut hätte in diesem Fall der Iterator natürlich auch mit ++ inkrementiert werden können. Für größere Sprünge ist std::advance() jedoch hilfreich.

Der eigentliche Grund, warum es Iteratoren gibt, liegt in ihrer Eigenschaft als Bindeglied zwischen Containern und Algorithmen. So stehen im C++ Standard zahlreiche Algorithmen zur Verfügung, mit denen Daten in Containern verarbeitet werden können. Diese Algorithmen erwarten jedoch als Parameter keine Container, sondern Iteratoren. Auf diese Weise können Algorithmen auf bestimmte Elemente in Containern angewandt werden und müssen nicht sämtliche Elemente in einem Container verarbeiten.


7.6 Algorithmen

Mit Elementen in Containern arbeiten

Der C++ Standard definiert gut 60 Algorithmen in der Headerdatei algorithm. Es handelt sich dabei um freistehende Funktionen, die mindestens zwei Iteratoren als Parameter erwarten: Ein Iterator, der auf den Anfang einer Sequenz von Elementen zeigt, und einer, der dahinter zeigt. Wenn Sie einen Algorithmus auf alle Elemente in einem Container anwenden möchten, können Sie als Parameter die Iteratoren übergeben, die von begin() und end() zurückgegeben werden.

#include <list> 
#include <algorithm> 
#include <iostream> 

int main() 
{ 
  std::list<char> l; 
  l.push_back('a'); 
  l.push_back('b'); 
  l.push_back('c'); 

  std::reverse(l.begin(), l.end()); 
  for (std::list<char>::iterator it = l.begin(); it != l.end(); ++it) 
    std::cout << *it << std::endl; 
} 

Im obigen Beispiel wird der Algorithmus std::reverse() verwendet, der die Reihenfolge der Elemente zwischen den beiden Iteratoren, die als Parameter übergeben werden, umdreht.

#include <list> 
#include <algorithm> 
#include <iostream> 

int main() 
{ 
  std::list<char> l; 
  l.push_back('a'); 
  l.push_back('b'); 
  l.push_back('c'); 

  std::list<char>::iterator it = l.begin(); 
  std::advance(it, 1); 
  std::rotate(l.begin(), it, l.end()); 
  for (std::list<char>::iterator it = l.begin(); it != l.end(); ++it) 
    std::cout << *it << std::endl; 
} 

Der Algorithmus std::rotate() ist eine Funktion, die als Parameter drei Iteratoren erwartet. std::rotate() rotiert die Elemente, auf die die Iteratoren zeigen, im Kreis. Der mittlere Iterator bestimmt dabei das Element, das nach der Rotation das erste Element sein soll.

#include <list> 
#include <algorithm> 
#include <iostream> 

int main() 
{ 
  std::list<char> l; 
  l.push_back('a'); 
  l.push_back('b'); 
  l.push_back('c'); 

  std::cout << std::count(l.begin(), l.end(), 'c') << std::endl; 
} 

Mit Hilfe des Algorithmus std::count() kann die Anzahl der Elemente zwischen zwei Iteratoren gefunden werden, die mit dem Wert, der als dritter Parameter übergeben wird, identisch sind.

Beachten Sie, dass die Iteratoren, die Sie als Parameter an Algorithmen übergeben, immer auf Elemente im gleichen Container verweisen müssen. Es hat unvorhersehbare Folgen und kann zu einem Absturz führen, wenn Sie Iteratoren übergeben, die auf Elemente in unterschiedlichen Containern verweisen.


7.7 Aufgaben

Übung macht den Meister

Sie können die Lösungen zu allen Aufgaben in diesem Buch als ZIP-Datei erwerben.

  1. Erstellen Sie ein Programm, das über die Standardeingabe eine beliebige Eingabe entgegennimmt und die Anzahl der Leerzeichen in dieser Eingabe errechnet und ausgibt.

    Um eine beliebige Eingabe entgegennehmen zu können, müssen Sie über std::cin Daten unformatiert lesen. Greifen Sie hierzu zum Beispiel auf getline() zu. Übergeben Sie dieser Methode einen ausreichend großen Puffer.

    Um die Anzahl der Leerzeichen zu ermitteln, bietet es sich an, einen String vom Typ std::string mit dem Puffer zu initialisieren. Denn std::string besitzt zahlreiche Methoden, die das Suchen nach Zeichen erleichtern.

  2. Erstellen Sie ein Programm, das über die Standardeingabe beliebig viele Zahlen entgegennimmt. Wenn der Anwender die Eingabe der Zahlen beendet, indem er beispielsweise ein x eingibt, soll die Summe aller Zahlen errechnet und auf die Standardausgabe ausgegeben werden.

    Verwenden Sie einen Container wie std::vector, um die eingegebenen Zahlen zu speichern. Um die Summe zu errechnen, können Sie entweder selbst über die Zahlen im Container iterieren oder den Algorithmus std::accumulate() aus dem C++ Standard verwenden. Im Gegensatz zu vielen anderen Algorithmen ist std::accumulate() nicht in algorithm, sondern in der Headerdatei numeric definiert.

  3. Erstellen Sie ein Programm, das über die Standardeingabe ein Wort entgegennimmt. Das Wort soll umgedreht auf die Standardausgabe ausgegeben werden.

    Es stehen mehrere Lösungsmöglichkeiten zur Verfügung. Sie können zum Beispiel den Algorithmus std::reverse() verwenden, um einen String umzudrehen. Das funktioniert deswegen, weil std::string wie alle Container im C++ Standard die Methoden begin() und end() anbietet.

  4. Erstellen Sie ein Programm, das über die Standardeingabe beliebig viele Wörter entgegennimmt, bis der Anwender x eingibt. Das Programm soll daraufhin die beiden Wörter ausgeben, die gemäß einer lexikographischen Sortierung an erster und an letzter Stelle stehen. Zum Beispiel sollen bei einer Eingabe von "b d a c x" die beiden Buchstaben a und d ausgegeben werden.

    Es stehen wiederum mehrere Lösungsmöglichkeiten zur Verfügung. Sie können die Wörter zum Beispiel in einem Container wie std::vector speichern und sie anschließend sortieren. Sie können aber auch auf einen Container wie std::set zugreifen, der Elemente automatisch sortiert.

  5. Erstellen Sie ein Programm, das den Anwender zur Eingabe beliebig vieler Ganzzahlen auffordert. Gibt der Anwender keine Zahl ein, sondern zum Beispiel x, soll das Programm ausgeben, wie oft welche Zahl eingegeben wurde - und zwar nach der Häufigkeit absteigend sortiert.

    Wie zuvor gibt es mehrere Möglichkeiten, die Aufgabe zu lösen. Sie können zum Beispiel die eingegebenen Zahlen in einem Container speichern, um sie anschließend zu verarbeiten. Oder Sie verarbeiten jede Zahl direkt nach der Eingabe, indem Sie beispielsweise überprüfen, ob sie bereits zuvor eingegeben wurde.

  6. Erstellen Sie ein Programm, dem Sie beim Aufruf als Kommandozeilenparameter den Namen einer Textdatei übergeben. Das Programm soll daraufhin in der Datei die beiden Wörter ermitteln, die am längsten und am kürzesten sind.

    Verwenden Sie einen Stream, um die Textdatei zu öffnen. Sie können dann die Wörter aus der Datei so lesen wie Sie Strings von der Standardeingabe lesen würden. Speichern Sie die Wörter in einem geeigneten Container, um dann mit Hilfe eines Algorithmus nach dem längsten und kürzesten Wort zu suchen.

  7. Erstellen Sie ein Programm, das genauso viele Zufallszahlen in einem Container speichert wie als Kommandozeilenparameter angegeben wird. Der Container soll von Zufallszahlen bereinigt werden, die mehrfach generiert wurden, so dass jede Zufallszahl nur einmal im Container gespeichert wird. Anschließend soll die Summe aller Zahlen errechnet und auf die Standardausgabe ausgegeben werden.

    Um den Kommandozeilenparameter vom Typ char* in einen int-Wert umzuwandeln, können Sie auf die Funktion std::atoi() zugreifen, die in der Headerdatei cstdlib definiert ist. Diese Headerdatei stammt aus dem C Standard, der in den C++ Standard übernommen wurde. So bietet diese Headerdatei auch eine Funktion std::rand() an, um Zufallszahlen zu generieren.

  8. Entwickeln Sie eine Funktion answer_to_life(), der als einziger Parameter ein Ausgabestream übergeben wird. Die Funktion soll die Zahl 42 in den Stream hineinschreiben. Rufen Sie die Funktion auf und übergeben Sie einmal std::cout und ein ander Mal ein Objekt vom Typ std::ofstream.

    Allen Ausgabestreams ist gemeinsam, dass sie von der Klasse std::ostream abgeleitet sind, die in der Headerdatei ostream definiert ist. Sie können diese Klasse als Datentyp für den Parameter der Funktion answer_to_life() verwenden.

  9. Erstellen Sie ein Programm, das die Umlaute ä, ö und ü auf die Standardausgabe ausgibt. Außerdem soll die Zahl 12345678.9 als 12.345.678,90 ausgegeben werden - also mit einem Punkt als Tausendertrennzeichen und zwei Nachkommastellen.

    Sie müssen das sogenannte Locale ändern, damit Ihr Programm die Umlaute richtig ausgibt und sie tatsächlich als Umlaute auf der Standardausgabe erkennbar sind. Sehen Sie sich dazu die Headerdatei locale an. Die Änderung des Locales ist auch notwendig, damit die Zahl in einer Schreibweise ausgegeben wird, wie sie typischerweise in Deutschland verwendet wird.

  10. Erstellen Sie ein Programm, das den Anwender zur Eingabe beliebig vieler Zahlen auffordert. Diese sollen jeweils sofort in einem Container gespeichert werden. Wird die Eingabe beendet, indem der Anwender zum Beispiel ein x eingibt, sollen die Zahlen in umgedrehter Reihenfolge wieder ausgegeben werden. Dabei soll jede ausgegebene Zahl sofort aus dem Container entfernt werden.

    Verwenden Sie für den Container die Klasse std::stack aus der Headerdatei stack.