Dieser Artikel beschreibt die Vererbung auf drei Ebenen: Anfänger, Mittelstufe und Fortgeschrittene. Experte Nr. Und kein Wort über SOLID. Ehrlich gesagt.
Anfänger
Was ist Vererbung?
Vererbung ist eines der Grundprinzipien von OOP. Demnach kann eine Klasse die Variablen und Methoden einer anderen Klasse als ihre eigenen verwenden.
Eine Klasse, die Daten erbt, wird als Unterklasse, abgeleitete Klasse oder untergeordnete Klasse bezeichnet. Die Klasse, von der Daten oder Methoden geerbt werden, wird als Superklasse, Basisklasse oder übergeordnete Klasse bezeichnet. Die Begriffe "Eltern" und "Kind" sind äußerst nützlich für das Verständnis der Vererbung. Wenn ein Kind die Merkmale seiner Eltern erhält, erhält die abgeleitete Klasse die Methoden und Variablen der Basisklasse.
Vererbung ist nützlich, weil Sie damit Code strukturieren und wiederverwenden können, was wiederum der Fall ist kann den Entwicklungsprozess erheblich beschleunigen. Trotzdem sollte die Vererbung mit Vorsicht angewendet werden, da die meisten Änderungen an der Oberklasse alle Unterklassen betreffen, was zu unvorhergesehenen Konsequenzen führen kann.
In diesem Beispiel wurden die Methode turn_on()
und die Variable serial_number
in der Unterklasse Computer
nicht deklariert oder definiert. Sie können jedoch verwendet werden, da sie von der Basisklasse geerbt werden.
Wichtiger Hinweis : Private Variablen und Methoden können nicht vererbt werden.
#include <iostream> using namespace std; class Device { public: int serial_number = 12345678; void turn_on() { cout << "Device is on" << endl; } private: int pincode = 87654321; }; class Computer: public Device {}; int main() { Computer Computer_instance; Computer_instance.turn_on(); cout << "Serial number is: " << Computer_instance.serial_number << endl; // cout << "Pin code is: " << Computer_instance.pincode << endl; // will cause compile time error return 0; }
Arten der Vererbung
In C ++ gibt es verschiedene Arten der Vererbung:
- öffentlich - öffentliche (
public
) und geschützte ( protected
) Daten werden vererbt, ohne die Zugriffsebene zu ändern. - geschützt (
protected
) - alle geerbten Daten werden geschützt; - privat - Alle geerbten Daten werden privat.
Für die Device
ändert sich die Datenzugriffsebene nicht. Da die vom Computer
abgeleitete Klasse Daten als privat erbt, werden die Daten für die Computer
privat.
#include <iostream> using namespace std; class Device { public: int serial_number = 12345678; void turn_on() { cout << "Device is on" << endl; } }; class Computer: private Device { public: void say_hello() { turn_on(); cout << "Welcome to Windows 95!" << endl; } }; int main() { Device Device_instance; Computer Computer_instance; cout << "\t Device" << endl; cout << "Serial number is: "<< Device_instance.serial_number << endl; Device_instance.turn_on(); // cout << "Serial number is: " << Computer_instance.serial_number << endl; // Computer_instance.turn_on(); // will cause compile time error cout << "\t Computer" << endl; Computer_instance.say_hello(); return 0; }
Die Computer
Klasse verwendet jetzt die turn_on()
-Methode wie jede private Methode: turn_on()
kann innerhalb der Klasse aufgerufen werden. Der Versuch, sie direkt von main
aufzurufen, führt jedoch beim Kompilieren zu einem Fehler. Für die Basisklasse Device
ist die Methode turn_on()
öffentlich geblieben und kann von main
aus aufgerufen werden.
Konstruktoren und Destruktoren
In C ++ werden Konstruktoren und Destruktoren nicht vererbt. Sie werden jedoch aufgerufen, wenn die untergeordnete Klasse ihr Objekt initialisiert. Konstruktoren werden nacheinander hierarchisch aufgerufen, beginnend mit der Basisklasse und endend mit der zuletzt abgeleiteten Klasse. Destruktoren werden in umgekehrter Reihenfolge aufgerufen.
Wichtiger Hinweis: Dieser Artikel behandelt keine virtuellen Destruktoren. Zusätzliches Material zu diesem Thema finden Sie beispielsweise in diesem Artikel über die Habr .
#include <iostream> using namespace std; class Device { public: // constructor Device() { cout << "Device constructor called" << endl; } // destructor ~Device() { cout << "Device destructor called" << endl; } }; class Computer: public Device { public: Computer() { cout << "Computer constructor called" << endl; } ~Computer() { cout << "Computer destructor called" << endl; } }; class Laptop: public Computer { public: Laptop() { cout << "Laptop constructor called" << endl; } ~Laptop() { cout << "Laptop destructor called" << endl; } }; int main() { cout << "\tConstructors" << endl; Laptop Laptop_instance; cout << "\tDestructors" << endl; return 0; }
Konstruktoren: Device
-> Computer
-> Laptop
.
Zerstörer: Laptop
-> Computer
-> Device
.
Mehrfachvererbung
Mehrfachvererbung tritt auf, wenn eine Unterklasse zwei oder mehr Oberklassen hat. In diesem Beispiel erbt die Laptop
Klasse gleichzeitig Monitor
und Computer
.
#include <iostream> using namespace std; class Computer { public: void turn_on() { cout << "Welcome to Windows 95" << endl; } }; class Monitor { public: void show_image() { cout << "Imagine image here" << endl; } }; class Laptop: public Computer, public Monitor {}; int main() { Laptop Laptop_instance; Laptop_instance.turn_on(); Laptop_instance.show_image(); return 0; }
Probleme mit Mehrfachvererbung
Mehrfachvererbung erfordert sorgfältiges Design, da dies zu unvorhergesehenen Konsequenzen führen kann. Die meisten dieser Konsequenzen werden durch Mehrdeutigkeiten bei der Vererbung verursacht. In diesem Beispiel erbt Laptop
die Methode turn_on()
von beiden Elternteilen, und es ist nicht klar, welche Methode aufgerufen werden soll.
#include <iostream> using namespace std; class Computer { private: void turn_on() { cout << "Computer is on." << endl; } }; class Monitor { public: void turn_on() { cout << "Monitor is on." << endl; } }; class Laptop: public Computer, public Monitor {}; int main() { Laptop Laptop_instance; // Laptop_instance.turn_on(); // will cause compile time error return 0; }
Trotz der Tatsache, dass private Daten nicht vererbt werden, ist es unmöglich, eine mehrdeutige Vererbung aufzulösen, indem die Zugriffsebene auf private Daten geändert wird. Beim Kompilieren wird zunächst nach einer Methode oder Variablen gesucht und anschließend die Zugriffsebene überprüft.
Rautenproblem

Das Diamantproblem ist ein klassisches Problem in Sprachen, die Mehrfachvererbung unterstützen. Dieses Problem tritt auf, wenn die Klassen B
und C
A
und die Klasse D
B
und C
erben C
Beispielsweise definieren die Klassen A
, B
und C
die Methode print_letter()
. Wenn print_letter()
von Klasse D
aufgerufen wird, ist nicht klar, welche Methode aufgerufen werden soll - eine Methode der Klassen A
, B
oder C
Verschiedene Sprachen haben unterschiedliche Ansätze zur Lösung rautenförmiger Probleme. In C ++ liegt die Lösung des Problems im Ermessen des Programmierers.
Das rautenförmige Problem ist in erster Linie ein Entwurfsproblem und sollte in der Entwurfsphase bereitgestellt werden. In der Entwicklungsphase kann dies wie folgt gelöst werden:
- Rufen Sie die Methode einer bestimmten Oberklasse auf.
- das Objekt der Unterklasse als Objekt einer bestimmten Oberklasse bezeichnen;
- Überschreiben Sie die problematische Methode in der letzten
turn_on()
Klasse (im Code turn_on()
in der Laptop
Unterklasse).
#include <iostream> using namespace std; class Device { public: void turn_on() { cout << "Device is on." << endl; } }; class Computer: public Device {}; class Monitor: public Device {}; class Laptop: public Computer, public Monitor { /* public: void turn_on() { cout << "Laptop is on." << endl; } // uncommenting this function will resolve diamond problem */ }; int main() { Laptop Laptop_instance; // Laptop_instance.turn_on(); // will produce compile time error // if Laptop.turn_on function is commented out // calling method of specific superclass Laptop_instance.Monitor::turn_on(); // treating Laptop instance as Monitor instance via static cast static_cast<Monitor&>( Laptop_instance ).turn_on(); return 0; }
Wenn die Methode turn_on()
in Laptop nicht überschrieben wurde, führt der Aufruf von Laptop_instance.turn_on()
zu einem Kompilierungsfehler. Ein Laptop
Objekt kann gleichzeitig auf zwei turn_on()
zugreifen: Device:Computer:Laptop.turn_on()
und Device:Monitor:Laptop.turn_on()
.
Das Diamantproblem: Konstruktoren und Destruktoren
Da in C ++ beim Initialisieren des Objekts der untergeordneten Klasse die Konstruktoren aller übergeordneten Klassen aufgerufen werden, tritt ein weiteres Problem auf: Der Konstruktor der Basisklasse Device
wird zweimal aufgerufen.
#include <iostream> using namespace std; class Device { public: Device() { cout << "Device constructor called" << endl; } }; class Computer: public Device { public: Computer() { cout << "Computer constructor called" << endl; } }; class Monitor: public Device { public: Monitor() { cout << "Monitor constructor called" << endl; } }; class Laptop: public Computer, public Monitor {}; int main() { Laptop Laptop_instance; return 0; }
Virtuelle Vererbung
Die virtuelle Vererbung verhindert, dass mehrere Basisklassenobjekte in der Vererbungshierarchie angezeigt werden. Daher wird der Konstruktor der Basisklasse Device
nur einmal aufgerufen, und der Zugriff auf die Methode turn_on()
, ohne sie in der turn_on()
Klasse zu überschreiben, führt nicht zu einem Kompilierungsfehler.
#include <iostream> using namespace std; class Device { public: Device() { cout << "Device constructor called" << endl; } void turn_on() { cout << "Device is on." << endl; } }; class Computer: virtual public Device { public: Computer() { cout << "Computer constructor called" << endl; } }; class Monitor: virtual public Device { public: Monitor() { cout << "Monitor constructor called" << endl; } }; class Laptop: public Computer, public Monitor {}; int main() { Laptop Laptop_instance; Laptop_instance.turn_on(); return 0; }
Hinweis : Die virtuelle Vererbung in den Klassen Computer
und Monitor
keine rhomboide Vererbung zu, wenn die untergeordnete Klasse Laptop
die Laptop
nicht virtuell erbt ( class Laptop: public Computer, public Monitor, public Device {};
).
Abstrakte Klasse
In C ++ wird eine Klasse, in der mindestens eine reine virtuelle Methode vorhanden ist, als abstrakt betrachtet. Wenn die virtuelle Methode in der untergeordneten Klasse nicht überschrieben wird, wird der Code nicht kompiliert. In C ++ ist es außerdem unmöglich, ein Objekt einer abstrakten Klasse zu erstellen. Ein Versuch führt auch zu einem Kompilierungsfehler.
#include <iostream> using namespace std; class Device { public: void turn_on() { cout << "Device is on." << endl; } virtual void say_hello() = 0; }; class Laptop: public Device { public: void say_hello() { cout << "Hello world!" << endl; } }; int main() { Laptop Laptop_instance; Laptop_instance.turn_on(); Laptop_instance.say_hello(); // Device Device_instance; // will cause compile time error return 0; }
Schnittstelle
Im Gegensatz zu einigen OOP-Sprachen bietet C ++ kein separates Schlüsselwort für eine Schnittstelle. Die Implementierung der Schnittstelle ist jedoch möglich, indem eine reine abstrakte Klasse erstellt wird - eine Klasse, in der nur Methodendeklarationen vorhanden sind. Solche Klassen werden oft auch als Abstract Base Class (ABC) bezeichnet.
#include <iostream> using namespace std; class Device { public: virtual void turn_on() = 0; }; class Laptop: public Device { public: void turn_on() { cout << "Device is on." << endl; } }; int main() { Laptop Laptop_instance; Laptop_instance.turn_on(); // Device Device_instance; // will cause compile time error return 0; }
Erweitert
Obwohl Vererbung ein Grundprinzip von OOP ist, sollte sie mit Vorsicht angewendet werden. Es ist wichtig zu glauben, dass jeder Code, der verwendet wird, wahrscheinlich geändert wird und auf eine Weise verwendet werden kann, die für den Entwickler nicht offensichtlich ist.
Vererbung von einer implementierten oder teilweise implementierten Klasse
Wenn die Vererbung nicht von einer Schnittstelle (einer reinen abstrakten Klasse im Kontext von C ++) stammt, sondern von einer Klasse, in der Implementierungen vorhanden sind, sollte berücksichtigt werden, dass der Erbe über die engstmögliche Verbindung mit der übergeordneten Klasse verbunden ist. Die meisten Änderungen an der Elternklasse können sich auf den Erben auswirken, was zu unerwartetem Verhalten führen kann. Solche Änderungen im Verhalten des Erben sind nicht immer offensichtlich - ein Fehler kann im bereits getesteten und funktionierenden Code auftreten. Diese Situation wird durch das Vorhandensein einer komplexen Klassenhierarchie verschärft. Es ist immer daran zu erinnern, dass der Code nicht nur von der Person geändert werden kann, die ihn geschrieben hat, und dass die für den Autor offensichtlichen Vererbungspfade von seinen Kollegen möglicherweise nicht berücksichtigt werden.
Im Gegensatz dazu ist anzumerken, dass die Vererbung von teilweise implementierten Klassen einen unbestreitbaren Vorteil hat. Bibliotheken und Frameworks funktionieren häufig wie folgt: Sie bieten dem Benutzer eine abstrakte Klasse mit mehreren virtuellen und vielen implementierten Methoden. Somit wurde bereits der größte Teil der Arbeit erledigt - die komplexe Logik wurde bereits geschrieben, und der Benutzer kann die vorgefertigte Lösung nur an seine Bedürfnisse anpassen.
Schnittstelle
Die Vererbung von einer Schnittstelle (reine abstrakte Klasse) bietet die Vererbung als Gelegenheit, den Code zu strukturieren und den Benutzer zu schützen. Da die Schnittstelle beschreibt, welche Arbeit die Implementierungsklasse ausführen wird, jedoch nicht, wie, wird jeder Benutzer der Schnittstelle vor Änderungen in der Klasse geschützt, die diese Schnittstelle implementiert.
Schnittstelle: Verwendungsbeispiel
Zunächst ist anzumerken, dass das Beispiel eng mit dem Konzept des Polymorphismus verwandt ist, aber im Zusammenhang mit der Vererbung von einer rein abstrakten Klasse betrachtet wird.
Eine Anwendung, die abstrakte Geschäftslogik ausführt, muss aus einer separaten Konfigurationsdatei konfiguriert werden. In einem frühen Stadium der Entwicklung wurde die Formatierung dieser Konfigurationsdatei nicht vollständig erstellt. Das Übergeben der Dateianalyse hinter einer Schnittstelle bietet mehrere Vorteile.
Die mangelnde Klarheit hinsichtlich der Formatierung der Konfigurationsdatei verlangsamt den Entwicklungsprozess des Hauptprogramms nicht. Zwei Entwickler können parallel arbeiten - einer für die Geschäftslogik und der andere für den Parser. Da sie über diese Schnittstelle interagieren, kann jeder von ihnen unabhängig arbeiten. Dieser Ansatz erleichtert das Codieren von Komponententests mit Code, da die erforderlichen Tests mit mock für diese Schnittstelle geschrieben werden können.
Auch beim Ändern des Konfigurationsdateiformats wird die Geschäftslogik der Anwendung nicht beeinflusst. Das einzige, was einen vollständigen Übergang von einer Formatierung zur anderen erfordert, ist das Schreiben einer neuen Implementierung der bereits vorhandenen abstrakten Klasse (Parser-Klasse). Die Rückkehr zum ursprünglichen Dateiformat erfordert nur minimalen Aufwand - das Ersetzen eines vorhandenen Parsers durch einen anderen.
Fazit
Vererbung bietet viele Vorteile, muss jedoch sorgfältig entworfen werden, um die Probleme zu vermeiden, für die sich eine Chance bietet. Im Zusammenhang mit der Vererbung bietet C ++ eine breite Palette von Tools, die dem Programmierer eine Vielzahl von Möglichkeiten eröffnen.
Und SOLID ist gut.