Kapitel 13 - Objektorientierte Programmierung

Peter Ulbrich

🚀 by Decker

Lehrevaluation

fig/Evaluation_EidP_WiSe25-26.png

Einleitung

  • Bisher: prozedurale bzw. imperative Programmierung
    • GeprĂ€gt durch hierarchisches Aufrufen von Funktionen
    • Variablen (z.B. int, char), Datenstrukturen (struct) und Funktionen sind getrennt
  • Jetzt: Neue Sichtweise auf die Programmierung

    → Objektorientierte Programmierung (OOP)

  • OOP hat einige interessante Eigenschaften:
    • BĂŒndelt logisch zusammengehörige Funktionen und Daten in einer Datenstruktur → Objekt
    • Sauber definierte Schnittstellen fĂŒr Interaktion mit Daten
    • Verstecken von internen Funktionen bzw. Variablen → information hiding

Keine Sorge! OOP ist „nur“ eine Erweiterung des Bekannten.

Einleitung

  • Modellierung
    • Anwendungsproblem \(\Rightarrow\) Modellierung \(\Rightarrow\) Reduzieren auf das „Wesentliche“

    • „Wesentlich“ im Sinne unserer Sicht auf die Dinge bei diesem Problem

      → Es existieren verschiedene Sichten auf dasselbe Problem!

  • Objektorientierte Programmierung
    • Formulierung eines Modells in Konzepten und Begriffen der realen Welt
    • Nicht in computertechnischen Konstrukten (Hauptprogramm, Unterprogramm, Funktionen, 
)
      • Stattdessen: Klassen (Objekte) mit Attributen und Methoden
      • Interaktion untereinander
  • Achtung: Programmiersprache ist hier nur ein Implementierungsdetail

UML-Klassendiagramme

  • PlĂ€ne zur Beschreibung von abstrakten Objekt-Relationen

Objektorientierte Programmierung

Programmierung bis jetzt

#include <iostream>

struct Punkt {
    double x;
    double y;
};

void print_punkt(Punkt p) {
    std::cout << "X: " << p.x <<
        " Y: " << p.y << std::endl;
}

Objektorientierte Programmierung

#include <iostream>

class Punkt {
    double x;
    double y;
public:
    void print_punkt() {
        std::cout << "X: " << x <<
            " Y: " << y << std::endl;
    }
};

 

AuffĂ€llige Unterschiede: Neue SchlĂŒsselwörter class und public, sonst gleich

Nomenklatur

  • Als Teil einer Klasse heißen 

    • Funktionen → Methoden
    • Variablen → Attribute
  • Achtung: C++ kennt diese Begriffe offiziell nicht
    • Stattdessen: Member einer Klasse
    • Attribut bzw. Methode werden aber trotzdem hĂ€ufig synonym verwendet
  • Bauplan eines Objekts → Klasse
  • Mit Werten gefĂŒllte Klasse → Instanz

Auch hier: Instanz und Objekt werden oft synonym verwendet. Aufpassen!

// Definition des Bauplans
class Punkt {
    double x;
    double y;
};

// Instanz: Initialisierung
Punkt p;

Sichtbarkeit

  • Abkapselung von Informationen mit SchlĂŒsselwörtern public und private
  • Ziele:
    • Sauberer Zugriff auf Variablen
    • Verstecken von Informationen, die nicht öffentlich zugĂ€nglich sein sollen
  • Compiler hilft bei der Durchsetzung
struct Punkt {
    double x;
    double y;
};
// Bisher erlaubt
Punkt p;
p.x = 2.0;

(Bei struct ist alles öffentlich)

class Punkt {
    double x;
    double y;
};
// Compiler-Fehler
Punkt p;
p.x = 2.0;

(Bei class ist alles privat)

Alles unsichtbar

  • Grund fĂŒr Compiler-Fehler: private wird bei Klassen automatisch gesetzt
class Punkt {
// private: implizit gesetzt
    double x;
    double y;
};
class Punkt {
private: // Äquivalent
    double x;
    double y;
};
  • FĂŒr Strukturen ist implizit public → Zugriff erlaubt
    • Einzige Unterschied zwischen Klassen und Strukturen
    • Ansonsten sind sie vollstĂ€ndig identisch
struct Punkt {
// public: implizit gesetzt
    double x;
    double y;
};
struct Punkt {
public: // Äquivalent
    double x;
    double y;
};

Zugriff auf Attribute

  • Externer Zugriff auf private Attribute nur ĂŒber Umwege
  • Grund: Werte der Attribute sollen stets wohldefiniert sein
  • Lösung
    • Verwendung öffentlicher Funktionen fĂŒr Zugriff
    • get() → Abrufen
    • set() → Setzen
    • Heißen deswegen auch Getter bzw. Setter
    • Ermöglicht ÜberprĂŒfung bzw. Konvertierung der Werte
class Punkt {
    double x;
    double y;
public: // Ab hier alles öffentlich
    void set_x(double new_x) {
        if (new_x != 0.0) {
            x = new_x;
        }
    }
    void set_y(double new_y) {
        if (new_y != 0.0) {
            y = new_y;
        }
    }
    double get_x() { return x; }
    double get_y() { return y; }
};

Sichtbarkeitsmodifikation

  • Beliebiges Mischen von private und public ist prinzipiell möglich

  • Aber: Schlecht lesbar und unĂŒbersichtlich

    → Möglichst vermeiden!

  • Bei großen Projekten oft erst öffentlicher Teil

    → Weniger Sucharbeit bei sehr großen Klassen

class VeryBigClass {
public:
    /* Viele Funktionen */
private:
    /* Noch mehr private Details */
};
// Schlecht lesbar
class Punkt {
private:
    double x;
public:
    double get_x() {
        return x;
    }
private:
    double y;
public:
    double get_y() {
        return y;
    }
};

Information Hiding

  • Ziel: Trennung von Klassendefinition und Implementierung
    • Umsetzung ĂŒber Header- und Source-Datei
    • Definition im Header und Implementierung in der Source-Datei
  • HĂ€ufige Konvention:
    • Eine Klasse pro Header-/Source-Datei
    • Name von Header-/Source-Datei entspricht dem Klassennamen
  • Achtung: Die Methoden benötigen hier zusĂ€tzlich das Namespace-PrĂ€fix der Klasse!
// punkt.h
#pragma once

class Punkt {
    double x;
    double y;
public:
    void print_punkt();
};
// punkt.cpp
#include "punkt.h"
#include <iostream>

using std::cout, std::endl;
void Punkt::print_punkt() {
    cout << "X: " << p.x
        << " Y: " << p.y
        << endl;
}

Initialisierung der Member-Variablen

  • Wir kennen jetzt den grundlegenden Aufbau von Klassen in C++
  • Aber: Bisher keine Initialisierung möglich
    • Struct-Initialisierung nicht ohne Weiteres erlaubt
      • Punkt p = {2.0, 3.1}; // Error
    • NachtrĂ€gliches Setzen mithilfe einer set()-Methode → sehr umstĂ€ndlich
class Punkt {
    double x;
    double y;
public:
    void set_x(double new_x) {
        x = new_x;
    }
    double get_x() { return x; }
    /* Restliche Implementierung */
};

Punkt p;
p.set_x(2.0); // UmstÀndlich
  • Lösung: Konstruktor
    • Besondere Methode, die automatisch bei der Initialisierung aufgerufen wird
    • Stellt sicher, dass alle Member-Variablen korrekt und konsistent initialisiert sind

Konstruktoren: Initialisierung der Member-Variablen

  • Konstruktoren haben eine Reihe von besonderen Eigenschaften:
  • Sind an die jeweilige Klasse fest gebunden
  • Haben den gleichen Namen wie die Klasse
  • Können nicht explizit aufgerufen werden
  • Automatischer Aufruf bei Instanziierung
class Punkt {
    double x;
    double y;
public:
    Punkt() {} // Default
    // Parametrisiert
    Punkt(double x, double y)
        : x(x), y(y) {}
};
  • Es gibt vier Hauptarten von Konstruktoren
    • Default-Konstruktor              → Punkt() {}
    • Parametrisierte Konstruktor → Punkt(double nx, double ny) : x(nx), y(ny) {}
    • Copy-Konstruktor                  → Punkt(const Punkt& p) {}
    • Move-Konstruktor                 → Punkt(Punkt&& p) {}

Default-Konstruktor

  • Wird bei jeder Klassendefinition automatisch eingefĂŒgt,

    falls kein anderer Konstruktor definiert wird

  • Ist ein parameterloser Konstruktor

  • Die folgenden beiden Definitionen sind identisch:

class Punkt {
    double x;
    double y;
public:
    Punkt() {} // Explizit genannt
};
class Punkt {
    double x;
    double y;
// public: // Automatisch eingefĂŒgt
//     Punkt() {}
};

Eigenschaften des Default-Konstruktor

  • Initialisiert standardmĂ€ĂŸig keine Member-Variablen
  • Konsistente Initialisierung auf verschiedene Weisen möglich
  • Hier: Durch Überschreiben des Konstruktors bzw. durch Inline-Zuweisungen
#include <iostream>
using std::cout;
class Punkt {
    double x = 1.0; // Inline
    double y = 1.0;
public:
    Punkt() {}
    double get_x() { return x; }
    double get_y() { return y; }
};
int main() {
    Punkt p;
    cout << p.get_x() << " " << p.get_y();
}
cpp
#include <iostream>
using std::cout;
class Punkt {
    double x;
    double y;
public:
    Punkt() { x = 2.0; y = 2.0; }
    double get_x() { return x; }
    double get_y() { return y; }
};
int main() {
    Punkt p;
    cout << p.get_x() << " " << p.get_y();
}
cpp

Member Initializer-Liste

  • Initialisierung der Member ĂŒber eine separate Stelle im Code

  • Eigenschaft: wird vor dem Body der Funktion ausgefĂŒhrt

  • Aufbau:

    <class name>(<function parameters>) : <initializer list> { <function body> }

  • Wichtig:

    Reihenfolge der Initialisierung = Reihenfolge der Deklaration

  • Alternative Schreibweise: {} statt ()

class Punkt {
    double y;
    double x;
public:
    Punkt() : x(2.0), y(1.0) {}
    // Alternativ: Braced Init.
    Punkt() : x{2.0}, y{1.0} {}

    /* Restliche Implementierung */
};

Parametrisierter Konstruktor

  • Initialisierung der Member mit beliebigen Werten

  • Prinzip bereits bekannt, jetzt mit Funktionsparametern

  • Wichtig: Beliebig viele, verschiedene Konstruktoren erlaubt

    → Nennt sich Überladung des Konstruktors

class Punkt {
    double x_;
    double y_;
    bool g_; // Geheim
public:
    Punkt() :
        x_(2.0), y_(1.0), g_(false) {}

    Punkt(double x, double y) :
        x_(x), y_(y), g_(false) {}

    Punkt(double x, double y, bool g) :
        x_(x), y_(y), g_(g) {}

    /* Restliche Implementierung */
};

Parametrisierter Konstruktor

#include <iostream>

using std::cout, std:: endl;

class Punkt {
    double x_;
    double y_;
    bool g_; // Geheim
public:
    Punkt() : x_(2.0), y_(1.0), g_(false) {}

    Punkt(double x, double y) : x_(x), y_(y), g_(false) {}

    Punkt(double x, double y, bool g) : x_(x), y_(y), g_(g) {}

    void print_punkt() {
        cout << "X: " << x_ << " Y: " << y_ << " G: " << g_ << endl;
    }
};

int main() {
    Punkt p1;
    Punkt p2( 1.5, 2.5);
    Punkt p3{ 3.14, 47.11, true };

    p1.print_punkt();
    p2.print_punkt();
    p3.print_punkt();
}
cpp

Konstruktoren

  • Bisher: Beliebige Anzahl von verschiedener Konstruktoren definieren
  • Aber: Können wir Konstruktoren auch verbieten?
  • Lösung: SchlĂŒsselwort delete löscht automatisch erzeugte Konstruktoren

    → Punkt P() = delete;

  • Analog dazu: Konstruktoren auch explizit als default markierbar

    → Punkt P() = default;

  • delete und default sollen die Intention des Programmierers verdeutlichen

    → Macht Außenstehendem sofort deutlich, dass die Konstruktoren korrekt sind

Destruktor

  • Das Erzeugen und Initialisieren von Objekten ist jetzt bekannt
  • Der durch Objekte belegte Speicher muss aber irgendwann auch wieder freigegeben werden
    • FĂŒr primitive Datentypen wie int, double geschieht dies automatisch
    • Aber: Komplexere Datentypen wie Zeiger mĂŒssen explizit wieder freigegeben werden
    • Gleiches gilt im Übrigen auch fĂŒr von der Klasse gehaltene Ressourcen
      • Beispiele: Geöffnete Datei, aktive Datenbankverbindung
      • 
 oder was auch immer gerade benötigt wird
  • Hier wird der Destruktor relevant!

Destruktor

  • Destruktor ist das Komplement zum Konstruktor
    • Benennung: Vorangestellte Tilde (~) vor dem Klassennamen → ~Punkt() {}
    • Wird ebenfalls automatisch erzeugt und muss ggf. ĂŒberschrieben werden
    • Allerdings keine Überladung möglich (immer nur ein Destruktor pro Klasse)
  • Wird bei Verlassen des Scopes aufgerufen, in dem das Objekt instanziiert wurde
class DataHandle {
    void * data;
    int size_;
public:
    DataHandle() = delete;
    DataHandle(int size): size_(size) {
        data = malloc(size * sizeof(void*));
    }
    ~DataHandle() {
        free(data);
    }
};


int main() {
    DataHandle dh{3}; // Instanziierung
    /* Arbeite mit dh */
}   /* Aufruf Destruktor dh */

Destruktor

#include <iostream>

class DataHandle {
    void * data;
    int size_;
public:
    DataHandle() = delete;
    DataHandle(int size): size_(size) {
        data = malloc(size * sizeof(void*));
        std::cout << "Constructor called" << std::endl;
    }
    ~DataHandle() {
        free(data);
        std::cout << "Destructor called" << std::endl;
    }
};

int main() {
    std:: cout << "Entering main()" << std::endl;
    DataHandle dh{3}; // Instanziierung
    std:: cout << "Exiting main()" << std::endl;
}
cpp

Beispiel - Copy-Konstruktor

  • Angenommen, folgender Code wird ausgefĂŒhrt. Was wird passieren?
#include <iostream>

class DataHandle {
    void * data;
    int size_;
public:
    DataHandle() = delete;
    DataHandle(int size): size_(size) {
        data = malloc(size * sizeof(void*));
    }
    ~DataHandle() {
        std::cout << "Destructor called" << std::endl;
        free(data);
    }
};

int main() {
    DataHandle dh1{4};
    {
        DataHandle dh2 = dh1;
    }
}
cpp

Copy-Konstruktor

  • dh2 ist eine bitweise Kopie von dh1 (Shallow Copy)
  • Beinhaltet auch den Zeiger data
  • dh1.data und dh2.data zeigen auf gleichen Speicher
  • Doppelter Aufruf von Destruktor
  • Ergebnis: Zweifaches free() fĂŒhrt zu Absturz des Programms đŸ€Ż
class DataHandle {
    void * data;
    int size_;
public:
    ~DataHandle() {
        std::cout
            << "Destructor called"
            << std::endl;
        free(data);
    }
};
int main() {
    DataHandle dh1{4};
    {
        DataHandle dh2 = dh1;
    } // Destruktor dh2
}     // Destruktor dh1. BOOM

Deshalb gibt es den Copy-Konstruktor

Copy-Konstruktor

  • Copy-Konstruktor hat einen Parameter → Referenz auf Objekt
    • Beispiel: DataHandle(const DataHandle& d) {}
    • const verbietet Modifikation des Originals
    • Referenz → kein Copy-by-Value des Originals (aufwendig)
  • Korrektes Initialisieren von Member (Deep Copy)
class DataHandle {
    void * data;
    int size_;
public:
    DataHandle(const DataHandle& d) : size_(d.size_) {
        data = malloc(d.size_);
        std::memcpy(data, d.data, d.size_);
    }
};

Copy-Konstruktor

#include <iostream>
#include <cstring>

class DataHandle {
    void * data;
    int size_;
public:
    DataHandle() = delete;
    DataHandle(int size): size_(size) {
        data = malloc(size * sizeof(void*));
        std::cout << "Constructor called" << std::endl;
    }
    DataHandle(const DataHandle & d) : size_(d.size_) {
        data = malloc(d.size_);
        std::memcpy(data, d.data, d.size_);
        std::cout << "Copy Constructor called" << std::endl;
    }
    ~DataHandle() {
        free(data);
        std::cout << "Destructor called" << std::endl;
    }
};


int main() {
    std:: cout << "main(): Enter" << std::endl;
    DataHandle dh1{4};
    {
        DataHandle dh2 = dh1;
    }
    std:: cout << "main(): Exit" << std::endl;
}
cpp

Move-Konstruktor

  • Ist ein etwas fortgeschritttener Aspekt von C++
  • Hier: Konzept verstehen, statt jedes technische Detail zu ergrĂŒnden
  • ZunĂ€chst ein abstraktes Beispiel: Staffellauf
  • Dabei ĂŒbergibt ein bisheriger LĂ€ufer (→ Objekt) einem neuen LĂ€ufer eine wichtige Ressource (→ Stab)
  • Mit unserem bisherigen Wissen wĂ€re das nicht gut umsetzbar 🙁

An dieser Stelle kommt der Move-Konstruktor ins Spiel!

Move-Konstruktor

  • Ziel: effizientes Verschieben von Ressourcen von einer Instanz zu einer anderen
    • Beispiel Zeiger: Verschieben des Zeigerwerts zum neuen Objekt

    • Wichtig: Originales Objekt sollte danach keinen Zugriff mehr auf die Ressource haben

      → Bester Weg: Alter Zeiger wird nullptr

  • Spezielle Signatur:

    <Type>(<Type>&& var_name);

    • Beispiel: DataHandle(DataHandle&& dh);
    • Formal ist && dh eine Referenz auf einen Rvalue
    • Der VollstĂ€ndigkeit halber: Rvalues sind temporĂ€r existierende Werte/AusdrĂŒcke (z.B. 3 + 3;)
3 + 3; // Temp. Rvalue

Rvalues sollen hier nicht weiter interessieren!

Move-Konstruktor

  • C++ bietet fĂŒr die Konvertierung eine Hilfsfunktion: std::move
#include <utility> // fĂŒr std::move
DataHandle dh1("foo");
// Type-Casting auf DataHandle &&
// gefolgt von Konstruktor-Aufruf
DataHandle dh2(std::move(dh1));
  • IrrefĂŒhrender Name, std::move bewegt eigentlich nichts

  • Stattdessen nur Type-Casting zu Referenz auf Rvalue

    → DataHandle wird zu DataHandle &&

  • Gecasteter Wert wird anschließend als Parameter im Konstruktor-Aufruf verwendet

Move-Konstruktor

  • Move-Konstruktor hat grundsĂ€tzlich Ähnlichkeit zum Copy-Konstruktor

  • Wichtiger Unterschied: Ressourcen beim alten Objekt unzugĂ€nglich machen

  • Grund: Alte Objekte existieren nach Move weiterhin

    (und könnten prinzipiell auch Ressourcen manipulieren)

    class DataHandle {
      void * data;
      int size_;
    public:
      DataHandle(DataHandle && dh) : data(dh.data), size_(dh.size_) {
          dh.data = nullptr; // Zugangssperre
      }
      /* Restliche Implementierung */
    };

Aufrufreihenfolge Destruktor

  • Wichtig: Aufrufreihenfolge von Konstruktoren und Destruktoren
  • Zerstörung immer in umgekehrter Reihenfolge zur Deklarierung
#include <iostream>
using std::cout, std::endl;

class A {
public:
    A() { cout << "A: Constructor" << endl; }
    ~A() { cout << "A: Destructor" << endl; }
};

class B {
public:
    B() { cout << "B: Constructor" << endl; }
    ~B() { cout << "B: Destructor" << endl; }
};

int main() {
    A a;
    B b;
}
cpp

Resource Acquisition Is Initialization (RAII)

  • Ist eine interessante Programmiertechnik: Resource Acquisition Is Initialization (RAII)
    • Möglich durch automatische Destruktoraufrufe bei Verlassen des Scopes
    • Ressourcen in diesem Kontext mĂŒssen vorher akquiriert werden
    • Beispiel: Öffnen einer Datei bzw. Datenbankverbindung
  • Grundgedanke: Bei Konstruktor-Aufruf wird Ressource akquiriert, bei Destruktor-Aufruf automatisch wieder freigegeben
  • Sehr praktisch: Speicherleck/Ressourcenleck wird dadurch unmöglich

Beispiel - Resource Acquisition Is Initialization

  • Situation: Automatisches Öffnen und Schließen einer Datei bei Initialisierung und Destruktor-Aufruf
#include <iostream>
#include <fstream>

using std::cout, std::endl;
using std::ios;

class FileHandler {
    std::fstream file; // File Handle
public:
    FileHandler(const std::string& filename) {
        file.open(filename, ios::in | ios::app);
        if (file.is_open()) {
            cout << "File opened successfully" << endl;
        }
    }
    ~FileHandler() {
        if (file.is_open()) {
            file.close();
            cout << "File closed" << endl;
        }
    }
    /* Implementierung (read(), write(), ...) */
};

int main() {
    cout << "main(): enter" << endl;
    {
        FileHandler fh("test.txt");
        /* Lese-/Schreiboperationen der Datei... */
    }
    cout << "main(): exit" << endl;
}
cpp

RAII ist eine extrem nĂŒtzliche Programmiertechnik. Nutzt sie!

Zwischenstand

  • Zeit fĂŒr einen kurzen Zwischenstand:
    • Grundlegende Eigenschaften von Objekten wurden vorgestellt
    • Die grundlegende Reihenfolge der Initialisierung/Destruktion ebenso
    • Konstruktoren/Destruktoren sind jetzt bekannt
  • Es fehlt: Smart Pointer (besseres Speicher-Management bei Zeigern)
    • Teil einer Reihe von Verbesserungen, die mit C++11 eingefĂŒhrt wurden

ZunÀchst einmal kommen aber noch einige allgemeine Konzepte!

Operator-Überladung

  • Zur Erinnerung: C++ erlaubt das Überladen von Funktionen (und Konstruktoren)
  • Außerdem: Operatoren könnnen auch ĂŒberladen werden (Klassen/Strukturen)
    • Bereits bekannt: <<-Operator bei std::cout
    • std::cout << "Echo";
  • Überladung mithilfe des SchlĂŒsselworts operator
    • operator+=(int i); // Überlade += fĂŒr int-Parameter
  • Ist fĂŒr die meisten Operatoren möglich, mit ein paar Ausnahmen:
    • . (bzw. .*) → Member-Zugriff (bzw. Member-Zugriff ĂŒber Zeiger)
    • :: → Scope Resolution Operator
    • ?: → TernĂ€rer Operator

Beispiel - Operator-Überladung

  • Beispiel: Wrapper-Klasse fĂŒr Integer
#include <iostream>

class Integer {
    int val;
public:
    Integer() = delete;
    Integer(int i) : val(i) {}

    int operator+(int summand) { return val + summand; }
    void operator+=(int summand) { val += summand; }

    int value() { return val; }
};

int main() {
    Integer I{3};
    std::cout << "Sum: " << I + 5 << std::endl;
    I += 10;
    std::cout << "New value: " << I.value() << std::endl;
}
cpp

Operator-Überladung ist sehr nĂŒtzlich, wenn es an den richtigen Stellen eingesetzt wird!

Einschub: Rule of Three/Five

  • Betrachtung einer wichtigen Regel in C++: Rule of Three/Five

  • Rule of Three

    • Wenn es notwendig ist, einen eigenen Destruktor, Copy-Konstruktor, oder Copy-Assignment-Operator (operator=(const &)) zu erstellen, sollen* alle drei erstellt werden
  • Rule of Five

    • Erweiterung der Rule of Three um Move-Konstruktor und Move-Assignment-Operator (operator=(&&))
  • Grund ist simpel: Ressourcen sollen immer konsistent initialisiert werden bzw. verfĂŒgbar sein

SchlĂŒsselwort friend

  • Externer Zugriff auf Member-Variablen nur via Methoden
  • Ausgangslage: Funktionen oder fremde Klasse soll direkten Zugriff haben
  • Problem: C++ verbietet das! đŸ˜±
class Punkt {
    double x;
    double y;
public:
    void print_punkt() { // Ok
        cout << "X=" << x <<
            " Y=" << y << endl;
    }
};
// Überladung von Operator << fĂŒr cout
// Compiler-Error: Zugriff auf private Member
ostream& operator<<(ostream& os, Punkt& p) {
    return os << "X=" << p.x
        << "Y=" << p.y << endl;
}
  • Mit den aktuellen Möglichkeiten geht das nicht!

    → Lösung: Neues SchlĂŒsselwort friend 😀

SchlĂŒsselwort friend

  • friend gestattet selektiven Zugriff fĂŒr externe Klassen und Funktionen

    → friend ostream& operator<<(ostream& os, Punkt& p);

  • Randbemerkung: Aufruf von externen Funktionen ohne Namespace-PrĂ€fix

#include <iostream>
using std::cout, std::endl;
using std::ostream;

class Punkt {
    double x;
    double y;
public:
    Punkt(double x, double y) : x(x), y(y) {}

    friend void print_punkt(Punkt& p);
    friend ostream& operator<<(ostream& os, Punkt& p);
};

ostream& operator<<(ostream& os, Punkt& p) { // Extern definiert
    cout << "Member: X=" << p.x << " Y=" << p.y << endl;
    return os;
}

void print_punkt(Punkt& p) {
    cout << "Member: X=" << p.x << " Y=" << p.y << endl;
}

int main() {
    Punkt p{1.5, 2.3};
    cout << "Ostream: \t" << p; // Jetzt erlaubt
    cout << "print_punkt: \t";
    print_punkt(p);
}
cpp

SchlĂŒsselwort this

  • Nur innerhalb einer Klasse verfĂŒgbar
  • Zeiger auf die aktuelle Instanz der Klasse (wird automatisch erzeugt)
  • Verwendung wie alle anderen Zeiger
    • this->print();
    • *this; // Objekt selbst
  • Vorteile
    • Vermeidung von Namenskonflikten
    • Manche OperatorĂŒberladungen erfordern Verweis auf sich selbst
    • Explizite Nennung verbessert Lesbarkeit des Quellcodes
class Integer {
    int val;
public:
    Integer(int i) : val(i) {}

    int value() {
        return val; // Implizit
    }
    int value_explicit() {
        return this->val;
    }
};

SchlĂŒsselwort static in Klassen

  • Member dĂŒrfen als static deklariert werden
  • Gehören keinem Objekt an, sondern der Klasse selbst
#include <iostream>

using std::cout, std::endl;

class A {
public:
    static int _count;
    A() { _count++; }
    ~A() { _count--; }
    static int count() { return _count; }
};
int A::_count = 0; // Initialisierung *außerhalb* der Klasse in *.cpp-Datei

int main() {
    A a1, a2;
    {
        cout << A::count() << endl;
        A a3;
        cout << A::count() << endl;
    }
    cout << A::count();
    return 0;
}
cpp

Dynamische Allokation in C++ (new/delete)

  • C++ erweitert die aus C bekannten Funktionen malloc und free

  • Operatoren ersetzen die Funktionen

    • malloc → new (dyn. Allokation)
    • free → delete (dyn. Deallokation)
  • Im globalen Namespace enthalten

    → kein Einbinden von Headern notwendig

  • Ansonsten gleiche Eigenschaften wie malloc und free

#include <iostream>
using std::cout, std::endl;

class Foo {
public:
    int bar;
    Foo(int val) : bar(val) {}
};

int main() {
    Foo * foo = new Foo(1);
    int * i = new int(5);
    cout << "Foo: " << foo->bar << endl;
    cout << "Int: " << *i << endl;
    delete i;
    delete foo;
}
cpp

Dynamische Allokation in C++ (new/delete)

  • new und delete können auch mit Arrays umgehen
  • Das Löschen von Arrays erfolgt mit Operator delete[]
  • Achtung Stolperfalle: Bei Arrays mit mehreren Dimensionen muss ĂŒber die Dimensionen iteriert werden
Foo * foo = new Foo[ROWS][COLS];
for (int i = 0; i < ROWS; ++i) {
    delete[] foo[i]; // Spalte i löschen
}
delete[] foo; // Alle Zeilen
f = nullptr; // Zeigerwert löschen
#include <iostream>
using std::cout, std::endl;

class Foo {
public:
    int bar;
    Foo() : bar(0) {}
    Foo(int val) : bar(val) {}
};

int main() {
    // 10 Foo-Objekte, default-initialisiert
    Foo * foo = new Foo[10];
    int * i = new int[5] { 1, 2, 4, 8, 16 };

    foo[2].bar = 4;
    cout << "Foo: " << foo[2].bar << endl;
    cout << "Int: " << i[4] << endl;
    delete[] i;
    delete[] foo;
}
cpp

Dynamische Allokation in C++ (new/delete)

  • new und delete haben die gleichen SchwĂ€chen wie free und malloc 🙁
  • Syntax ist jetzt etwas angenehmer
  • Aber: Programmierer:in kann immer noch Aufrufe vergessen
  • Ist es also am Ende in C++ genauso schlimm wie in C?

    → Nein!

  • C++11 hat die sogenannten Smart Pointer eingefĂŒhrt

Smart Pointer

  • Seit C++11 Bestanteil des Standards
  • Objekte kapseln Zeigervariable
  • Verwenden dazu im Kern das RAII-Idiom
  • Zwei wichtige Typen
    • unique_ptr
    • shared_ptr
  • Klassische Zeiger werden oft als Raw Pointer bezeichnet

Unique Pointer

  • Lebensdauer von unique_ptr vollstĂ€ndig an den aktuellen Scope gebunden (→ RAII)
  • Bei Verlassen des Scopes wird ein unique_ptr zerstört
// Vereinfachte Darstellung eines
// Unique Pointers fĂŒr int *
class UniquePtr {
private:
    int * ptr_;
public:
    UniquePtr(int * ptr) : ptr_(ptr) {}
    ~UniquePtr() { delete ptr_; }
}

Unique Pointer

  • Benutzung von Unique Pointer ist sehr einfach

    → Deklaration: std::unique_ptr<int> i_ptr; // Zeiger auf int *

  • Erstellung eines neuen Zeigers mithilfe std::make_unique
#include <memory> // std::unique_ptr
using std::unique_ptr;
using std::make_unique;

// Erstellt neuen int *
// Wert des derefenzierten int* ist 5
unique_ptr<int> i_ptr = make_unique<int>(5);

int i = *i_ptr + 3; // 8
  • Bestehenden Zeiger ĂŒbernehmen → Konstruktur von unique_ptr
#include <memory> // std::unique_ptr
using std::unique_ptr;

class Foo{ /* */ };

int main() {
    Foo * f = new Foo();
    unique_ptr<Foo> f_ptr(f);
} // f_ptr ruft delete

Unique Pointer

  • Dank Operator-Überladung Zugriff auf gekapselten Zeiger
  • Dereferenzierung mit *i_ptr

  • Member-Zugriff mit ->

    (bei Klassen)

Dereferenzierung

unique_ptr<int> i_ptr = make_unique<int>(5);

int i = *i_ptr + 3; // 8

Member-Zugriff

class Foo {
public:
    void bar() {}
}

unique_ptr<Foo> f_ptr = make_unique<Foo>();

f_ptr->bar();

Beispiel - Unique Pointer

#include <memory>
#include <iostream>

using std::unique_ptr;
using std::cout, std::endl;

class Foo {
public:
    Foo() { cout << "Foo constructor called\n"; }
    ~Foo() { cout << "Foo destructor called\n"; }

    void print() { cout << "Hello Foo\n"; }
};

int main() {
    cout << "main(): Enter" << endl;
    Foo * f = new Foo();
    {
        cout << "Scope: Enter" << endl;
        unique_ptr<Foo> f_ptr(f); // Konstruktoraufruf, Übernahme von f
        f_ptr->print();
        cout << "Scope: Exit" << endl;
    } // delete f
    cout << "main(): Exit" << endl;
}
cpp

Unique Pointer sind deutlich besser als hÀndisches Verwalten! Nutzt sie, wenn ihr könnt.

Shared Pointer

  • Analog zu unique_ptr gibt es noch shared_ptr
    • Hauptzweck: Einsatz in nebenlĂ€ufiger Programmierung (→ z.B. auf mehreren CPU-Kernen)
    • Unterschied zu unique_ptr: Bei neuem Kopieren wird ZĂ€hler inkrementiert (ZĂ€hlen der Referenzen)
class SharedPtr { // Vereinfachte Darstellung
private:
    int * ptr;
    int counter = 1;
public:
    SharedPtr(SharedPtr& cpy) { ++counter; }
    ~SharedPtr() {
        --counter;
        if (counter == 0)
            delete ptr;
    }
}
  • Bei Aufrufen des Destruktors wird dieser ZĂ€hler dekrementiert
    • Erst wenn ZĂ€hler Wert 0 annimmt, wird der gehaltene Zeiger zerstört
  • Achtung: Zum Teilen von Membern in verschiedenen Klassen gedacht
    • Aber: Widerspricht dem Grundgedanken der sauberen Kapselung durch Klassen

Überdenkt euer Design, falls der Einsatz von shared_ptr jemals notwendig erscheinen sollte shared_ptr ist hauptsĂ€chlich fĂŒr nebenlĂ€ufige Programmierung gedacht!

Einschub: Garbage Collection

  • Idee hinter Shared Pointer: ZĂ€hlen von Referenzen
  • Gibt es auch in anderen Programmiersparchen: z.B. Java, Python, C#, Javascript
  • Keine automatischen Destruktor-Aufrufe vorhanden wie in C++
  • Stattdessen: Konzept namens Garbage Collection
    • Dazu wird fĂŒr jedes Objekt zusĂ€tzlich ein ReferenzenzĂ€hler angelegt
  • AusfĂŒhrungsumgebung (Runtime) pausiert die AusfĂŒhrung regelmĂ€ĂŸig und zĂ€hlt
    • Falls ein ZĂ€hler den Wert 0 hat, wird das zugehörige Objekt entfernt
    • Das Programm steht in der Zwischenzeit!
      • NatĂŒrlich deutlich langsamer als in C++ (Je nach Sprache 1-2 GrĂ¶ĂŸenordnungen)
      • DafĂŒr muss der Programmierer sich aber keine Gedanken ĂŒber Speichermanagement machen

Zusammenfassung

  • Aufbau von Klassen
    • Sichtbarkeitsmodifikatoren
    • Konstruktoren (Copy, Move, 
)
    • Destruktoren
  • Konzept der Operator-Überladung
  • Resource Acquisition Is Initialization
  • Dynamische Speicherallokation(new / delete)
  • Smart Pointer
Logo Sys
Kapitel 13 - Objektorientierte Programmierung