EMZETT.
Login

Kurz: In C++ gehört jede Ressource einem Besitzer. Modernes C++ löst das über Objekte und Smart Pointer (<memory>), nicht über manuelles new/delete.

Teil des Kurses C++

Kapitel 8 von 15 im Kurs C++. Mit Fortschritt, Quiz und Zertifikat auf der Lernseite.

In C++ gehört jede Ressource einem Besitzer. Modernes C++ löst das über Objekte und Smart Pointer (<memory>), nicht über manuelles new/delete.

Stack und Heap

  • Stack: lokale Variablen, automatisch, schnell, begrenzt
  • Heap: dynamisch reservierter Speicher (new), muss freigegeben werden
#include <iostream>
 
int main() {
    int a = 5;                        // Stack
    int* p = new int(7);              // Heap (roh!)
    std::cout << a << ' ' << *p << '\n';
    delete p;                         // Pflicht, sonst Speicherleck
    p = nullptr;
    return 0;
}

Ausgabe:

5 7

Das funktioniert, ist aber fehleranfällig: vergessenes delete, doppeltes delete, Ausnahmen zwischen new und delete. Deshalb: kein rohes new/delete im Alltag.

unique_ptr: ein Besitzer

std::unique_ptr<T> besitzt ein Objekt allein und löscht es automatisch. Er ist nicht kopierbar, nur verschiebbar:

#include <iostream>
#include <memory>
#include <string>
 
struct Konto {
    std::string name;
    explicit Konto(std::string n) : name(std::move(n)) { std::cout << "erzeugt " << name << '\n'; }
    ~Konto() { std::cout << "gelöscht " << name << '\n'; }
};
 
std::unique_ptr<Konto> erzeuge(const std::string& n) {
    return std::make_unique<Konto>(n);              // Besitz wird zurückgegeben
}
 
int main() {
    auto a = erzeuge("A");
    {
        auto b = std::make_unique<Konto>("B");
        std::cout << b->name << '\n';
    }                                               // B wird hier automatisch gelöscht
    auto c = std::move(a);                          // Besitz wandert zu c
    std::cout << (a == nullptr) << ' ' << c->name << '\n';
    return 0;
}

Ausgabe:

erzeugt A
erzeugt B
B
gelöscht B
1 A
gelöscht A

std::make_unique<T>(args...) ist der richtige Weg, ein Objekt zu erzeugen.

shared_ptr: gemeinsamer Besitz

std::shared_ptr<T> zählt, wie viele Besitzer es gibt, und löscht das Objekt, wenn der letzte weg ist:

#include <iostream>
#include <memory>
 
struct Bild { ~Bild() { std::cout << "Bild gelöscht\n"; } };
 
int main() {
    std::shared_ptr<Bild> a = std::make_shared<Bild>();
    std::cout << a.use_count() << '\n';
    {
        std::shared_ptr<Bild> b = a;                   // zweiter Besitzer
        std::cout << a.use_count() << '\n';
    }
    std::cout << a.use_count() << '\n';
    a.reset();                                          // letzter Besitzer gibt frei
    std::cout << "ende\n";
    return 0;
}

Ausgabe:

1
2
1
Bild gelöscht
ende

std::weak_ptr verweist auf ein shared_ptr-Objekt, ohne Besitzer zu sein. Das bricht Zyklen auf (zwei Objekte, die sich gegenseitig mit shared_ptr halten, würden nie gelöscht).

Welcher Zeiger wann?

SituationWahl
Wert reichtkein Zeiger, einfach Objekt oder std::vector
ein klarer Besitzerstd::unique_ptr
geteilter Besitzstd::shared_ptr
nur beobachten, nicht besitzenReferenz, roher Zeiger (T*) ohne Besitz, weak_ptr
Zahl/Text wachsendstd::vector, std::string

Ein roher Zeiger (T*) oder eine Referenz bedeutet in modernem C++ immer: “Ich leihe mir das Objekt nur, ich besitze es nicht.”

Move-Semantik

Kopieren großer Objekte ist teuer. Beim Verschieben (std::move) wird der Inhalt übertragen, ohne zu kopieren:

#include <iostream>
#include <vector>
#include <string>
#include <utility>
 
std::vector<int> baue(int n) {
    std::vector<int> v;
    for (int i = 0; i < n; i++) v.push_back(i);
    return v;                                // wird nicht kopiert: Move bzw. Copy Elision
}
 
int main() {
    std::vector<int> a = baue(1000);
    std::vector<int> b = std::move(a);       // Besitz des Puffers wandert
    std::cout << a.size() << ' ' << b.size() << '\n';
 
    std::string s1 = "ein ziemlich langer Text, der nicht in den kleinen Puffer passt";
    std::string s2 = std::move(s1);
    std::cout << s1.size() << ' ' << (s2.size() > 10) << '\n';
 
    std::vector<std::string> liste;
    std::string s = "Hallo";
    liste.push_back(s);                      // Kopie
    liste.push_back(std::move(s));           // Verschieben
    liste.emplace_back("direkt");            // direkt im Vektor erzeugen
    std::cout << liste.size() << '\n';
    return 0;
}

Ausgabe:

0 1000
0 1
3

Gib lokale Objekte einfach per return zurück (nicht std::move darauf schreiben), der Compiler optimiert das.

Typische Fehler und wie man sie vermeidet

FehlerBesser
new ohne deletestd::make_unique, Container
Zeiger auf lokale Variable zurückgebenWert zurückgeben
Dangling Reference (Referenz auf zerstörtes Objekt)Lebensdauer prüfen, Wert statt Referenz speichern
Iteratoren nach push_back weiterbenutzenIteratoren neu holen
delete[] vs delete verwechselnstd::vector/std::array
zirkuläre shared_ptrweak_ptr

Werkzeuge

Wie bei C: -fsanitize=address,undefined, Valgrind, clang-tidy und cppcheck finden Speicherfehler früh.

Merke

  • Kein rohes new/delete: std::make_unique, std::make_shared, Container
  • unique_ptr = ein Besitzer, shared_ptr = geteilter Besitz, weak_ptr beobachtet
  • Roher Zeiger oder Referenz = Leihgabe ohne Besitz
  • std::move überträgt Ressourcen statt sie zu kopieren
  • RAII gibt Ressourcen auch bei Ausnahmen frei

Übungsaufgabe

Baue eine einfach verkettete Liste mit std::unique_ptr<Knoten> und füge drei Elemente ein. Wie wird die Liste am Ende freigegeben?

Quiz zur Selbstkontrolle

Weiter im Kurs

Zurück: Vererbung und Polymorphie

Weiter: Templates und Concepts

Alle Kapitel: C++ im Überblick