Hallo und herzlich willkommen. Als mein treuer Begleiter hast du letztes Mal gemeinsam mit mir das Array entdeckt. Wir haben das Konzept dahinter und den Spezialfall des Char Arrays kennengelernt. Außerdem wissen wir, wie mit dieser Datenstruktur gearbeitet wird.

Das Array hat seinen Ursprung in der Programmiersprache C und du findest viele Quellcodes, in diese C-Style Felder verwendet werden.

C++ unterstützt zwar Arrays, stellt aber eigene, darauf aufbauende Template Klassen zur Speicherung und Anordnung von Daten oder für Zeichenketten bereit. Obwohl damit viele Nachteile verschwinden und die Benutzung um einiges komfortabler ist, wird ihnen oft nicht die verdiente Beachtung geschenkt.

Das möchte ich ändern und deshalb schauen wir uns in diesem Beitrag std::vector und std::string aus der C++ Standard Library an.

Vektoren in C++

Setzt du ein statisches Array ein, dann musst du dir genau Gedanken darüber machen wie groß du es anlegst. Schließlich soll es in jeder Situation ausreichen. Leider muss dabei als Kompromiss die feste Speichergröße hingenommen werden. Egal wie der Füllstand des Feldes ist.

Es wäre so viel einfacher und effizienter, wenn ein Array dynamisch, der aktuellen Datenmenge entsprechend wachsen oder schrumpfen würde. C bietet dynamische Arrays nicht von sich aus, doch mit den Memory Management Fähigkeiten

free the mallocs! ;-)

und Pointern lässt sich ein eigenes dynamisches Objekt erstellen.

Ständig erreichst du an einen Punkt, an dem dynamisches Verhalten gefragt wäre. Und aus diesem Grund bietet die C++ Standard Library verschiedene einsatzbereite Lösungen. Dort gibt es Container Klassen, die für die Speicherung von Daten gedacht sind. Diese werden in sequentielle und assoziative Container unterschieden. Sequentielle Container sind auf Einfügen, Entfernen und Bearbeiten von beherbergten Elementen spezialisiert. Die assoziativen Container haben ihre Stärke beim Durchsuchen nach einem bestimmten Elementinhalt.

Natürlich wollen wir gerne ein verbessertes Array. Also eine dynamische Datenstruktur, die unsere Informationen in einer bestimmten Reihenfolge bereithält. Für diesen Zweck finden wir in der C++ Standard Library den sequentiellen Container std::vector.

Diese Template Klasse bietet dir die typische Funktionalität eines dynamischen Arrays. Das wirklich evolutionäre dieses Datentyps sind seine charakteristischen Eigenschaften.

  • Anhängen von weiteren Elementen an das Feld mit konstanter Zeit. Die Zeit, um ein Element am Ende des Arrays anzufügen oder zu verwerfen ist unabhängig von der Größe des Feldes.
  • Die benötigte Zeit Elemente in der Mitte des Feldes ist direkt proportional zur Anzahl der danach folgenden Elemente.
  • Die Anzahl aller Elemente des Arrays ist dynamisch. Die Vector Klasse kümmert sich um die Speicherverwaltung.

Die Funktionsweise und Bestandteile von std::vector sind in der C++ Standard Library definiert und somit standardisiert. Für dich bedeutet das, dass du Vektoren in allen C++ Entwicklungsumgebungen einsetzen kannst, die sich zum ISO Standard bekennen.

std::vector in Aktion

So, genug der Worte. Jetzt wollen wir std::vector endlich in Aktion sehen. Lass uns zunächst einen Vector für Integer deklarieren. Anschließend wollen wir noch einen weiteren für Characters initialisieren. Wir dürfen dabei auf keinen Fall vergessen dem Präprozessor mit Hilfe der #include Direktive mitzuteilen, dass wir die Klasse <vector> verwenden wollen.

#include <iostream>
#include <vector>                              // including vector class

int main()
{
    std::vector<int> intVector(3);             // declaring a vector of integers
    intVector[2] = 76;                         // assigning a value to an element

    std::cout << intVector[2] << std::endl;

    std::vector<char>charVector{'H','i', '!'}; // initialize a vector of characters
    std::cout << charVector.at(0)<< std::endl;

    return 0;
}

// listing1: declaring and initializing a vector

Du siehst schon, die Syntax ähnelt C-Style Arrays sehr. Da steckt nicht der große Unterschied. Der große Vorteil neben dem dynamischen Verhalten sind die zahlreichen Funktionen, die diese Container Klasse beherrscht.

std::vector kontrolliert genauso wenig wie ein konventionelles Array, ob das Element, auf das du zugreifen willst überhaupt existiert. Du läufst also Gefahr Daten außerhalb der Grenzen des Feldes abzufragen. Doch std::vector ist sich dieser Problematik bewusst und hält eine sichere Lösung. Mit der Funktion vectorName.at(elementNumber) kannst du den Wert eines Elements abfragen und bekommst beim Verlassen der Grenzen des Feldes eine Rückmeldung. Damit hast du eine Funktion für mehr Sicherheit.

Unterscheidung zwischen Größe und Kapazität

Wie sieht der Speicherbedarf eines std::vector Objekts aus? Wenn es um die Größe geht, musst du zwischen Größe und Kapazität unterscheiden.

Die Funktion vectorName.size() gibt dir die Anzahl der Elemente innerhalb des Feldes zurück. Ähnlich wie bei einem statischen Array kannst du die Anzahl mit der Speichergrößer des Datentyps der Elemente multiplizieren und so den gesamten Speicherbedarf zu ermitteln.

Leider ist das nur die halbe Wahrheit. Aufgrund des dynamischen Verhaltens von std::vector ist der Speicherbedarf oft höher als die reine Datenmenge der Elemente. Der Bedarf kann gleich oder größer sein. Doch manchmal belegt die Container Klasse zusätzlichen Speicher, um auf dynamisches Wachstum reagieren zu können, ohne eine neue Speicherzuordnung vornehmen zu müssen.

Doch die Klasse lässt dich nicht in Dunkeln stehen. Dir wir die Funktion vectorName.capacity() an die Hand gegeben. Diese Funktion gibt dir die aktuell besetzte Speichermenge des Vector Objekts als Anzahl der möglichen Elemente.

Da liegt der kleine, aber doch essentielle Unterschied. Mit vectorName.size() bekommst die Anzahl der Elemente im Feld. vectorName.capacity() zeigt dir wie viele Elemente in die Speichergröße des Objekts passen.

Um dich noch mehr zu verwirren, begrenzt die Kapazität nicht die Größe des Felds. Hängst du weitere Elemente an das Vector Objekt und überschreitest dabei die Kapazität, dann wird dem Container Objekt automatisch mehr Speicher zugewiesen.

Das Anhängen von zusätzlichen Elementen kannst du nicht bis ins Unendliche treiben. Irgendwann ist Schluss. Die Funktion vectorName.max_size() gibt dir den theoretischen Grenzwert für die Größe des Objekts zurück.

#include <iostream>
#include <vector>                                                   // including vector class

int main()
{
    std::vector<char>charVector{'H','i', '!'};

    std::cout << "Size: " << charVector.size() << std::endl;         // number of elements
    std::cout << "Capacity: " << charVector.capacity() << std::endl; // size of charVector given in number of elements
    std::cout << "Max size: " << charVector.max_size() << std::endl; // maximum size of charVector

    return 0;
}

// listing2: size() and capacity()

Lass uns weitere Funktionen von std::vector näher betrachten.

push_back(), insert(), pop_back()

Eingangs habe ich behauptet, dass diese Container Klasse eine dynamische Größe besitzt und das Hinzufügen sowie Entfernen von Elementen zulässt. Natürlich möchte ich diese Aussage nicht einfach so stehen lassen.

Mit der Funktion vectorName.push_back(Value) kannst du während der Laufzeit einen weiteren Wert am Ende des Feldes anhängen. Möchtest du allerdings den letzten Wert entfernen, dann hilft dir dabei die Funktion vectorName.pop_back(). Schauen wir uns in Listing3 die Funktionen an einem Beispiel an. vectorName.back() gibt dir den Wert des letzten Elements zurück.

#include <iostream>
#include <vector>                              // including vector class

int main()
{
    std::vector<int> intVector{ 0, 1, 2, 3, 4 };

    int sizeVec = intVector.size();         // number of elements
    int valueVec = intVector.back();        // value of the last element

    std::cout << "size: " << sizeVec << std::endl;
    std::cout << "value at the end: " << valueVec << std::endl;

    intVector.push_back(5);                 // adding an element to the end

    sizeVec = intVector.size();
    valueVec = intVector.back();

    std::cout << "new size: " << sizeVec << std::endl;
    std::cout << "New value at the end: " << valueVec << std::endl;

    intVector.pop_back();                   // removing an element from the end

    sizeVec = intVector.size();
    valueVec = intVector.back();

    std::cout << "size after pop_back(): " << sizeVec<< std::endl;
    std::cout << "value after pop_back():  " << valueVec << std::endl;

    std::cout << "value after pop_back():  " << intVector.at(sizeVec-1) << std::endl;

    return 0;
}

// listing3: push_back() and pop_back()

Es lassen sich nicht nur Elemente am Ende anheften oder löschen. Mit den beiden Funktionen nameVector.insert() und nameVector.erase() kannst du an jeder beliebigen Stelle des Vector Objekts Elemente hinzufügen bzw. entfernen.

Damit nameVector.insert() einen Wert als neues Element vor der angegebenen Position einfügt, benötigt die Funktion zwei Parameter. Die Position innerhalb des Vector Objekts muss als Iterator übergeben werden. Kurzgefasst zeigt ein Iterator auf einen Ort im Speicher, der zu einem Container Objekt gehört. In Listing4 wird als Iterator die Funktion nameVector.begin() verwendet, die auf den Anfang des Vectors zeigt. Damit du auf ein bestimmtes Element zeigen kannst, musst du dazu noch die relative Position im Feld hinzuaddieren.

Der Wert gilt es darauf zu achten, dass er dem Datentyp der anderen Elemente entspricht.

Das Entfernen eines Elements funktioniert genauso. Übergebe der Funktion nameVector.erase() den Iterator auf das zu entsprechende Element.

Da Vectoren auf Arrays basieren, wird bei der Verwenden von nameVector.insert() oder nameVector.erase() werden die nachfolgenden Elementen im Speicher neu zugeordnet. Das klingt nicht nur umständlich, sondern ist auch eher ineffizient. Die Vector Klasse ist darauf optimiert am Ende zu wachsen oder zu schrumpfen. Brauchst du häufig Operationen, die das innere des Feldes verändern, dann schau dich besser bei den anderen Container Klassen der Standard Library, die dafür gedacht sind um.

#include <iostream>
#include <vector>                              // including vector class

int main()
{
    int position= 3;
    std::vector<int> intVector{ 0, 1, 2, 3, 4 };

    int sizeVec = intVector.size();
    int valueVec = intVector.at(position);

    std::cout << "size: " << sizeVec << std::endl;
    std::cout << "value at position: " << valueVec << std::endl;

    intVector.insert(intVector.begin()+position, 1, 6);             // inserting an element

    sizeVec = intVector.size();
    valueVec = intVector.at(position);

    std::cout << "new size: " << sizeVec << std::endl;
    std::cout << "New value at position: " << valueVec << std::endl;

    intVector.erase(intVector.begin()+position);                     // erasing an element

    sizeVec = intVector.size();
    valueVec = intVector.at(position);

    std::cout << "size after erase(): " << sizeVec<< std::endl;
    std::cout << "value after erase():  " << valueVec << std::endl;

    return 0;
}

// listing4: insert() and erase()

Alle Funktionen von std::vector würden an dieser Stelle den Rahmen sprengen. Doch die wichtigsten haben wir abgedeckt und können nun mit dieser Datenstruktur arbeiten. Möchtest du dich tiefer mit std::vector auseinandersetzen, dann schau einfach in die C++ Standard Library.

Scheint sehr eindimensional

Dir ist vielleicht schon aufgefallen, dass in allen Beispielen die Vector Objekte nur eine Länge und sonst keine weitere Dimension besitzen. Die Klasse Vector ist ein sequentieller Container und hat nur diese eine Dimension. Ebenso wie das C-Style Array umschließt ein Vector eine Sequenz von Speicherplätzen.

Doch es gibt auch hier die Möglichkeit ein mehrdimensionales Objekt anzulegen. Du nimmst einfach einen Vector aus Vectoren. Vergleichbar mit dem Array aus Arrays.

Eine andere Lösung bietet dir C++ oder die Standard Library von sich aus nicht. Alternativ kannst du dir eine eigene mehrdimensionale Array Klasse schreiben oder in anderen Bibliotheken danach suchen und diese dann verwenden.

// declaration of a multi-dimensional array of integers
int[][] mulitArray;

// declaration of a multi-dimensional vector of integers
std::vector<std::vector<int>> mulitVector;

// listing5: multi-dimensional vector

C++ und Strings

Im letzten Beitrag zu Arrays haben wir uns den Sonderfall des Char Arrays angeschaut. Bevor du jetzt voreilig ein “Char Vector” als Alternative oder Equivalent vorschlägst muss ich dich bremsen. Klar, kannst du so machen. Ist dann halt…

Kommen wir direkt auf den Punkt. Die C++ Standard Library stellt extra Klasse std::string für die Verarbeitung von Zeichenketten bereit. Strings sind Objekte, die aus einer Sequenz von Character bestehen. Sie sind der Vector Klasse ähnlich, haben keine feste Größe und können sich bei Bedarf dynamisch an Datenmengen anpassen. Sie sind aber auf den Datentypen Character spezialisiert.

Strings gehen effizient und sicher mit Texteingaben um und bieten Funktionen zur Manipulation von Zeichenketten. Damit sind sie ganz klar unsere erste Wahl, wenn wir Text verarbeiten wollen.

Lass uns einen einfachen Dialog zwischen Benutzer und Konsole schreiben. Zunächst teilen wir dem Präprozessor mit, dass wir die Klasse <string> verwenden wollen. <iostream> liefert uns die Funktionen um Nutzereingaben entgegenzunehmen und für Ausgaben auf die Konsole.

Zunächst definieren wir den String greeting und geben den Wert aus. Die folgende Handlungsaufforderung ist ein String Literal. Anschließend wird der String name deklariert, um ihn anshcließend mit einer Nutzereingabe zu befüllen. Zum Schluß wird der Inhalt des Strings und seine Länge ausgegeben.


#include <iostream>
#include <string>                   // including string class

int main()
{
    std::string greeting ("Hello there!");              // defining a string variable
    std::cout << greeting << std::endl;                 // showing value of string variable

    std::cout << "What is your name?" << std::endl;     // showing string literal

    std::string name;                                   // declaring a string variable
    std::cin >> name;                                   // assign a stream to a string variable

    std::cout << "Hi "<< name << "," << std::endl;
    std::cout << "your name has "<< name.length()       // length of a string
              << " characters" << std::endl;

    return 0;
}

// listing6: c++ string

Wir haben schon viel mit Zeichenketten in Form von String Literalen gearbeitet. Die Beispiele der vorherigen Beiträge wären mit std::string um einiges eleganter und besser zu lösen gewesen. Zudem gehören diese in vielen Lehrbüchern zu den ersten Datentypen, die du gezeigt bekommst.

Warum habe ich mit der Vorstellung so lange gewartet?

Ich finde es etwas ungeschickt mit einem komplexeren Datentyp aus der C++ Standard Library zu beginnen. Dadurch besteht die Gefahr von Unklarheiten und flaschen Annahmen. Erst wenn du die primitiven Datentypen kennst und das Konzept der Datentypen verstanden hast, dann sind komplexe Datentypen viel leichter zu verstehen.

Wir werden nun häufiger std::string einsetzen und immer mehr der Funktionen kennenlernen.

Das bleibt hängen

Unser Portfolio an Datentypen wächst und wir arbeiten sogar mit ersten Klassen der C++ Standard Library. std::vector ist eine Container Klasse daraus. Sie dient als dynamische Alternative zu den statischen Arrays, deren Wurzeln in der Programmiersprache C liegen. Zu der Fähigkeit, sich im Speicher neu anzuordnen und seine Größe anzupassen, kommen noch Funktionen, die das Arbeiten sicherer und einfacher für dich machen.

Hast du eine Sequenz von Variablen desselben Datentyps, dann sollte std::vector deine erste Wahl sein. Besteht die Sequenz aus Daten des Typs Character, so ergibt sich ein Sonderfall. Hierfür beinhaltet die C++ Standard Library die spezialisierte Klasse std::string. Damit kannst du sehr komfortabel Texte und Nutzereingabe verarbeiten.

Du merkst, es gibt eine riesige Vielfalt an Datentypen, die du in der entsprechenden Situation nutzen kannst. Welcher jeweils der richtige ist, darüber lässt sich ausgiebig diskutieren. Am Ende ist es deine persönliche Entscheidung. Schließlich dienen alle nur dazu, deinen Quellcode zielgerichtet, sicherer und einfacher zu gestalten.

Jetzt sind wir mit einer guten Auswahl an Datentypen gewappnet und können sinnvolle Variablen anlegen, um Daten mit geeigneten Typen im Speicher zu halten. Als nächstes beginnen wir mit den Variablen zu arbeiten.

Ich wünsche dir maximalen Erfolg!


Quellen

  • [1] B. Stroustrup, A Tour of C++. Pearson Education, 2. Auflage, 29. Juni 2018.
  • [2] B. Stroustrup, Programming: Principles and Practice Using C++. Addison Wesley, 2. Auflage, 15. Mai 2014.
  • [3] U. Breymann, Der C++ Programmierer. C++ lernen – professionell anwenden – Lösungen nutzen. Aktuell zu C++17. München: Carl Hanser Verlag GmbH & Co. KG; 5. Auflage, 6. November 2017