Zum Inhalt springen
C++ Anfänger 35 min

Klassen, Pointer & RAII

Klassen in C++, Konstruktor / Destruktor, Speichermodell mit Stack und Heap, Smart Pointers und das fundamentale Prinzip RAII.

Aktualisiert:
Inhaltsverzeichnis

Klassen, Pointer & RAII

Das ist das Kapitel, in dem C++ wirklich eigen wird. RAII (Resource Acquisition Is Initialization) ist das fundamentale C++-Idiom - versteh es, und der Rest wird klar.

Klassen - Basics

class Rechteck {
public:
    Rechteck(double breite, double hoehe)
        : breite_{breite}, hoehe_{hoehe}
    {}

    double flaeche() const {
        return breite_ * hoehe_;
    }

    void skalieren(double faktor) {
        breite_ *= faktor;
        hoehe_ *= faktor;
    }

private:
    double breite_;
    double hoehe_;
};

int main() {
    Rechteck r{3.0, 4.0};
    std::cout << r.flaeche() << "\n";  // 12
    r.skalieren(2.0);
    std::cout << r.flaeche() << "\n";  // 48
}

Struktur

  • public: - von aussen zugreifbar
  • private: - nur in der Klasse zugreifbar (Default)
  • protected: - in Klasse und Subklassen
  • _-Suffix fuer private Felder ist Konvention (oder manche nutzen m_ als Praefix)

Konstruktor

Rechteck(double breite, double hoehe)
    : breite_{breite}, hoehe_{hoehe}
{}

Die Member-Initializer-Liste (: name{wert}, ...) ist effizienter und manchmal noetig.

const-Methoden

double flaeche() const {
    return breite_ * hoehe_;
}

const nach der Parameter-Liste bedeutet: “diese Methode aendert das Objekt nicht”. Wichtig, um const-Objekte nutzen zu koennen.

struct vs class

struct Punkt {
    double x;
    double y;
};

class Rechteck {
    // ...
};

Der einzige Unterschied: struct-Member sind public by default, class-Member private by default.

Konvention:

  • struct fuer einfache Datencontainer (wie C-Structs)
  • class fuer komplexere Typen mit Kapselung

Stack vs Heap

Das wichtigste C++-Konzept - wo leben deine Objekte?

Stack (schnell, automatisch)

void funktion() {
    Rechteck r{3, 4};  // lebt auf dem Stack
    // ...
}   // r wird hier automatisch zerstoert

Heap (manuell verwaltet)

void funktion() {
    Rechteck* r = new Rechteck{3, 4};  // lebt auf dem Heap
    // ...
    delete r;   // muss manuell freigegeben werden
}

Problem: Wenn du delete vergisst, hast du ein Memory Leak. Wenn du zweimal delete aufrufst, Double-Free. Wenn du r nach delete noch nutzt, Use-After-Free.

RAII - das Heilmittel

RAII (Resource Acquisition Is Initialization) bedeutet:

Ressourcen werden im Konstruktor geholt und im Destruktor freigegeben.

Destruktor heisst ~Klassenname:

class Datei {
public:
    Datei(const std::string& pfad) {
        f_ = std::fopen(pfad.c_str(), "r");
    }

    ~Datei() {
        if (f_) std::fclose(f_);  // garantierte Freigabe
    }

    // Kopieren verbieten - einfacher sicher zu sein
    Datei(const Datei&) = delete;
    Datei& operator=(const Datei&) = delete;

private:
    FILE* f_ = nullptr;
};

void arbeiten() {
    Datei d{"daten.txt"};
    // ... nutzen
} // Destruktor wird garantiert aufgerufen - Datei wird geschlossen

Das garantiert, dass die Datei geschlossen wird - auch bei Exceptions.

Smart Pointers

Rohe Pointer mit new/delete sind in modernem C++ fast nie noetig. Nutze Smart Pointers aus <memory>:

std::unique_ptr

Exklusiver Besitz - genau ein Owner:

#include <memory>

void arbeiten() {
    auto p = std::make_unique<Rechteck>(3.0, 4.0);
    std::cout << p->flaeche();
    // ... keine delete noetig
}   // unique_ptr zerstoert sich, gibt Rechteck frei

Bewegen, aber nicht kopieren:

auto a = std::make_unique<Rechteck>(3, 4);
auto b = std::move(a);    // a ist jetzt leer, b hat den Zeiger

std::shared_ptr

Geteilter Besitz - mehrere Owner, Reference Counting:

auto p = std::make_shared<Rechteck>(3, 4);

{
    auto q = p;   // beide zeigen aufs gleiche Rechteck
    std::cout << p.use_count();  // 2
}
std::cout << p.use_count();      // 1 - q ist weg

std::weak_ptr

Nicht-besitzende Referenz, um Zyklen zu vermeiden:

std::weak_ptr<Rechteck> w = p;
if (auto locked = w.lock()) {
    // locked ist ein shared_ptr, wenn das Objekt noch lebt
}

Wann welchen?

  • unique_ptr: der Standard, wenn nur einer Owner ist
  • shared_ptr: wenn wirklich mehrere Stellen gleichzeitig besitzen
  • weak_ptr: fuer “weak references” (z.B. Caches, Observer)
  • Raw Pointer T*: nur als nicht-besitzende Referenz, oder fuer temporaeren Blick

Move Semantics (kurz)

std::vector<int> a = {1, 2, 3};
std::vector<int> b = std::move(a);
// a ist jetzt in "moved-from"-Zustand - nicht mehr nutzen
// b hat die Daten

std::move uebergibt “Eigentum” - ohne zu kopieren. Das ist bei grossen Objekten ein riesiger Performance-Gewinn.

Vererbung (knapp)

class Tier {
public:
    virtual ~Tier() = default;

    virtual std::string geraeusch() const = 0;   // abstract
};

class Hund : public Tier {
public:
    std::string geraeusch() const override {
        return "Wuff!";
    }
};

void drucke(const Tier& t) {
    std::cout << t.geraeusch() << "\n";
}

int main() {
    Hund h;
    drucke(h);   // "Wuff!"
}

Wichtig fuer Vererbung

  • virtual ~Tier() - virtueller Destruktor, sonst Undefined Behavior beim Polymorphismus
  • virtual erlaubt Ueberschreiben
  • override macht klar: du ueberschreibst (Compiler prueft)
  • = 0 am Ende macht die Methode abstrakt (rein virtuell)

Ein komplettes Beispiel

#include <iostream>
#include <memory>
#include <string>
#include <vector>

class Form {
public:
    virtual ~Form() = default;
    virtual double flaeche() const = 0;
    virtual std::string name() const = 0;
};

class Kreis : public Form {
public:
    explicit Kreis(double radius) : radius_{radius} {}

    double flaeche() const override {
        return 3.14159 * radius_ * radius_;
    }

    std::string name() const override { return "Kreis"; }

private:
    double radius_;
};

class Rechteck : public Form {
public:
    Rechteck(double breite, double hoehe)
        : breite_{breite}, hoehe_{hoehe} {}

    double flaeche() const override { return breite_ * hoehe_; }
    std::string name() const override { return "Rechteck"; }

private:
    double breite_, hoehe_;
};

int main() {
    std::vector<std::unique_ptr<Form>> formen;
    formen.push_back(std::make_unique<Kreis>(5.0));
    formen.push_back(std::make_unique<Rechteck>(3.0, 4.0));
    formen.push_back(std::make_unique<Kreis>(2.0));

    for (const auto& f : formen) {
        std::cout << f->name() << ": " << f->flaeche() << "\n";
    }
}   // alle Formen werden automatisch freigegeben

Das musst du mitnehmen

  1. RAII ist das Herzstueck: Konstruktor holt Ressourcen, Destruktor gibt sie frei
  2. Smart Pointers statt new/delete - fast immer
  3. unique_ptr als Default, shared_ptr nur wenn wirklich noetig
  4. const-Methoden markieren read-only
  5. virtual und override fuer Polymorphie, virtual ~...() nicht vergessen
  6. Stack ist gratis, Heap braucht Smart Pointer

Zusammenfassung

  • Klassen mit Konstruktor, Destruktor, Member-Funktionen, Private/Public
  • Stack vs. Heap verstehen
  • RAII als Kern-Idiom: Ressourcen an Objekt-Lebenszeit koppeln
  • Smart Pointers (unique_ptr, shared_ptr) statt rohe new/delete
  • Polymorphie mit virtual / override
  • const-Methoden fuer “nicht-aendernd”

Damit hast du das Fundament fuer echte C++-Projekte. Im weiteren Kurs gehen wir auf Templates, die Standard-Library, Exception-Handling und Concurrency ein.

Zurück zum C++ Kurs