Zum Inhalt springen

Klassen, Structs & Kapselung

Verstehe den Unterschied zwischen struct und class, Zugriffsspezifizierer, Kapselung, const-Memberfunktionen, statische Member und moderne Aggregat-Initialisierung.

Was ist eine Klasse?

Eine Klasse in C++ ist ein benutzerdefinierter Typ, der Daten (Membervariablen) und Operationen auf diesen Daten (Memberfunktionen) zu einer einzigen Einheit bündelt. Dieses Bündeln nennt man Kapselung — eine der vier Säulen der OOP.

Die zentrale Erkenntnis: Eine Klasse definiert eine Invariante, also eine Menge von Regeln, die über ihren internen Zustand immer gelten müssen. Eine Date-Klasse könnte zum Beispiel verlangen, dass der Monat immer zwischen 1 und 12 liegt. Kapselung erlaubt dir, solche Invarianten durchzusetzen, indem du den Zugriff auf den internen Zustand kontrollierst.

struct vs. class

In C++ sind struct und class nahezu identisch. Der einzige Unterschied ist der voreingestellte Zugriffsspezifizierer: Bei struct ist er public, bei class ist er private. Konvention: Verwende struct für reine Datenaggregate und class, wenn du Invarianten zu wahren hast.

struct_vs_class.cpp
#include <iostream>
#include <string>

// struct: members are public by default
struct Point {
    double x;  // public
    double y;  // public
};

// class: members are private by default
class BankAccount {
    std::string owner;   // private
    double balance;      // private

public:
    BankAccount(const std::string& name, double initial)
        : owner{name}, balance{initial} {}

    double get_balance() const { return balance; }

    void deposit(double amount) {
        if (amount > 0) {
            balance += amount;
        }
    }
};

int main() {
    Point p{3.0, 4.0};  // aggregate initialization
    std::cout << p.x << ", " << p.y << '\n';

    BankAccount acct{"Alice", 1000.0};
    acct.deposit(500.0);
    std::cout << acct.get_balance() << '\n';  // 1500
    // acct.balance = -999;  // ERROR: private member

    return 0;
}

Zugriffsspezifizierer & der this-Zeiger

C++ bietet drei Zugriffsebenen: public (jeder darf zugreifen), private (nur die Klasse selbst und ihre friends) und protected (die Klasse, ihre friends und abgeleitete Klassen). Der this-Zeiger ist ein impliziter Zeiger auf das aktuelle Objekt, der in jeder nicht-statischen Memberfunktion zur Verfügung steht.

access_specifiers.cpp
#include <iostream>
#include <string>

class Employee {
private:
    std::string name_;
    int id_;

protected:
    double salary_;  // accessible to derived classes

public:
    Employee(const std::string& name, int id, double salary)
        : name_{name}, id_{id}, salary_{salary} {}

    // const member function: promises not to modify the object
    const std::string& get_name() const { return name_; }
    int get_id() const { return id_; }

    // 'this' pointer usage — return *this for method chaining
    Employee& set_name(const std::string& name) {
        this->name_ = name;  // 'this->' disambiguates
        return *this;
    }

    Employee& set_id(int id) {
        this->id_ = id;
        return *this;
    }

    void print() const {
        std::cout << "Employee{" << name_ << ", #" << id_
                  << ", $" << salary_ << "}\n";
    }
};

int main() {
    Employee e{"Bob", 42, 75000.0};
    e.set_name("Robert").set_id(43);  // method chaining via *this
    e.print();  // Employee{Robert, #43, $75000}
    return 0;
}

Statische Member & friend

Statische Member gehören zur Klasse, nicht zu einer einzelnen Instanz. Verwende sie für gemeinsam genutzten Zustand oder für Hilfsfunktionen. Das Schlüsselwort friend gewährt einer Nicht-Member-Funktion oder einer anderen Klasse Zugriff auf private Member — setze es sparsam ein, denn es durchbricht die Kapselung.

static_friend.cpp
#include <iostream>

class Widget {
    int value_;

    // C++17: inline static — define in header, no .cpp needed
    inline static int count_ = 0;

public:
    explicit Widget(int v) : value_{v} { ++count_; }
    ~Widget() { --count_; }

    static int get_count() { return count_; }

    // friend function can access private members
    friend std::ostream& operator<<(std::ostream& os, const Widget& w);
};

std::ostream& operator<<(std::ostream& os, const Widget& w) {
    return os << "Widget(" << w.value_ << ')';  // accesses private value_
}

int main() {
    Widget a{10}, b{20};
    std::cout << Widget::get_count() << '\n';  // 2
    std::cout << a << '\n';  // Widget(10)
    {
        Widget c{30};
        std::cout << Widget::get_count() << '\n';  // 3
    }
    std::cout << Widget::get_count() << '\n';  // 2
    return 0;
}

Aggregat-Initialisierung & designierte Initialisierer (C++20)

Ein Aggregat ist eine Klasse oder ein Struct ohne benutzerdeklarierte Konstruktoren, ohne private oder protected nicht-statische Datenmember, ohne virtuelle Funktionen und ohne virtuelle Basisklassen. C++20 ergänzt designierte Initialisierer, mit denen du benennen kannst, welche Member du gerade initialisierst.

designated_init.cpp
#include <iostream>
#include <string>

struct Config {
    std::string host = "localhost";
    int port = 8080;
    bool use_tls = false;
    int max_connections = 100;
};

int main() {
    // Traditional aggregate initialization
    Config c1{"example.com", 443, true, 200};

    // C++20 designated initializers — much more readable
    Config c2{
        .host = "api.example.com",
        .port = 443,
        .use_tls = true
        // .max_connections uses default value 100
    };

    std::cout << c2.host << ':' << c2.port << '\n';
    std::cout << "TLS: " << std::boolalpha << c2.use_tls << '\n';
    std::cout << "Max: " << c2.max_connections << '\n';  // 100
    return 0;
}
Best Practice

Mache Datenmember standardmäßig private und gib über eine minimale öffentliche Schnittstelle nur das nach außen, was wirklich gebraucht wird. Hat eine Klasse keine Invariante zu wahren (ist sie also nur ein Bündel von Daten), verwende stattdessen ein struct mit öffentlichen Membern. Schreibe nicht für jedes Feld Getter und Setter — das führt den Sinn der Kapselung ad absurdum. Biete stattdessen aussagekräftige Operationen wie deposit() und withdraw() an statt set_balance().

Stolperfalle

Wenn eine Memberfunktion den Zustand des Objekts nicht verändert, markiere sie als const. Ohne const kannst du die Funktion weder auf einem const-Objekt noch über eine const-Referenz aufrufen — und genau das passiert ständig, wenn Objekte per const& übergeben werden. Ein fehlendes const an einem Accessor ist in größeren Codebasen eine häufige Quelle von Kompilierfehlern.

Wichtigste Erkenntnisse
  • struct und class unterscheiden sich nur im voreingestellten Zugriff: struct ist public, class ist private
  • Kapselung schützt Klasseninvarianten — verwende private-Daten mit einer aussagekräftigen öffentlichen Schnittstelle
  • Markiere nicht-verändernde Memberfunktionen mit const, damit sie auch mit const-Referenzen funktionieren
  • Verwende inline static (C++17) für statische Member, die in Headern definiert werden
  • Die designierten Initialisierer aus C++20 machen die Aggregat-Initialisierung lesbar und selbsterklärend
  • friend sollte sparsam eingesetzt werden — typischerweise für Operatorüberladungen, die Zugriff auf private Member brauchen

Quiz — Teste dein Wissen

(15 XP)

1. Was ist der EINZIGE Unterschied zwischen `struct` und `class` in C++?

2. Warum solltest du Accessor-Memberfunktionen als `const` markieren?

3. Was bewirkt `inline static int count_ = 0;` in C++17?