Typen, Werte & das Typsystem
Erkunde die fundamentalen Typen von C++, Ganzzahltypen fester Breite, die Initialisierungsformen, die auto-Typableitung und das const-System, das Korrektheit bereits zur Kompilierzeit erzwingt.
Das Typsystem: deine erste Verteidigungslinie
C++ ist eine statisch typisierte Sprache — jeder Ausdruck hat einen zur Kompilierzeit bekannten Typ. Das Typsystem ist nicht bloß Buchhaltung; es ist dein mächtigstes Werkzeug, um Fehler zu finden, bevor dein Code überhaupt läuft. Ein sauber typisiertes C++-Programm trägt seine Invarianten im Typsystem selbst und macht damit ganze Fehlerklassen unmöglich.
Der Typ einer Variablen bestimmt drei Dinge: wie viel Speicher sie belegt, welche Werte sie darstellen kann und welche Operationen auf ihr erlaubt sind. Typen richtig zu wählen ist die Grundlage für korrekten C++-Code.
Fundamentale Typen
C++ bietet eine Reihe eingebauter Typen, die sich direkt auf die Fähigkeiten der Hardware abbilden. Ihre genauen Größen hängen von der Plattform ab, doch der Standard garantiert Mindestwertebereiche:
#include <iostream>
#include <cstdint> // Fixed-width integer types
#include <limits> // std::numeric_limits
int main() {
// Boolean
bool is_valid = true; // true or false, typically 1 byte
// Character types
char letter = 'A'; // At least 8 bits, holds ASCII
char8_t u8ch = u8'A'; // C++20: exactly 8 bits, UTF-8
char16_t u16ch = u'\u00E9'; // At least 16 bits, UTF-16
char32_t u32ch = U'\U0001F600'; // At least 32 bits, UTF-32
// Integer types (sizes are minimums, typical x86-64 shown)
short s = 32000; // At least 16 bits (16)
int i = 2'000'000; // At least 16 bits (32) — digit separator C++14
long l = 100'000L; // At least 32 bits (64 on Linux, 32 on Windows)
long long ll = 9'000'000'000'000'000'000LL; // At least 64 bits
// Unsigned variants
unsigned int ui = 42u; // Cannot be negative
// Floating-point types
float f = 3.14f; // ~7 decimal digits precision
double d = 3.141592653589793; // ~15 decimal digits precision
long double ld = 3.14159265358979323846L;
// Fixed-width types from <cstdint> — use these when size matters
std::int32_t exact = 42; // Exactly 32 bits, guaranteed
std::uint64_t big = 18'446'744'073'709'551'615ULL; // Exactly 64 bits
std::int_fast32_t fast = 42; // At least 32 bits, fastest available
std::size_t index = 0; // Unsigned, suitable for sizes and indices
std::cout << "int: " << sizeof(int) << " bytes, max = "
<< std::numeric_limits<int>::max() << '\n';
}Klicke auf Ausführen, um dieses Beispiel zu kompilieren und auszuführen.
Initialisierung: der richtige Weg
C++ hat im Laufe der Zeit mehrere Initialisierungssyntaxen hervorgebracht. Die einheitliche Initialisierung mit geschweiften Klammern {} ist in modernem C++ zu bevorzugen, weil sie verengende Konvertierungen verhindert:
#include <iostream>
#include <string>
int main() {
// Direct initialization
int a(42); // OK but can be confused with function declaration
// Copy initialization
int b = 42; // OK, familiar from C
// Uniform (brace) initialization — preferred in modern C++
int c{42}; // Direct-list-initialization
int d = {42}; // Copy-list-initialization
// Brace initialization PREVENTS narrowing conversions
// double to int loses precision — the compiler will ERROR:
// int narrow{3.14}; // ERROR: narrowing conversion
int ok = 3.14; // Compiles! Silently truncates to 3 (dangerous)
// Value initialization (zero for built-in types)
int zero{}; // Guaranteed to be 0
double dzero{}; // Guaranteed to be 0.0
bool bfalse{}; // Guaranteed to be false
// auto deduction
auto x = 42; // int
auto y = 3.14; // double
auto z = 'A'; // char
auto s = std::string{"hello"}; // std::string (not const char*!)
// decltype — queries the type of an expression
decltype(x) another = 100; // same type as x → int
decltype(auto) ref = (x); // int& because (x) is an lvalue expression
std::cout << "zero-initialized int: " << zero << '\n';
std::cout << "auto-deduced string: " << s << '\n';
}Klicke auf Ausführen, um dieses Beispiel zu kompilieren und auszuführen.
In C wird der Nullzeiger durch das Makro NULL dargestellt, das üblicherweise zu 0 oder (void*)0 expandiert. In C++ führt das zu Mehrdeutigkeiten, denn 0 ist auch ein gültiger int. Stell dir eine Funktion vor, die sowohl für int als auch für einen Zeigertyp überladen ist — die Übergabe von NULL (also 0) würde die int-Überladung aufrufen, nicht die für Zeiger.
C++11 hat nullptr eingeführt, ein Schlüsselwort vom Typ std::nullptr_t, das sich implizit in jeden Zeigertyp konvertieren lässt, aber nicht in Ganzzahlen. Verwende für Nullzeiger immer nullptr statt NULL oder 0.
Genauso solltest du '\0' (das Nullzeichen) bevorzugen, wenn du das Zeichen mit dem Wert null meinst, statt 0 zu schreiben — so wird deine Absicht klar.
- Verwende für Nullzeiger immer nullptr statt NULL oder 0
- nullptr hat einen eigenen Typ (std::nullptr_t) und vermeidet Mehrdeutigkeiten bei Überladungen
- NULL ist ein C-Makro, das zu 0 expandiert — es ist eine Ganzzahl, kein Zeiger
- Verwende '\0' für das Nullzeichen, um es von der Ganzzahl 0 zu unterscheiden
const und Typqualifizierer
Der Qualifizierer const teilt dem Compiler (und menschlichen Lesern) mit, dass ein Wert nach der Initialisierung nicht mehr verändert werden darf. Er gehört zu den wichtigsten Werkzeugen in C++, um sicheren, selbsterklärenden Code zu schreiben.
const liest man von rechts nach links: const int* ist ein Zeiger auf einen const int (der int lässt sich nicht ändern, der Zeiger schon). int* const ist ein const-Zeiger auf einen int (der Zeiger lässt sich nicht ändern, der int schon). const int* const ist ein const-Zeiger auf einen const int.
constexpr (C++11) geht noch weiter: Es bedeutet, dass der Wert zur Kompilierzeit berechenbar sein muss. Dadurch kann der Compiler Ausdrücke schon beim Übersetzen auswerten und die Laufzeitkosten vollständig eliminieren. Verwende constexpr für Konstanten, die wirklich zur Kompilierzeit feststehen.
consteval (C++20) ist noch strenger: Es markiert eine Funktion, die zur Kompilierzeit ausgewertet werden muss — sie zur Laufzeit aufzurufen ist ein Übersetzungsfehler.
Der Qualifizierer volatile sagt dem Compiler, dass sich der Wert einer Variablen außerhalb der Kontrolle des Programms ändern kann (z. B. Hardwareregister, speicherabgebildete E/A). Er unterbindet bestimmte Optimierungen. Verwende volatile nicht für Nebenläufigkeit — nimm stattdessen std::atomic.
Einer der heimtückischsten Fehler in C und altem C++ kommt von verengenden Konvertierungen — impliziten Umwandlungen, bei denen Information verloren geht. Weist du zum Beispiel einen double einem int zu, wird der Nachkommateil stillschweigend abgeschnitten. Weist du einen großen int einem short zu, läuft der Wert stillschweigend über.
Die Initialisierung mit geschweiften Klammern {} verhindert verengende Konvertierungen bereits zur Kompilierzeit. Das ist einer der stärksten Gründe, sie der Initialisierung mit = vorzuziehen. Schreibst du int x{3.14};, meldet der Compiler einen Fehler. Bei int x = 3.14; wird stillschweigend auf 3 abgeschnitten.
Ebenso führt das Mischen von vorzeichenbehafteten und vorzeichenlosen Typen in Berechnungen zu impliziten Konvertierungen mit überraschenden Ergebnissen. Ein negativer int, der mit einem unsigned int verglichen wird, wird zuerst nach unsigned konvertiert und kann dabei zu einer sehr großen positiven Zahl werden.
- Die Initialisierung mit geschweiften Klammern {} entdeckt verengende Konvertierungen zur Kompilierzeit
- Die Initialisierung per Zuweisung (=) erlaubt stillschweigend Verengung — Daten gehen ohne Warnung verloren
- Das Mischen vorzeichenbehafteter und vorzeichenloser Typen führt zur impliziten Konvertierung nach unsigned
- Ein negativer int wird bei der Konvertierung nach unsigned zu einer sehr großen positiven Zahl
Typaliase
Typaliase verbessern die Lesbarkeit und erleichtern es, Typen später zu ändern. Bevorzuge in modernem C++ using gegenüber typedef:
#include <cstdint>
#include <vector>
#include <string>
// Modern style (using) — preferred
using Byte = std::uint8_t;
using StudentId = std::int32_t;
using Names = std::vector<std::string>;
// Old style (typedef) — equivalent but harder to read with complex types
typedef std::uint8_t OldByte;
typedef std::vector<std::string> OldNames;
// using works with templates; typedef does not easily
template <typename T>
using Vec = std::vector<T>;
int main() {
Byte b{255};
StudentId sid{12345};
Names names{"Alice", "Bob", "Charlie"};
Vec<double> values{1.0, 2.0, 3.0};
}Klicke auf Ausführen, um dieses Beispiel zu kompilieren und auszuführen.
- Jeder C++-Ausdruck hat einen zur Kompilierzeit bekannten Typ — der Typ bestimmt Größe, gültige Werte und gültige Operationen
- Verwende Typen fester Breite aus
(int32_t, uint64_t), wenn die genaue Größe zählt - Bevorzuge die Initialisierung mit geschweiften Klammern {} — sie verhindert gefährliche verengende Konvertierungen
- Verwende für Nullzeiger nullptr (nicht NULL oder 0), um Mehrdeutigkeiten bei Überladungen zu vermeiden
- auto leitet den Typ aus dem Initialisierer ab; decltype erfragt den Typ eines Ausdrucks, ohne ihn auszuwerten
- Verwende constexpr für Konstanten zur Kompilierzeit und 'using' für Typaliase
Quiz — Teste dein Wissen
(15 XP)1. Welchen Vorteil hat die Initialisierung mit geschweiften Klammern `int x{42};` gegenüber der Initialisierung per Zuweisung `int x = 42;`?
2. Warum solltest du in C++ `nullptr` statt `NULL` verwenden?
3. Welchen Typ leitet `auto x = 42;` für `x` ab?