1. Arrays sind manchmal unpraktisch
Wenn Sie bisher mit std::array<int, 10> gearbeitet haben, kennen Sie die angenehme Stabilität: ein Array hat immer 10 Elemente, nichts „zieht weg“, Indizes sind klar. Aber das Leben spielt einem oft Situationen zu, in denen die Anzahl der Daten vorher nicht bekannt ist. Zum Beispiel: ein Benutzer gibt Noten ein, bis er 0 eingibt; oder wir lesen Zahlen bis zum Ende der Eingabe; oder wir sammeln eine Einkaufsliste, bis der Benutzer sagt „genug“.
Mit einem festen Array entstehen zwei typische Unannehmlichkeiten. Erstens — man muss im Voraus eine maximale Größe wählen („na gut, nehmen wir 1000… vielleicht reicht das?“), was entweder das Risiko birgt, dass es nicht ausreicht, oder unnötig viel Speicher verschwendet. Zweitens — selbst wenn Sie ein großes Array wählen, müssen Sie trotzdem separat speichern, „wie viele Elemente tatsächlich belegt sind“. Genau dieses „tatsächlich belegt“ machen wir heute zur eingebauten Eigenschaft des Containers.
2. Kennenlernen von std::vector<T>
std::vector<T> ist ein Container der Standardbibliothek, der Elemente vom Typ T zusammenhängend im Speicher speichert (wie ein Array), aber die Anzahl der Elemente zur Laufzeit ändern kann. Also ein „Array, das wächst und schrumpft“, ohne Magie: alles sauber verwaltet von der Standardbibliothek.
Man bindet ihn genauso ein wie iostream:
#include <iostream>
#include <vector>
int main() {
std::vector<int> v;
std::cout << "ok\n"; // ok
}
Aus Sicht des Modells heute hat vector zwei wichtige Zahlen.
Eine kleine Tabelle, die nützlich im Kopf zu behalten ist:
| Begriff | Wie man ihn bekommt | Menschliche Bedeutung |
|---|---|---|
|
|
wie viele Elemente wirklich existieren |
|
|
wie viele Elemente ohne Speichererweiterung hineinpassen |
Von der „Speicher“-Seite sprechen wir heute nur oberflächlich, ohne low-level Details, aber die Existenz von capacity() und die Diskussion über seine Bedeutung sind Teil der Standard-Container-Schnittstelle.
3. Erzeugen eines Vektors
Wenn Sie einen std::vector anlegen, ist es nützlich, zwei Gedanken zu trennen: „die Variable existiert“ und „es sind Elemente drin“. Ein Vektor kann existieren und leer sein — das ist ein normales Zustandsbild, keine Fehlerbedingung.
Leerer Vektor
Ein leerer Vektor hat size() == 0.
#include <iostream>
#include <vector>
int main() {
std::vector<int> numbers;
std::cout << numbers.size() << '\n'; // 0
}
Der häufigste Anfängerfehler ist hier, sofort auf numbers[0] zuzugreifen „weil der Vektor ja existiert“. Der Vektor existiert, aber ein Element mit Index 0 existiert nicht. Das ist wie in einem leeren Kühlschrank zu fragen: „Wo ist meine Torte Nummer 0?“
Initialisierung mit einer Liste {...}
Wenn Sie die Startwerte kennen, können Sie den Vektor sofort befüllt erstellen:
#include <iostream>
#include <vector>
int main() {
std::vector<int> v{10, 20, 30};
std::cout << v.size() << '\n'; // 3
std::cout << v[0] << '\n'; // 10
}
4. Hinzufügen und Zugriff: push_back und size()
Die grundlegendste Möglichkeit, einen Vektor zu vergrößern, ist die Methode push_back(value). Sie fügt ein Element ans Ende hinzu, und danach erhöht sich size() um 1. Diese Operation kommt so häufig vor, dass man sie in C++ als das Pendant zu „füge dem Liste noch ein Element hinzu“ ansehen kann.
Kleines Beispiel in 7 Zeilen: wir fügen zwei Zahlen hinzu und prüfen die Größe.
#include <iostream>
#include <vector>
int main() {
std::vector<int> v;
v.push_back(5);
v.push_back(8);
std::cout << v.size() << '\n'; // 2
}
Achten Sie auf einen wichtigen Punkt: push_back fügt genau ans Ende hinzu. Die Reihenfolge der Elemente bleibt erhalten: was früher hinzugefügt wurde, hat den kleineren Index.
size(): „wie viele Elemente vorhanden sind“ und welche Indizes erlaubt sind
Wenn ein Vektor wächst, ist die praktischste Gewohnheit, die gesamte Logik um size() aufzubauen. Nicht um „ich glaube, da sind 10 Elemente“, nicht um „ich habe ein paar Mal hinzugefügt“, sondern um die echte Zahl, die der Container selbst kennt.
Wenn size() == n, dann sind gültige Indizes von 0 bis n - 1. Das ist genau die gleiche Regel wie bei Arrays, nur dass sich n zur Laufzeit ändern kann.
Beispiel:
#include <iostream>
#include <vector>
int main() {
std::vector<int> v;
v.push_back(100);
v.push_back(200);
std::cout << "size=" << v.size() << '\n'; // size=2
std::cout << v[0] << ' ' << v[1] << '\n'; // 100 200
}
Ein wichtiger Disziplinpunkt: wenn Sie eine Schleife schreiben, lautet die Bedingung fast immer so:
for (std::size_t i = 0; i < v.size(); ++i) {
// ...
}
Nicht <=, nicht „bis 10“, nicht „bis zum letzten“. Nur strikt i < v.size(). Das ist wie ein Sicherheitsgurt: langweilig, aber lebensrettend.
5. capacity(): warum ein Vektor „Platz hat“, aber noch keine Elemente
Das Wort capacity klingt wie etwas aus dem Fitnessbereich: „Kapazität“ — wie viel hineinpasst. Für std::vector ist das wörtlich so: capacity() zeigt, wie viele Elemente der Vektor jetzt gerade speichern kann, ohne den internen Speicher zu erweitern.
Warum gibt es das? Weil es teuer wäre, bei jedem push_back den Speicher für genau ein weiteres Element neu zu alloziieren. Deshalb reserviert der Vektor normalerweise „mit Puffer“: z. B. haben Sie 3 Elemente, aber capacity() könnte 4 oder 8 sein — damit die nächsten push_back schnell verlaufen.
Wichtig: capacity() ist fast immer >= size(). Das ist einer der grundlegenden «Invarianten» (Dinge, die immer wahr sein müssen).
Schauen wir uns ein diagnostisches Beispiel an:
#include <iostream>
#include <vector>
int main() {
std::vector<int> v;
v.push_back(1);
std::cout << v.size() << ' ' << v.capacity() << '\n'; // z.B.: 1 1 (oder 1 2)
v.push_back(2);
std::cout << v.size() << ' ' << v.capacity() << '\n'; // z.B.: 2 2 (oder 2 4)
v.push_back(3);
std::cout << v.size() << ' ' << v.capacity() << '\n'; // z.B.: 3 4
}
Die Werte von capacity() können zwischen Compilern und Plattformen variieren — das ist normal. Wichtig ist nicht, die Wachstumsformel zu erraten, sondern den Unterschied zu verstehen: size beschreibt die tatsächlichen Elemente, capacity beschreibt den vorhandenen Platz.
6. Invarianten von std::vector: was immer wahr sein muss
Das Wort „Invariant“ klingt wie der Name eines alten Drachen aus Fantasy, aber praktisch ist es einfach eine Liste von Versprechen, die der Container sich selbst gibt. Wir gehen nicht in die formale Mathematik; es reicht, einige Regeln zu verstehen, die helfen, keinen überraschenden Code zu schreiben.
Erste Invariante — 0 <= size() <= capacity(). Also kann die Anzahl der Elemente nicht negativ sein (danke, Captain), und ein Vektor kann nicht mehr Elemente enthalten, als derzeit Platz zugewiesen ist.
Zweite Invariante — die Elemente sind zusammenhängend gespeichert (wie bei einem Array). Das ist ein Grund, warum vector so beliebt ist: er arbeitet schnell mit Indizes und ist meist cachefreundlich. Mancher nennt ihn „contiguous“-Container; wir verwenden hier noch keine Iteratoren, aber das Etikett ist nützlich: vector ist „fast wie ein Array, nur klüger“.
Dritte Invariante — die Reihenfolge der Elemente bleibt erhalten. Wenn Sie 10, dann 20, dann 30 hinzufügen, dann gilt: v[0] == 10, v[1] == 20, v[2] == 30. Das macht vector zu einem guten „Listen“-Container im Alltag.
Praktische Konsequenz aus capacity(): beim Hinzufügen eines neuen Elements kann der Vektor seinen internen Speicher vergrößern. Man kann sich das vorstellen wie „der Vektor zieht in eine größere Wohnung“. Sie müssen nicht wissen, wie genau das passiert, aber merken Sie sich: capacity() kann sich beim push_back ändern. Deshalb bauen wir keine Logik auf Annahmen über konkrete Werte von capacity().
Eine kleine Modellskizze (ohne low-level Details, nur „wie man denken sollte“):
flowchart LR
A["vector: size=3"] -->|"push_back"| B{"ist Platz? capacity > size"}
B -->|ja| C["Element hinzugefügt size=4 capacity unverändert"]
B -->|nein| D["muss vergrößern capacity steigt dann size=4"]
7. Mini-App: „Sparschwein für Ausgaben“ mit std::vector<int>
Damit std::vector nicht nur Theorie bleibt, starten wir eine kleine Anwendung, die in späteren Vorlesungen erweitert wird. Heute bauen wir den Basisumfang: wir sammeln Zahlen in einem Vektor via push_back, geben dann Statistiken aus und beobachten size/capacity.
Die Geschichte ist einfach: ein Benutzer gibt Ausgaben (ganze Zahlen) ein, und 0 bedeutet „Stopp“. Wir sammeln alles in einem std::vector<int>, weil die Anzahl der Ausgaben vorher nicht bekannt ist.
Gerüst: Begrüßung und leere Liste
#include <iostream>
#include <vector>
int main() {
std::vector<int> expenses;
std::cout << "Enter expenses (0 to stop)\n"; // Enter expenses (0 to stop)
std::cout << "size=" << expenses.size() << '\n'; // size=0
}
Hier ist es nützlich, visuell zu sehen, dass die Liste existiert, aber leer ist.
Zahlen einlesen und push_back
Fügen wir eine Schleife hinzu. Wir vermeiden hier bewusst Fehlerbehandlung für ungültige Eingaben — das ist ein eigenes wichtiges Thema, heute liegt der Fokus auf dem Container.
#include <iostream>
#include <vector>
int main() {
std::vector<int> expenses;
int x = 0;
std::cout << "Enter expenses (0 to stop)\n"; // Enter expenses (0 to stop)
std::cin >> x;
while (x != 0) {
expenses.push_back(x);
std::cin >> x;
}
std::cout << "count=" << expenses.size() << '\n'; // z.B.: count=3
}
Wenn Sie z. B. 10 25 7 0 eingeben, wird size() 3. Also ist size() die tatsächliche Anzahl eingegebener Werte.
Summe mit einer normalen Schleife berechnen
Da wir noch keine STL-Algorithmen behandeln (die kommen später), summieren wir mit einer einfachen for-Schleife. Auch hier ist size() hilfreich — die Schleife hängt nicht von der Anzahl der eingegebenen Elemente ab.
#include <cstddef>
#include <iostream>
#include <vector>
int main() {
std::vector<int> expenses{10, 25, 7};
int sum = 0;
for (std::size_t i = 0; i < expenses.size(); ++i) {
sum += expenses[i];
}
std::cout << "sum=" << sum << '\n'; // sum=42
}
„Speicherdiagnostik“: wir geben capacity() aus
Das ist zwar nicht für Endnutzer nötig, aber sehr hilfreich, um zu spüren, wie der Vektor wächst.
#include <iostream>
#include <vector>
int main() {
std::vector<int> v;
for (int i = 1; i <= 8; ++i) {
v.push_back(i);
std::cout << "size=" << v.size()
<< " cap=" << v.capacity() << '\n';
}
}
Führen Sie das aus und beobachten Sie: size wächst um 1 bei jedem Schritt, während cap in Sprüngen zunimmt. Das ist normal, weil capacity() den Puffer beschreibt.
Aktuelle Version der App komplett
Dies ist die finale Version für diese Vorlesung: nur das, was wir bisher eingeführt haben — push_back, size, capacity, Indizes, eine while-Schleife und eine for-Schleife.
#include <cstddef>
#include <iostream>
#include <vector>
int main() {
std::vector<int> expenses;
std::cout << "Enter expenses (0 to stop)\n"; // Enter expenses (0 to stop)
int x = 0;
std::cin >> x;
while (x != 0) {
expenses.push_back(x);
// Diagnose für uns (nicht für den Nutzer):
std::cout << "[debug] size=" << expenses.size()
<< " cap=" << expenses.capacity() << '\n';
std::cin >> x;
}
int sum = 0;
for (std::size_t i = 0; i < expenses.size(); ++i) {
sum += expenses[i];
}
std::cout << "count=" << expenses.size() << '\n'; // z.B.: count=3
std::cout << "sum=" << sum << '\n'; // z.B.: sum=42
}
Ja, das ist noch ein „rohes Prototyp“. Aber es erfüllt die Hauptaufgabe: es speichert eine unbekannte Anzahl von Werten ohne vorher festgelegte Größe und benötigt keinen „manuellen Zähler“ wie bei einem normalen Array.
8. Typische Startfehler im Umgang mit std::vector
Fehler Nr. 1: verwechseln, dass „der Vektor existiert“ und dass „der Vektor Elemente enthält“.
Nach std::vector<int> v; ist der Vektor angelegt, aber v.size() ist null. Wenn Sie sofort v[0] lesen, greifen Sie auf ein nicht vorhandenes Element zu. Das lässt sich durch die Gewohnheit vermeiden, zuerst in Begriffen von size() zu denken und zu prüfen, ob tatsächlich genug Elemente da sind.
Fehler Nr. 2: denken, der letzte Index sei size().
Bei einem Vektor der Größe n ist der letzte Index n - 1, und das nur, wenn n > 0. Dieser Fehler ist besonders tückisch in Schleifen: i <= v.size() führt fast immer beim letzten Schritt zu einem Out-of-bounds. Zuverlässiges Muster — i < v.size().
Fehler Nr. 3: capacity() als „Anzahl der Elemente“ missbrauchen.
capacity() beschreibt den Speicherpuffer, nicht die tatsächliche Anzahl der Elemente. Man kann capacity() zur Diagnose ausgeben, aber gesamte Logik (Schleifen, Indizes, Prüfungen) sollte auf size() basieren.
Fehler Nr. 4: erwarten, dass capacity() „nach einer bestimmten Formel“ wächst und darauf Code aufbauen.
Manche möchten schreiben „wenn capacity sich verdoppelt, dann …“. Das sollte man vermeiden. Das Wachstum von capacity() ist Implementierungsdetail und kann variieren. Wichtig ist nur: capacity() kann sich beim Hinzufügen ändern, und seine Existenz samt Eigenschaften ist Teil der Container-Schnittstelle.
Fehler Nr. 5: Indizes als int speichern und unkritisch mit v.size() vergleichen.
v.size() liefert einen unsigned-Typ. Wenn Sie int und size_t mischen, können bizarre Effekte auftreten (insbesondere bei negativen Werten). Gewohnheitsmäßig verwenden Sie für Indizes std::size_t, wie in den Beispielen.
GO TO FULL VERSION