Konstruktoren, Destruktoren & Regel von 0/3/5
Beherrsche den vollständigen Lebenszyklus von C++-Objekten — von der Konstruktion bis zur Zerstörung — und lerne, wann du spezielle Memberfunktionen schreiben solltest und wann besser nicht.
Der Objekt-Lebenszyklus in C++
In C++ hast du die direkte Kontrolle darüber, wann und wie Objekte erzeugt und zerstört werden. Der Compiler kann bis zu sechs spezielle Memberfunktionen automatisch erzeugen: Standardkonstruktor, Kopierkonstruktor, Kopier-Zuweisungsoperator, Move-Konstruktor, Move-Zuweisungsoperator und Destruktor. Zu verstehen, wann du sie selbst schreibst — und wann du das dem Compiler überlässt —, ist eine der wichtigsten Fähigkeiten in C++.
Arten von Konstruktoren
Konstruktoren initialisieren ein Objekt. C++ kennt mehrere Arten: Standardkonstruktoren (ohne Argumente), parametrisierte, konvertierende (ein einziges Argument, implizite Konvertierung) und delegierende (ein Konstruktor ruft einen anderen auf).
#include <iostream>
#include <string>
class Temperature {
double celsius_;
std::string label_;
public:
// Default constructor
Temperature() : celsius_{0.0}, label_{"unknown"} {}
// Parameterized constructor
Temperature(double c, const std::string& label)
: celsius_{c}, label_{label} {}
// Converting constructor — allows implicit conversion from double
Temperature(double c) : celsius_{c}, label_{"sensor"} {}
// Delegating constructor — reuses another constructor
Temperature(const std::string& label)
: Temperature{0.0, label} {} // delegates to parameterized
void print() const {
std::cout << label_ << ": " << celsius_ << "°C\n";
}
};
int main() {
Temperature t1; // default
Temperature t2{36.6, "body"}; // parameterized
Temperature t3 = 100.0; // converting (implicit)
Temperature t4{"outdoor"}; // delegating
t1.print(); // unknown: 0°C
t2.print(); // body: 36.6°C
t3.print(); // sensor: 100°C
t4.print(); // outdoor: 0°C
return 0;
}Das Schlüsselwort explicit
Konstruktoren mit einem einzigen Argument ermöglichen implizite Konvertierungen, die subtile Fehler verursachen können. Verwende explicit, um ungewollte Konvertierungen zu verhindern — das ist fast immer das, was du willst.
#include <iostream>
#include <string>
class UserId {
int id_;
public:
explicit UserId(int id) : id_{id} {}
int get() const { return id_; }
};
void process(UserId uid) {
std::cout << "Processing user #" << uid.get() << '\n';
}
int main() {
UserId u1{42}; // OK: direct initialization
// UserId u2 = 42; // ERROR: implicit conversion blocked by explicit
// process(42); // ERROR: would require implicit conversion
process(UserId{42}); // OK: explicit construction
return 0;
}Member-Initialisierungslisten & Initialisierungsreihenfolge
Die Member-Initialisierungsliste ist der bevorzugte Weg, Member zu initialisieren. Für const-Member, Referenz-Member und Member ohne Standardkonstruktor ist sie sogar zwingend erforderlich. Member werden in der Deklarationsreihenfolge initialisiert, nicht in der Reihenfolge, in der sie in der Initialisierungsliste stehen.
#include <iostream>
#include <string>
class Connection {
const int max_retries_; // const: MUST use init list
std::string& log_target_; // reference: MUST use init list
std::string host_;
int port_;
public:
Connection(std::string& log, const std::string& host, int port)
: max_retries_{3} // const member
, log_target_{log} // reference member
, host_{host} // initialized in declaration order
, port_{port}
{
// Constructor body runs AFTER all members are initialized
log_target_ += "[Connection to " + host_ + " created]\n";
}
void print() const {
std::cout << host_ << ':' << port_
<< " (max retries: " << max_retries_ << ")\n";
}
};
int main() {
std::string log;
Connection conn{log, "db.example.com", 5432};
conn.print();
std::cout << log;
return 0;
}Destruktoren & die Regel von null / drei / fünf
Ein Destruktor (~ClassName()) läuft, wenn ein Objekt zerstört wird — er gibt Ressourcen frei und darf keine Exceptions werfen. Für die meisten Klassen genügt der vom Compiler erzeugte Destruktor völlig.
Die Regel von null: Bevorzuge RAII-Wrapper (std::string, std::vector, std::unique_ptr), damit du überhaupt keine speziellen Memberfunktionen schreiben musst. Musst du doch eine rohe Ressource verwalten, sagt die Regel von drei, dass du einen Destruktor, einen Kopierkonstruktor und einen Kopier-Zuweisungsoperator brauchst. Die Regel von fünf erweitert das um den Move-Konstruktor und den Move-Zuweisungsoperator.
#include <iostream>
#include <cstring>
// === Rule of Zero: the GOAL ===
// No special members needed — std::string handles everything
class Person {
std::string name_;
int age_;
public:
Person(const std::string& name, int age)
: name_{name}, age_{age} {}
// Compiler generates correct copy/move/destructor automatically!
};
// === Rule of Five: when managing a raw resource ===
class CharBuffer {
char* data_;
std::size_t size_;
public:
explicit CharBuffer(const char* str)
: size_{std::strlen(str)}
, data_{new char[std::strlen(str) + 1]}
{
std::strcpy(data_, str);
}
// Destructor
~CharBuffer() { delete[] data_; }
// Copy constructor (deep copy)
CharBuffer(const CharBuffer& other)
: size_{other.size_}
, data_{new char[other.size_ + 1]}
{
std::strcpy(data_, other.data_);
}
// Copy assignment (copy-and-swap idiom)
CharBuffer& operator=(CharBuffer other) { // note: by value
swap(*this, other);
return *this;
}
// Move constructor
CharBuffer(CharBuffer&& other) noexcept
: data_{other.data_}, size_{other.size_}
{
other.data_ = nullptr;
other.size_ = 0;
}
// Move assignment
CharBuffer& operator=(CharBuffer&& other) noexcept {
if (this != &other) {
delete[] data_;
data_ = other.data_;
size_ = other.size_;
other.data_ = nullptr;
other.size_ = 0;
}
return *this;
}
friend void swap(CharBuffer& a, CharBuffer& b) noexcept {
using std::swap;
swap(a.data_, b.data_);
swap(a.size_, b.size_);
}
void print() const {
std::cout << (data_ ? data_ : "(null)") << '\n';
}
};
int main() {
CharBuffer a{"Hello"};
CharBuffer b = a; // copy constructor
CharBuffer c{"World"};
c = a; // copy assignment
CharBuffer d = std::move(a); // move constructor
d.print(); // Hello
a.print(); // (null) — moved-from state
return 0;
}=default und =delete
Du kannst die vom Compiler erzeugte Version einer speziellen Memberfunktion mit =default ausdrücklich anfordern oder sie mit =delete ausdrücklich unterdrücken. Das macht deine Absicht klar und dokumentiert die Kopier- und Move-Semantik der Klasse.
#include <memory>
#include <string>
// Non-copyable, movable resource handle
class FileHandle {
std::string path_;
int fd_;
public:
FileHandle(const std::string& path, int fd)
: path_{path}, fd_{fd} {}
// Explicitly default the destructor
~FileHandle() = default;
// Delete copy operations — file handles should not be duplicated
FileHandle(const FileHandle&) = delete;
FileHandle& operator=(const FileHandle&) = delete;
// Default move operations
FileHandle(FileHandle&&) = default;
FileHandle& operator=(FileHandle&&) = default;
};
int main() {
FileHandle fh{"data.txt", 3};
// FileHandle copy = fh; // ERROR: copy deleted
FileHandle moved = std::move(fh); // OK: move is allowed
return 0;
}Member werden in der Reihenfolge initialisiert, in der sie in der Klasse deklariert sind, nicht in der Reihenfolge der Initialisierungsliste. Hängt ein Member von einem anderen ab, achte darauf, dass die Abhängigkeit zuerst deklariert wird. Compiler warnen davor mit -Wreorder — ignoriere diese Warnung niemals. Ein klassischer Fehler: size_ aus einem Parameter zu initialisieren und dann size_ zu nutzen, um data_ zu allokieren, während data_ vor size_ deklariert ist.
- Verwende
explicitbei Konstruktoren mit einem einzigen Argument, um ungewollte implizite Konvertierungen zu verhindern - Die Member-Initialisierungsliste ist für
const- und Referenz-Member zwingend und für alle Member effizienter - Member werden in Deklarationsreihenfolge initialisiert, nicht in der Reihenfolge der Initialisierungsliste
- Strebe die Regel von null an — nutze RAII-Typen, damit die vom Compiler erzeugten speziellen Member korrekt sind
- Schreibst du eines von Destruktor/Kopierkonstruktor/Kopier-Zuweisung, schreibe alle fünf (Regel von fünf)
=defaultdokumentiert die Absicht;=deleteverbietet eine Operation ausdrücklich
Quiz — Teste dein Wissen
(20 XP)1. Was verhindert das Schlüsselwort `explicit` an einem Konstruktor?
2. In welcher Reihenfolge werden Klassenmember initialisiert?
3. Was besagt die Regel von null?