Operatoren, Ausdrücke & Konvertierungen
Meistere die Operatorrangfolge, die arithmetischen Konvertierungen, die Gefahren beim Mischen von vorzeichenbehafteten und vorzeichenlosen Typen sowie die vier benannten Casts von C++.
Ausdrücke: die Bausteine der Berechnung
Ein Ausdruck ist in C++ eine Folge von Operatoren und Operanden, die einen Wert berechnet. Selbst eine einfache Anweisung wie x = a + b; enthält mehrere Teilausdrücke: a, b, a + b und x = a + b (die Zuweisung selbst ist ein Ausdruck, der den zugewiesenen Wert liefert).
Jeder Ausdruck hat zwei Eigenschaften: einen Typ und eine Wertkategorie (lvalue, rvalue usw.). Diese Eigenschaften zu verstehen ist wichtig, weil C++ implizite Konvertierungen zwischen Typen vornimmt und die Regeln dafür überraschende Ergebnisse liefern können, wenn man nicht aufpasst.
Die Rangfolge der Operatoren bestimmt, wie Operatoren gruppiert werden (welcher stärker bindet), während die Assoziativität die Gruppierungsrichtung für Operatoren gleicher Rangfolge festlegt. Wer das verwechselt, produziert subtile Fehler, die ohne jede Warnung durch den Compiler gehen.
Überraschungen bei der Operatorrangfolge
Die Operatorrangfolge ist eine der häufigsten Fehlerquellen. Setze im Zweifel Klammern, um deine Absicht klar zu machen:
#include <iostream>
int main() {
// Precedence trap #1: bitwise vs comparison
int x = 5;
// This does NOT check if bit 1 is set:
if (x & 1 == 0) { // Parsed as: x & (1 == 0) → x & 0 → 0
std::cout << "Never reached!\n";
}
// Correct:
if ((x & 1) == 0) { // Parentheses fix the grouping
std::cout << "Even\n";
}
// Precedence trap #2: ternary vs assignment
int a = 1, b = 2;
int result = a > b ? a : b = 10; // Parsed as: (a > b ? a : b) = 10 — ERROR!
// Correct:
int correct = (a > b) ? a : b; // Parenthesize for clarity
// Precedence trap #3: pointer dereference vs member access
// (*ptr).member vs ptr->member — both are equivalent
// *ptr.member is parsed as *(ptr.member) — usually a compile error
// Short-circuit evaluation
int* ptr = nullptr;
// Safe: the second operand is NOT evaluated if the first is false
if (ptr != nullptr && *ptr > 0) {
std::cout << "Value: " << *ptr << '\n';
}
// The && and || operators guarantee left-to-right evaluation and short-circuit
// Overloaded && and || do NOT short-circuit — avoid overloading them
}Klicke auf Ausführen, um dieses Beispiel zu kompilieren und auszuführen.
Arithmetische Konvertierungen & Integer-Promotion
Wenn du in einem Ausdruck Typen mischst, wendet C++ implizite Konvertierungen nach einem Regelwerk an, das übliche arithmetische Konvertierungen heißt:
1. Integer-Promotion: Typen, die kleiner als int sind (char, short, bool), werden zunächst zu int heraufgestuft (oder zu unsigned int, falls der Wert nicht in int passt). Das bedeutet, dass char + char einen int ergibt, keinen char.
2. Angleichen: Unterscheiden sich die Operanden nach der Promotion immer noch im Typ, wird der "kleinere" Typ nach folgender Rangordnung in den "größeren" umgewandelt: int → unsigned int → long → unsigned long → long long → unsigned long long → float → double → long double.
3. Zusammenspiel von signed und unsigned: Trifft ein vorzeichenbehafteter Typ auf einen vorzeichenlosen Typ desselben Rangs, wird der vorzeichenbehaftete Wert nach unsigned konvertiert. Genau hier entstehen die gefährlichsten impliziten Konvertierungen.
Vorzeichenbehaftete und vorzeichenlose Ganzzahlen zu mischen gehört zu den häufigsten und gefährlichsten Fallstricken in C++. Wird ein negativer vorzeichenbehafteter Wert implizit nach unsigned konvertiert, läuft er zu einer sehr großen positiven Zahl über (denn vorzeichenlose Typen rechnen modular).
Das passiert oft beim Vergleich von int-Werten mit size_t (das unsigned ist) — zum Beispiel, wenn du einen std::vector mit einem int-Schleifenzähler durchläufst und diesen mit vector.size() vergleichst.
Kompiliere mit -Wsign-compare (in -Wall enthalten), um solche Probleme zu entdecken.
- Ein negativer int wird bei der Konvertierung nach unsigned zu einer sehr großen Zahl (z. B. wird -1 bei 32 Bit zu 4294967295)
- Beim Vergleich vorzeichenbehafteter mit vorzeichenlosen Werten wird der vorzeichenbehaftete Wert zuerst nach unsigned konvertiert
- vector.size() liefert size_t (unsigned) — der Vergleich mit einem negativen int ergibt falsche Ergebnisse
- Verwende -Wsign-compare, um Vergleiche zwischen signed und unsigned schon zur Kompilierzeit zu finden
Benannte C++-Casts gegenüber Casts im C-Stil
C++ bietet vier benannte Cast-Operatoren, die den gefährlichen Cast im C-Stil (Type)expr ersetzen. Jeder hat einen bestimmten Zweck und bietet ein anderes Maß an Sicherheit:
#include <iostream>
#include <cstdint>
class Base {
public:
virtual ~Base() = default;
virtual void speak() { std::cout << "Base\n"; }
};
class Derived : public Base {
public:
void speak() override { std::cout << "Derived\n"; }
void only_in_derived() { std::cout << "Special!\n"; }
};
int main() {
// static_cast — compile-time checked, for "natural" conversions
double pi = 3.14159;
int truncated = static_cast<int>(pi); // Explicit: you acknowledge the truncation
std::cout << truncated << '\n'; // 3
// dynamic_cast — runtime-checked, for safe downcasting in class hierarchies
Base* base_ptr = new Derived();
Derived* derived_ptr = dynamic_cast<Derived*>(base_ptr);
if (derived_ptr) { // Check for success!
derived_ptr->only_in_derived(); // Safe
}
// const_cast — adds or removes const (use sparingly!)
const int ci = 42;
// int& mutable_ref = const_cast<int&>(ci); // Legal but modifying ci is UB!
// Only valid use: calling a non-const API with a const object you know won't be modified
// reinterpret_cast — reinterprets the bit pattern (dangerous!)
std::int64_t address = reinterpret_cast<std::int64_t>(base_ptr);
std::cout << "Address: " << address << '\n';
// C-style cast: (int)pi — NEVER use this in C++
// It tries static_cast, then const_cast, then reinterpret_cast —
// you don't know which one was applied!
delete base_ptr;
}Klicke auf Ausführen, um dieses Beispiel zu kompilieren und auszuführen.
Verwende die benannten Casts, um deine Absicht deutlich zu machen und passende Prüfungen durch den Compiler zu bekommen:
- static_cast — Für wohldefinierte Konvertierungen: Umwandlungen zwischen Zahlentypen, Upcasts in Klassenhierarchien und void* zu einem typisierten Zeiger. Diesen Cast wirst du am häufigsten brauchen.
- dynamic_cast — Für sichere Downcasts in polymorphen Hierarchien. Setzt mindestens eine virtuelle Funktion in der Basisklasse voraus. Liefert im Fehlerfall nullptr (bei Zeigern) oder wirft std::bad_cast (bei Referenzen). Kostet Laufzeit.
- const_cast — Nur für die Anbindung an alte Schnittstellen, die nicht const-korrekt sind. Benutze ihn niemals, um ein wirklich konstantes Objekt zu verändern.
- reinterpret_cast — Für die hardwarenahe Neuinterpretation von Bits. Fast immer ein Zeichen für plattformabhängigen Code. In Anwendungscode nur selten nötig.
Verwende in C++ niemals Casts im C-Stil (Type)expr — sie verbergen, welche Konvertierung tatsächlich stattfindet, und umgehen Sicherheitsprüfungen.
- Verwende static_cast für Zahlenkonvertierungen und sichere Upcasts — er wird zur Kompilierzeit geprüft
- Verwende dynamic_cast für sichere Downcasts in polymorphen Hierarchien — er wird zur Laufzeit geprüft
- Verwende const_cast nur zur Anbindung an alte, nicht const-korrekte Schnittstellen
- Verwende in C++ niemals Casts im C-Stil — sie verbergen, welche Konvertierung angewendet wird
- Die Operatorrangfolge bestimmt die Gruppierung — setze im Zweifel Klammern, um deine Absicht klar zu machen
- Die Kurzschlussauswertung bei && und || garantiert die Reihenfolge von links nach rechts und überspringt unnötige Auswertungen
- Die Integer-Promotion wandelt kleine Typen (char, short, bool) vor der Rechnung in int um
- Beim Mischen vorzeichenbehafteter und vorzeichenloser Typen wird der vorzeichenbehaftete Wert nach unsigned konvertiert — ein negativer int wird zu einer riesigen positiven Zahl
- Verwende die vier benannten Casts (static_cast, dynamic_cast, const_cast, reinterpret_cast) statt Casts im C-Stil
- Casts im C-Stil sind gefährlich, weil sie stillschweigend mehrere Cast-Arten durchprobieren, ohne dir zu verraten, welche angewendet wurde
Quiz — Teste dein Wissen
(15 XP)1. Wie wird der Ausdruck `x & 1 == 0` tatsächlich ausgewertet?
2. Was passiert beim Vergleich `-1 < 0u` (wobei `0u` vorzeichenlos ist)?
3. Welchen C++-Cast solltest du für einen sicheren Downcast in einer polymorphen Klassenhierarchie verwenden?