Kopiersemantik: tief vs. flach
Lerne den Unterschied zwischen flachen und tiefen Kopien kennen, wie du korrekte Kopieroperationen für ressourcenbesitzende Klassen implementierst und was das Copy-and-Swap-Idiom leistet.
Warum Kopiersemantik wichtig ist
Wenn du in C++ ein Objekt kopierst — was genau wird dann eigentlich kopiert? Bei einfachen Typen ist die Antwort klar: Alle Member werden Bit für Bit kopiert. Besitzt ein Objekt jedoch eine Ressource über einen Zeiger, erzeugt der voreingestellte (vom Compiler generierte) Kopierkonstruktor eine flache Kopie: Er kopiert den Zeigerwert, nicht aber die Daten, auf die er zeigt. Damit teilen sich zwei Objekte dieselbe Ressource, was zu Double-Free-Fehlern und Datenkorruption führt.
Kopiersemantik zu verstehen ist unverzichtbar, weil Kopien an vielen Stellen implizit entstehen: bei der Übergabe per Wert, beim Zurückgeben aus Funktionen, beim Einfügen in Container und bei Zuweisungen.
Das Problem der flachen Kopie
Der vom Compiler erzeugte Kopierkonstruktor kopiert jeden Member einzeln. Bei Zeigern kopiert er die Adresse — nicht die Daten, auf die gezeigt wird. Das ist eine flache Kopie und für ressourcenbesitzende Klassen gefährlich.
#include <iostream>
#include <cstring>
class BrokenString {
char* data_;
std::size_t len_;
public:
BrokenString(const char* s)
: len_{std::strlen(s)}
, data_{new char[std::strlen(s) + 1]}
{
std::strcpy(data_, s);
}
~BrokenString() {
delete[] data_; // frees the buffer
}
// No copy constructor defined!
// Compiler generates shallow copy: copies the pointer
void print() const {
std::cout << data_ << '\n';
}
};
int main() {
BrokenString a{"Hello"};
{
BrokenString b = a; // shallow copy: b.data_ == a.data_
b.print(); // "Hello" — works for now
} // b destroyed: delete[] b.data_ (same pointer as a.data_!)
// a.data_ is now a dangling pointer — UNDEFINED BEHAVIOR
a.print(); // may crash, print garbage, or appear to work
return 0;
}Eine tiefe Kopie implementieren
Eine tiefe Kopie allokiert neuen Speicher und kopiert die eigentlichen Daten. So besitzt jedes Objekt seine eigene, unabhängige Kopie der Ressource.
#include <iostream>
#include <cstring>
#include <algorithm>
class SafeString {
char* data_;
std::size_t len_;
public:
explicit SafeString(const char* s)
: len_{std::strlen(s)}
, data_{new char[std::strlen(s) + 1]}
{
std::strcpy(data_, s);
}
// Deep copy constructor
SafeString(const SafeString& other)
: len_{other.len_}
, data_{new char[other.len_ + 1]}
{
std::strcpy(data_, other.data_);
}
// Deep copy assignment with self-assignment check
SafeString& operator=(const SafeString& other) {
if (this == &other) return *this; // self-assignment guard
// Allocate new resource BEFORE releasing old one
char* new_data = new char[other.len_ + 1];
std::strcpy(new_data, other.data_);
// Release old resource
delete[] data_;
// Adopt new resource
data_ = new_data;
len_ = other.len_;
return *this;
}
~SafeString() { delete[] data_; }
void print() const {
std::cout << data_ << " (len=" << len_ << ")\n";
}
};
int main() {
SafeString a{"Hello"};
SafeString b = a; // deep copy: b gets its own buffer
SafeString c{"World"};
c = a; // deep copy assignment
a.print(); // Hello (len=5)
b.print(); // Hello (len=5)
c.print(); // Hello (len=5)
return 0;
}Das Copy-and-Swap-Idiom
Das Copy-and-Swap-Idiom liefert eine elegante, exception-sichere Implementierung des Kopier-Zuweisungsoperators. Es nutzt den Kopierkonstruktor und eine swap-Funktion wieder und bietet starke Exception-Sicherheit bei minimaler Code-Duplizierung.
#include <iostream>
#include <cstring>
#include <utility>
class Buffer {
char* data_;
std::size_t size_;
public:
explicit Buffer(std::size_t size)
: size_{size}, data_{new char[size]{}} {}
Buffer(const Buffer& other)
: size_{other.size_}, data_{new char[other.size_]}
{
std::memcpy(data_, other.data_, size_);
}
~Buffer() { delete[] data_; }
// Copy-and-swap: take parameter by VALUE (invokes copy ctor)
Buffer& operator=(Buffer other) { // copy happens here
swap(*this, other); // swap resources
return *this;
} // 'other' destroyed here, freeing our old resource
// Non-throwing swap — essential for strong exception safety
friend void swap(Buffer& a, Buffer& b) noexcept {
using std::swap;
swap(a.data_, b.data_);
swap(a.size_, b.size_);
}
char& operator[](std::size_t i) { return data_[i]; }
std::size_t size() const { return size_; }
};
int main() {
Buffer a{5};
a[0] = 'H'; a[1] = 'i';
Buffer b{10};
b = a; // copy-and-swap: exception-safe, self-assignment-safe
std::cout << b[0] << b[1] << " (size " << b.size() << ")\n";
return 0;
}Kopieren verbieten
Manche Typen sollten nicht kopierbar sein — zum Beispiel Typen, die einzigartige Ressourcen repräsentieren, etwa Dateihandles, Netzwerkverbindungen oder Threads. Verwende =delete, um das klar zu kommunizieren und zu erzwingen:
class UniqueConnection {
public:
UniqueConnection(const UniqueConnection&) = delete;
UniqueConnection& operator=(const UniqueConnection&) = delete;
// Move-Operationen können weiterhin aktiviert bleiben
};Vor C++11 erreichte man das, indem man die Kopieroperationen private deklarierte, ohne sie zu definieren — ein verwirrender Trick, den =delete elegant ersetzt.
Eine naive Kopier-Zuweisung, die die alte Ressource freigibt, bevor sie die neue allokiert, ist in zweierlei Hinsicht kaputt: (1) Bei einer Selbstzuweisung (a = a) löscht sie die Daten und versucht dann, aus ihnen zu kopieren, und (2) wenn new nach dem delete wirft, bleibt das Objekt in einem ungültigen Zustand zurück. Das Copy-and-Swap-Idiom löst beide Probleme: Die Kopie entsteht vor jeder Änderung, und swap ist noexcept.
Der beste Weg, Kopierfehler zu vermeiden, ist, rohe Ressourcen gar nicht erst selbst zu verwalten. Nutze std::string statt char*, std::vector statt roher Arrays und std::unique_ptr statt besitzender roher Zeiger. Wenn alle deine Member eine korrekte Kopiersemantik haben, sind die vom Compiler erzeugten Kopieroperationen automatisch korrekt. Das ist die Regel von null in der Praxis.
- Die vom Compiler erzeugte Kopie ist flach — sie kopiert Zeigerwerte, nicht die Daten, auf die gezeigt wird
- Ressourcenbesitzende Klassen brauchen einen Kopierkonstruktor und einen Zuweisungsoperator mit tiefer Kopie
- Das Copy-and-Swap-Idiom liefert Exception-Sicherheit und Sicherheit bei Selbstzuweisung gratis dazu
- Verwende
=delete, um das Kopieren von Typen mit einzigartigen Ressourcen ausdrücklich zu verbieten - Die beste Strategie ist die Regel von null: Nutze RAII-Wrapper, damit du nie eigene Kopierlogik brauchst
Quiz — Teste dein Wissen
(15 XP)1. Was passiert, wenn du ein Objekt flach kopierst, das Heap-Speicher über einen rohen Zeiger besitzt?
2. Warum nimmt das Copy-and-Swap-Idiom seinen Parameter per Wert?
3. Welcher Ansatz wird empfohlen, um eigene Kopieroperationen von vornherein überflüssig zu machen?