Kapitel 14 - Vererbung

Peter Ulbrich

🚀 by Decker

Einleitung

  • Bisher: Wichtigste Eigenschaften von OOP
    • Sichtbarkeit/Kapselung (→ private/public)
    • Darauf aufbauend: Abstraktion (Verstecken der Implementierungsdetails)
  • Für reale Anwendungsfälle fehlt aber noch etwas…
    • Vererbung (heute)
    • Polymorphismus

Initialisierung von Member-Objekten

  • Im letzten Kapitel ausgeklammert:

    Wie werden Objekte, die Member sind, korrekt initialisiert?

  • Objekte müssen als Member natürlich auch konsistent initialisiert werden
  • Zwei Möglichkeiten
    • Automatische Default-Initialisierung (sofern möglich)
    • Expliziter Aufruf des Konstruktors in der Initialisierungsliste
class Foo {
    int i;
public:
    Foo() : i(0) {}
    Foo(int num) : i(num) {}
};

class Bar {
    Foo f;
    double j;
public:
    Bar(double d) : j(d) {} // f() automatisch
    Bar(int num, double d) : f(num), j(d) {}
};

(Die Destruktoren von Membern werden übrigens auch automatisch bei Aufruf des Destruktors aufgerufen)

Beispiel - Initialisierung von Member-Objekten

#include <iostream>

using std::cout, std::endl;

class Foo {
    int i;
public:
    Foo() : i(0) { cout << "Foo default constructor called" << endl; }
    Foo(int num) : i(num) { cout << "Foo constructor called" << endl; }
    ~Foo() { cout << "Foo destructor called" << endl; }
    int value() { return i; }
};

class Bar {
    Foo f;
    double j;
public:
    Bar(double d) : j(d) { cout << "Bar default constructor called" << endl; }
    Bar(int num, double d) : f(num), j(d) { cout << "Bar constructor called" << endl; }
    ~Bar() { cout << "Bar destructor called" << endl; }
    void print() { cout << "Values: " << f.value() << ", " << j << endl; }
};

int main() {
    cout << "Entering main()" << std::endl;
    Bar b(1, 2.5);
    b.print();
    cout << "Exiting main()" << std::endl;
}
cpp

Und nun zur Vererbung 🥳

Einführungsbeispiel zur Vererbung

  • Beispiel: Tiere
  • Mit dem aktuellen Wissen wäre das bereits prinzipiell umsetzbar
class Dog {
    std::string name;
    std::string laut;
};
class Cat {
    std::string name;
    std::string laut;
};
class Mouse {
    std::string name;
    std::string laut;
};
  • Probleme
    • Sehr aufwendig, starke Duplizierung von Code
    • Fehleranfällig: Member vergessen beim Kopieren, falsche Initialisierung
    • …und sehr stupide Arbeit

Vererbung

  • Bessere Lösung: Vererbung

  • Tiere haben gemeinsame Eigenschaften

    • Zusammenfassen in einer Oberklasse (häufig auch Elternklasse bzw. Basisklasse)
    • Oberklasse Animal
  • Unterklassen Dog und Cat erben von Animal

    • Methoden und Member der Oberklasse stehen zur Verfügung
    • Hinzufügen weiterer Member in den Unterklassen möglich
    • Ebenso: Überschreiben von existierenden Methoden
  • Wichtige Eigenschaft: Jede Instanz einer Unterklasse ist gleichzeitig Instanz der Oberklasse

    • Beispiel: „Jeder Hund ist gleichzeitig ein Tier“

Vererbung in UML

fig/uml-generalization-example.svg
Beispiel für eine Vererbung in UML
  • Besondere Kennzeichnung: Weiße Pfeilspitze, zeigt auf Basisklasse
  • Wird in UML auch Generalisierung genannt

Grundlagen der Vererbung

  • Vererbung findet in C++ bei der Klassendeklaration statt
  • Auflistung Basisklassen hinter Klassennamen
  • Initialiserung der Basisklasse im Konstruktor
  • Parameter werden entsprechend weitergeleitet
using std::string;
using std::cout;
class Animal {
private:
    string name;
    string laut;
public:
    Animal() = delete;
    Animal(string n, string l):
        name(n), laut(l) {}
};

class Dog : public Animal {
public:
    Dog(string name, string laut):
        Animal(name, laut) {}
    void bellen() {
        cout << this->laut;
    }
};

Initialisierungsreihenfolge

  • Es gilt: Ober- vor Unterklasse
  • Erst Konstruktoren der Oberklasse(n) aufrufen
  • Danach folgt Initialisierung der Kindklasse
  • Randbemerkung: Der Vorgang des Erbens wird häufig auch Ableitung genannt

    → „Dog wird von Animal abgeleitet“

  • Auch hier gilt: Die Begriffe Erben und Ableiten werden häufig synonym verwendet

using std::string;
using std::cout;
class Animal {
private:
    string name;
    string laut;
public:
    Animal() = delete;
    Animal(string n, string l):
        name(n), laut(l) {}
};

class Dog : public Animal {
public:
    Dog(string name, string laut):
        Animal(name, laut) {}
    void bellen() {
        cout << this->laut;
    }
}

Sichtbarkeit bei Vererbung

  • Ist jetzt alles geklärt? Leider nein 🙁

  • private bedeutet wirklich privat!

    nur die Klasse kann zugreifen

  • Die Methode bellen() erzeugt Compiler-Fehler

#include <string>
#include <iostream>
using std::cout;
using std::string;

class Animal {
private:
    string name;
    string laut;
public:
    Animal() = delete;
    Animal(string n, string l):
        name(n), laut(l) {}
};

class Dog : public Animal {
private:
    bool lieb = true;
public:
    Dog(string name, string laut):
        Animal(name, laut) {}
    void bellen() { cout << this->laut; }
};

int main() {
    Dog paul("Paul", "Wuff");
    paul.bellen();
}
cpp

Selektive Vererbung

  • Problem: Nur ein Teil der Basisklasse vererben
  • Lösung: Schlüsselwort protected 🔒
    • Verwendung wie private und public
    • Member mit protected sind für Kindklassen sichtbar
class Animal {
private:   // Unsichtbar in Kind
    bool secret = true;
protected: // Ab hier: sichtbar
    std::string name;
    std::string laut;
public:
    Animal() = delete;
    Animal(string n, string l):
        name(n), laut(l) {}
};
  • Der Bereich protected ist so etwas wie ein Familiengeheimnis

    → Die Kinder kennen es, aber außerhalb der Familie niemand

  • private entspricht hingegen einem persönlichen Geheimnis

    → Ist nur der Klasse selbst bekannt

Vererbung

Jetzt funktioniert alles ✅

#include <iostream>

using namespace std;

class Animal {
private:   // Unsichtbar in Kind
    bool secret = true;
protected: // Ab hier: sichtbar
    std::string name;
    std::string laut;
public:
    Animal() = delete;
    Animal(string n, string l):
        name(n), laut(l) {}
};

class Dog : public Animal {
public:
    Dog(string name, string laut):
        Animal(name, laut) {}
    void bellen() { cout << this->laut << endl; }
};

int main() {
    Dog paul("Paul", "Wuff Wuff");
    paul.bellen();

}
cpp

Selektive Vererbung

  • Eigenart von C++: Basisklassen automatisch als private deklariert
    • Folge: Alle Member der Basisklasse werden automatisch private
    • Kein Zugriff durch Kindklasse mehr erlaubt
    • Analog für protected (verbietet public)
  • Umgehung durch explizite Deklaration als public
  • Achtung: Sehr beliebter Flüchtigkeitsfehler
class Animal {
private:   // Unsichtbar in Kind
    int secret;
protected: // Ab hier: sichtbar
    string name;
    string laut;
public:
    string get_name() {
        return name;
    }
};
// Alles aus Animal wird private
class Dog : Animal {};

// Ändert public zu protected
class Dog : protected Animal {};

// Keine Veränderungen
class Dog : public Animal {};

Mehrfachvererbung

  • Ableitung von mehreren Basisklassen
  • Prinzipiell genau gleich wie bisher
  • Einzige Besonderheit: Reihenfolge der Konstruktoraufrufe der Basisklassen hängt von der Auflistung während der Deklaration ab (von links nach rechts)
class Pet {
protected:
    bool is_pet; // Haustier-Check
};

class Animal {
private:   // Unsichtbar in Kind
    int secret;
protected: // Ab hier: sichtbar
    string name;
    string laut;
public:
    string get_name() {
        return name;
    }
};

// Erst Init. von Pet, dann Animal
class Dog : public Pet, public Animal {};

Mehrfachvererbung

  • Klassen können beliebig oft abgeleitet werden
  • Das wird auch so gemacht!
  • Darstellung aller Vererbungen über sogenannte Klassenhierarchie (typischerweise UML-Grafik)
fig/uml-class-hierarchy-example.svg

Mehrfachvererbung

  • Problem: Praktische Umsetzung in C++ dieser Klassenhierarchie nicht möglich
  • Bei Deklaration von Poodle sind die Klassen Mother und Father noch gar nicht deklariert
  • Hier hilft Forward Declaration
  • Aus C kennen wir etwas Verwandtes: extern
// Forward Declaration
class Father;
class Mother;

class Poodle {
    Father vater;
    Mother mutter;
}

Großes Beispiel

  • Praktisch umgesetzt sieht diese Hierarchie folgendermaßen aus
#include <string>
#include <iostream>

using std::string;
using std::cout;

class Animal {
protected: // Ab hier: sichtbar
    std::string name;
    std::string laut;
public:
    Animal() = delete;
    Animal(string n, string l):
        name(n), laut(l) {}
};

class Dog : public Animal {
public:
    Dog(string name, string laut):
        Animal(name, laut) {}
    void bellen() { cout << this->laut; }

};

class Father;
class Mother;
class Poodle: public Dog {
protected:
    bool flauschig = true;
    Father* vater;
    Mother* mutter;
public:
    Poodle(string name, string laut) :
        Dog(name, laut), vater(nullptr), mutter(nullptr) {}
    Poodle(string name, string laut, Father* v, Mother* m) :
        Dog(name, laut), vater(v), mutter(m) {}
};

class Father: public Poodle {
    bool is_father = true;
public:
    Father(string name, string laut) :
        Poodle(name, laut) {}
};

class Mother: public Poodle {
    bool is_mother = true;
public:
    Mother(string name, string laut) :
        Poodle(name, laut) {}
};

int main() {
    Father paul_senior ("Paul Senior", "Wau");
    Mother martha ("Martha", "Woof");
    Poodle paul("Paul", "Wuff Wuff", &paul_senior, &martha);
    paul.bellen();
}
cpp

Umgang mit verschiedenen Kindklassen

  • Szenario: Abspeichern von Objekten verschiedener Kindklassen
    • Zum Beispiel: Array aller Katzen und Hunde
    • Mit klassischen Arrays geht das nicht → zwei verschiedene Typen
  • Lösung: Beide sind Objekte vom Typ Animal → Array vom Typ Animal
Animal anim_array[10]; // Default-Init.

anim_array[0] = new Dog("Paul", "Wuff");
  • Wichtig: Methoden von Kindklassen sind nicht unmittelbar zugreifbar
    • Diese Eigenschaft wird als Slicing bezeichnet → mehr im nächsten Kapitel
    • Umweg über Type Casting möglich
  • Das Gleiche gilt natürlich auch für Zeiger!
Animal* a = new Dog("Paul", "Wuff");

unique_ptr<Animal> = make_unique<Dog>("Paul", "Wuff");

Ausblick: Abstrakte Klassen

  • Die bisherigen Beispielklassen sind alle noch instanziierbar gewesen
  • Jetzt: Basisklassen als Schablonen
    • Können nicht mehr direkt instanziiert werden
    • Stattdessen müssen Kindklassen abgeleitet werden
class Form {
    virtual void get_area() = 0;
};

class Rechteck : public Form {/*...*/};
class Quadrat  : public Form {/*...*/};
class Kreis    : public Form {/*...*/};
// ...
  • Das Schlüsselwort virtual hilft hierbei → mehr im nächsten Kapitel

Zusammenfassung

  • Vererbung erlaubt akkurate Modellierung der Realität
  • Initialisierungsreihenfolge der Konstruktoren
  • Sichtbarkeitsmodifikatoren entscheiden über Weitergabe von Informationen
    • public → für alle zugreifbar
    • protected → nur für die Klasse und Unterklassen
    • private → nur für die Klasse selbst
Logo Sys
Kapitel 14 - Vererbung