HOWTO · C++

Pre-Increment- und Post-Increment-Operatoren in C++

In diesem Artikel wird die Verwendung von Pre-Increment VS Post-Increment in C++ gezeigt

Auf dieser Seite

Bei einer integrierten arithmetischen Variable in C++ addieren sowohl ++x als auch x++ den Wert 1 zu x. Der Unterschied liegt im Wert des Ausdrucks: Prä-Inkrement liefert den aktualisierten Wert, Post-Inkrement den Wert vor der Aktualisierung. Wählen Sie die Form danach, welchen Wert der umgebende Ausdruck benötigt. Bei einem benutzerdefinierten Typ richten sich überladene Inkrementoperatoren nach ihrer jeweiligen Definition; üblicherweise bilden sie das entsprechende Verhalten von Präfix und Postfix nach.

Unterschied zwischen Prä-Inkrement und Post-Inkrement in C++

Bei integrierten arithmetischen Typen aktualisiert ++x zuerst x und liefert dann das Ergebnis des Ausdrucks. x++ liefert den alten Wert und führt die Erhöhung als Teil der Operation aus. In beiden Fällen wird die Variable genau einmal geändert. Überladene Operatoren können sich im Detail anders verhalten, folgen aber üblicherweise demselben Unterschied zwischen Präfix und Postfix.

Führen Sie dieses Beispiel aus:

#include <iostream>

int main() {
    int value = 4;
    int previous = value++;
    int current = ++value;

    std::cout << previous << ' ' << current << ' ' << value << '\n';
}
4 6 6

previous erhält den Wert vor dem ersten Inkrement. current erhält den Wert nach dem zweiten Inkrement; dieser entspricht auch dem Endwert von value.

Die Position eines Inkrements im Schleifenrumpf bestimmt, welchen Wert der Rumpf ausgibt. In der ersten Schleife ist i++ der Ausgabeausdruck: Zuerst wird der aktuelle Wert ausgegeben, danach wird i erhöht. In der zweiten Schleife wird ++j vor dem Ausgabeausdruck ausgeführt, sodass der Rumpf den aktualisierten Wert ausgibt.

#include <iostream>

int main() {
    int i = 0;
    while (i < 5) {
        std::cout << i++ << '\n';
    }
    std::cout << "final i = " << i << '\n';
}
#include <iostream>

int main() {
    int j = 0;
    while (j < 5) {
        std::cout << ++j << '\n';
    }
    std::cout << "final j = " << j << '\n';
}

Die erste Schleife gibt 0, 1, 2, 3 und 4 aus, gefolgt von final i = 5. Die zweite gibt 1, 2, 3, 4 und 5 aus, gefolgt von final j = 5. In beiden Fällen startet der Zähler bei 0, der Schleifenrumpf wird fünfmal ausgeführt und der Endwert des Zählers ist 5. In diesen Beispielen findet das Inkrement im Schleifenrumpf statt. Steht es dagegen im Iterationsausdruck einer for-Schleife, wird es nach dem Schleifenrumpf ausgeführt. Wird sein Ergebnis verworfen, ändert der Austausch von ++k durch k++ die Abfolge nicht: Sowohl for (int k = 0; k < 5; ++k) als auch die Variante mit k++ durchlaufen die Werte 0, 1, 2, 3 und 4 und enden anschließend mit k gleich 5. Die unveränderten Ausgabeblöcke verwenden die englischen Beschriftungen final i und final j.

Dieselbe Variable nicht in einem Ausdruck ändern und lesen

Kombinieren Sie ein Inkrement einer Variablen nicht mit einem weiteren Lesen dieser Variablen in einem nicht sequenzierten Ausdruck. Zum Beispiel hat k++ * k in C++ undefiniertes Verhalten: Die Änderung und das andere Lesen sind nicht zueinander sequenziert. Das Ergebnis lässt sich nicht vorhersagen und kann nicht als gültige Beispielausgabe verwendet werden.

Soll der alte Wert mit dem darauffolgenden Wert multipliziert werden, machen Sie die Schritte ausdrücklich und sequenziert: Initialisieren Sie k, speichern Sie den Wert in old, erhöhen Sie k und berechnen Sie anschließend old * k.

Die einzelnen Anweisungen speichern zuerst den alten Wert und erhöhen danach k; anschließend multiplizieren sie den gespeicherten Wert mit dem aktualisierten Wert. Führen Sie dieses Beispiel aus:

#include <iostream>

int main() {
    int k = 10;
    int old = k;
    ++k;
    int product = old * k;

    std::cout << product << '\n';
}
110

Wie die Wahl bei Iteratoren und benutzerdefinierten Typen den Unterschied macht

Am wichtigsten wird der Unterschied der Werte, wenn ein Inkrement in demselben Ausdruck mit einer anderen Operation kombiniert wird. Der häufige Fall ist das Dereferenzieren: *it++ bedeutet *(it++), liest also das Element, auf das der Iterator gerade zeigt, und rückt den Iterator danach weiter. Dagegen rückt *++it zuerst weiter und liest dann das neue Element. Wird beim Durchlaufen ein Element übersprungen oder wiederholt, ist das oft genau eine solche Verwechslung von Präfix und Postfix und kein Fehler in der Schleifenbedingung.

#include <iostream>
#include <vector>

int main() {
    std::vector<int> values{10, 20, 30};
    auto it = values.begin();

    std::cout << *it++ << '\n';
    std::cout << *it << '\n';
    std::cout << *++it << '\n';
}
10
20
30

Die erste Zeile gibt 10 aus und lässt den Iterator auf 20 zeigen; die Ausgabe von *it bestätigt die neue Position, bevor *++it erneut weiterrückt und 30 ausgibt.

Bei einem benutzerdefinierten Typ sind die beiden Formen zwei verschiedene Funktionen. Üblicherweise wird das Präfix-Inkrement als operator++() deklariert und gibt eine Referenz auf das aktualisierte Objekt zurück, während das Postfix-Inkrement als operator++(int) deklariert wird — das int ist nur eine Dummy-Markierung und kein Wert, den der Aufrufer übergibt —, eine Kopie des alten Zustands speichert, über die Präfixform erhöht und diese Kopie als Wert zurückgibt.

#include <iostream>

class Counter {
public:
    explicit Counter(int value) : value_(value) {}

    Counter& operator++() {
        ++value_;
        return *this;
    }

    Counter operator++(int) {
        Counter old = *this;
        ++(*this);
        return old;
    }

    int value() const { return value_; }

private:
    int value_;
};

int main() {
    Counter counter{5};
    Counter old = counter++;
    Counter& ref = ++counter;

    std::cout << old.value() << ' ' << counter.value() << ' '
              << ( &ref == &counter ) << '\n';
}
5 7 1

Hier behält old den Startwert 5, der Zähler selbst endet nach beiden Inkrementen bei 7, und die abschließende 1 bestätigt, dass das Präfix-Inkrement den Zähler selbst und keine Kopie zurückgegeben hat. Derselbe Unterschied erklärt, warum sich das Ergebnis von ++x weiterhin auf x selbst bezieht, während das Ergebnis von x++ eine temporäre Kopie des alten Werts ist.

Hier wird auch die Leistungsfrage konkret. Bei einer integrierten Ganzzahl, deren Ergebnis verworfen wird, erzeugen Compiler für beide Formen üblicherweise denselben Code; das Präfix ist dort also eine Konvention für Lesbarkeit und kein Geschwindigkeitsvorteil. Die Kopie beim Postfix-Inkrement verursacht nur dann Aufwand, wenn Kopieren echte Arbeit ist — etwa bei einem benutzerdefinierten Iterator oder Zähler, der Zustand kopiert, oder bei einem überladenen Operator, der mehr tut als eins zu addieren. Definiert ein Typ nur eine der beiden Formen, ist die andere schlicht nicht verfügbar; prüfen Sie die Deklarationen des Typs, statt vorauszusetzen, dass beide existieren.

Wann Prä-Inkrement und wann Post-Inkrement verwenden?

Verwenden Sie Post-Inkrement, wenn der Ausdruck den alten Wert benötigt, wie bei int previous = value++;. Verwenden Sie Prä-Inkrement, wenn der aktualisierte Wert benötigt wird oder wenn der Wert des Ausdrucks verworfen wird, etwa im Iterationsausdruck einer Schleife.

Bei Iteratoren ist Prä-Inkrement üblich, wenn der vorherige Iteratorwert nicht benötigt wird. Die überladenen Operatoren eines benutzerdefinierten Iterators richten sich nach dessen Implementierung, bilden aber üblicherweise den Unterschied zwischen integriertem Präfix und Postfix nach. Eine Postfix-Operation kann eine Kopie des vorherigen Iterators aufbewahren und zurückgeben. Bei Iterator-Typen, für die das Kopieren Aufwand verursacht, kann Prä-Inkrement diese Arbeit vermeiden. Daraus folgt kein messbarer Geschwindigkeitsunterschied für ein bestimmtes Programm: Die Compileroptimierung und die Iteratorimplementierung beeinflussen den erzeugten Aufwand.

Fazit

Beide Formen erhöhen die Variable genau einmal. Prä-Inkrement liefert den aktualisierten Wert, Post-Inkrement den vorherigen Wert. Verwenden Sie die Form, die vermittelt, welchen Wert der Ausdruck benötigt. Bei Iteratoren ist Prä-Inkrement vorzuziehen, wenn der vorherige Iteratorwert ungenutzt bleibt; eine Geschwindigkeitssteigerung ist dadurch jedoch nicht garantiert.

Häufig gestellte Fragen

Was ist der Hauptunterschied zwischen Prä-Inkrement und Post-Inkrement?

Bei integrierten arithmetischen Typen aktualisiert ++x zuerst die Variable und liefert dann den aktualisierten Wert. x++ liefert den vorherigen Wert und führt das Inkrement aus. Überladene Operatoren richten sich nach ihrer Definition, bieten aber üblicherweise entsprechendes Präfix- und Postfixverhalten.

Kann ich beide Formen in einer Schleife verwenden?

Ja. Im Iterationsausdruck einer for-Schleife wird das Ergebnis üblicherweise verworfen. Daher ändert der Austausch von ++i durch i++ die Iterationsfolge dieser Schleife nicht.

Welche Form ist in C++ schneller?

Es gibt keine allgemeingültige Rangfolge der Geschwindigkeit für alle Ausdrücke und Typen. Post-Inkrement kann bei Iteratoren oder benutzerdefinierten Typen eine temporäre Kopie erfordern; eine Optimierung kann unnötigen Aufwand beseitigen. Wählen Sie die Form nach dem benötigten Wert und der klaren Absicht. Bevor Sie Aussagen zur Leistung treffen, messen Sie eine reale Arbeitslast.

Wann ist Post-Inkrement sinnvoll?

Verwenden Sie es, wenn der Ausdruck den Wert vor dem Inkrement benötigt, zum Beispiel, um diesen Wert einer anderen Variablen zuzuweisen.

Verhalten sich Prä-Inkrement und Post-Inkrement in jeder Sprache gleich?

Nein. Ähnlich geschriebene Operatoren können je nach Sprache unterschiedliche Regeln haben. Prüfen Sie die Semantik der verwendeten Sprache; die Erklärungen hier beziehen sich auf C++.