Kompilierzeit- vs. Laufzeit-Polymorphie
Teil des Abschnitts Objektorientierte Programmierung der C++-Journey von Coddy — Lektion 56 von 104.
Polymorphismus bedeutet „viele Formen“ und ist ein zentrales OOP-Konzept, das es ermöglicht, Objekte einheitlich zu behandeln, während sie sich unterschiedlich verhalten. C++ unterstützt zwei verschiedene Arten von Polymorphismus, die jeweils zu einem unterschiedlichen Zeitpunkt der Programmausführung aufgelöst werden.
Compile-Time-Polymorphismus (auch statischer Polymorphismus genannt) wird vom Compiler aufgelöst, bevor das Programm ausgeführt wird. Der Compiler bestimmt anhand der Funktionssignatur genau, welche Funktion aufgerufen werden soll. Dies umfasst Funktionsüberladung und Templates:
void print(int x) { std::cout << "Integer: " << x << std::endl; }
void print(double x) { std::cout << "Double: " << x << std::endl; }
print(5); // Compiler wählt print(int)
print(3.14); // Compiler wählt print(double)Laufzeitpolymorphismus (auch dynamische Polymorphie genannt) wird aufgelöst, während das Programm ausgeführt wird. Die Entscheidung darüber, welche Funktion aufgerufen werden soll, hängt vom tatsächlichen Objekttyp ab, nicht vom Zeiger- oder Referenztyp. Dies wird durch virtuelle Funktionen erreicht:
class Shape {
public:
virtual void draw() { std::cout << "Drawing shape" << std::endl; }
};
class Circle : public Shape {
public:
void draw() override { std::cout << "Drawing circle" << std::endl; }
};
Shape* s = new Circle();
s->draw(); // Zur Laufzeit entschieden: "Drawing circle"Der entscheidende Kompromiss: Compile-Zeit-Polymorphismus hat keinen Laufzeit-Overhead, da Entscheidungen während der Kompilierung getroffen werden, während Laufzeit-Polymorphismus geringe Kosten verursacht (vtable-Lookup), aber eine größere Flexibilität für die Arbeit mit Objekten bietet, deren Typen erst zur Ausführungszeit bekannt sind.
Aufgabe
EinfachLassen Sie uns ein Taschenrechner-System bauen, das beide Arten von Polymorphismus nebeneinander demonstriert. Sie erstellen ein System, in dem Compile-Time-Polymorphismus verschiedene Eingabetypen durch Funktionsüberladung verarbeitet, während Runtime-Polymorphismus es ermöglicht, verschiedene Berechnungsstrategien dynamisch auszutauschen.
Sie organisieren Ihren Code über drei Dateien:
Calculator.h: Definieren Sie eine BasisklasseCalculator, die jede Berechnungsstrategie repräsentiert:- Eine virtuelle Methode
calculate(int a, int b), die einenintzurückgibt und Folgendes ausgibt:"Base calculation: "<a>" ? "<b> (gibt 0 zurück) - Ein virtueller Destruktor
- Eine virtuelle Methode
Operations.h: Definieren Sie zwei abgeleitete Taschenrechner-Klassen, die das Berechnungsverhalten überschreiben:Adder: Überschreiben Siecalculate(), um"Adding: "<a>" + "<b> auszugeben und die Summe zurückzugebenMultiplier: Überschreiben Siecalculate(), um"Multiplying: "<a>" * "<b> auszugeben und das Produkt zurückzugeben
overrideverwenden.main.cpp: Erstellen Sie ein System, das beide Polymorphismus-Typen zeigt. Lesen Sie zwei Ganzzahl-Eingaben ein (jede in einer separaten Zeile).Demonstrieren Sie zuerst Compile-Time-Polymorphismus, indem Sie drei überladene
display()-Funktionen erstellen:display(int x)gibt aus:"Integer value: "<x>display(double x)gibt aus:"Double value: "<x>display(const std::string& x)gibt aus:"String value: "<x>
Demonstrieren Sie dann Runtime-Polymorphismus, indem Sie ein Array von
Calculator*-Poitern erstellen, das einen Basis-Calculator, einenAdderund einenMultiplierenthält. Durchlaufen Sie das Array in einer Schleife und rufen Sie für jedencalculate()mit Ihren Eingabewerten auf, wobei Sie das Ergebnis nach jeder Berechnung ausgeben.Strukturieren Sie Ihre Ausgabe wie folgt:
=== Compile-Time Polymorphism === <display outputs for int, double, string> === Runtime Polymorphism === <calculate outputs with results>Rufen Sie für den Compile-Time-Abschnitt
display()mit der ersten Eingabe als Integer auf, dann als Double (derselbe Wert mit .5 addiert) und dann als den String "Result". Bereinigen Sie Ihre dynamisch zugewiesenen Taschenrechner, wenn Sie fertig sind.
Zum Beispiel bei den Eingaben 10 und 3:
=== Compile-Time Polymorphism ===
Integer value: 10
Double value: 10.5
String value: Result
=== Runtime Polymorphism ===
Base calculation: 10 ? 3
Result: 0
Adding: 10 + 3
Result: 13
Multiplying: 10 * 3
Result: 30Beachten Sie, wie der Compiler die korrekte display()-Überladung basierend auf dem Argumenttyp auswählt (Entscheidung zur Compile-Zeit), während die korrekte calculate()-Methode durch den tatsächlichen Objekttyp zur Laufzeit über den vtable-Mechanismus bestimmt wird.
Spickzettel
C++ unterstützt zwei Arten von Polymorphismus, die zu unterschiedlichen Zeitpunkten aufgelöst werden:
Kompilierzeit-Polymorphismus (statischer Polymorphismus) wird vom Compiler vor der Ausführung aufgelöst. Dazu gehören Funktionsüberladung und Templates:
void print(int x) { std::cout << "Integer: " << x << std::endl; }
void print(double x) { std::cout << "Double: " << x << std::endl; }
print(5); // Compiler wählt print(int)
print(3.14); // Compiler wählt print(double)Laufzeit-Polymorphismus (dynamischer Polymorphismus) wird während der Ausführung mithilfe von virtuellen Funktionen aufgelöst. Der tatsächliche Objekttyp bestimmt, welche Funktion aufgerufen wird:
class Shape {
public:
virtual void draw() { std::cout << "Drawing shape" << std::endl; }
};
class Circle : public Shape {
public:
void draw() override { std::cout << "Drawing circle" << std::endl; }
};
Shape* s = new Circle();
s->draw(); // Zur Laufzeit entschieden: "Drawing circle"Abwägung: Kompilierzeit-Polymorphismus hat keinen Laufzeit-Overhead, während Laufzeit-Polymorphismus geringe Kosten verursacht (vtable-Lookup), aber eine größere Flexibilität bietet.
Probier es selbst
#include <iostream>
#include <string>
#include "Calculator.h"
#include "Operations.h"
// TODO: Erstelle drei überladene display()-Funktionen:
// 1. display(int x) - gibt "Integer value: <x>" aus
// 2. display(double x) - gibt "Double value: <x>" aus
// 3. display(const std::string& x) - gibt "String value: <x>" aus
int main() {
// Lies zwei Integer-Eingaben ein
int a, b;
std::cin >> a;
std::cin >> b;
// === Compile-Time Polymorphism ===
std::cout << "=== Compile-Time Polymorphism ===" << std::endl;
// TODO: Rufe display() auf mit:
// - a als Integer
// - a als Double (addiere 0.5 dazu)
// - dem String "Result"
std::cout << std::endl;
// === Runtime Polymorphism ===
std::cout << "=== Runtime Polymorphism ===" << std::endl;
// TODO: Erstelle ein Array aus Calculator*-Pointern mit 3 Elementen:
// - ein Basis-Calculator
// - ein Adder
// - ein Multiplier
// TODO: Iteriere durch das Array, rufe calculate(a, b) für jedes Element auf,
// und gib nach jeder Berechnung "Result: <return_value>" aus
// TODO: Bereinige den dynamisch allokierten Speicher
return 0;
}
Diese Lektion enthält ein kurzes Quiz. Starte die Lektion, um es zu beantworten und deinen Fortschritt zu speichern.
Alle Lektionen in Objektorientierte Programmierung
1Grundlagen der OOP
Externe DateienC++ Build & KompilierungHeader-Dateien & QuelldateienNamensräume & GültigkeitsbereicheEinführung in OOP in C++Klassen vs. ObjekteDer 'this'-PointerMethoden (Elementfunktionen)Attribute (Datenelemente)Grundlagen zu Ctors & DtorsRückblick - Einfacher Taschenrechner4Klasseneigenschaften
Instanz- vs. statische MemberGetter und SetterConst-MemberfunktionenMutable-SchlüsselwortStatische Methoden und VariablenFriend-Funktionen & KlassenZusammenfassung - Bankkonto-Manager7Vererbung
Grundlagen der VererbungZugriffsstufen bei VererbungAufrufreihenfolge von Ctor & DtorÜberschreiben von MethodenVirtuelle Funktionen & VTableMehrfachvererbungVirtuelle VererbungRückblick - Mitarbeiter-Hierarchie2Speicherverwaltung
Stack vs. Heap SpeicherZeiger und ReferenzenDynamischer Speicher (new/delete)Smart Pointers in C++RAII in C++Rückblick – Dynamischer Array-Manager5Kapselung
Zugriffsspezifizierer in C++Zugriffsspezifizierer im DetailInformation HidingStruct vs. ClassVerschachtelte & innere KlassenRückblick - Studentenverwaltungssystem8Polymorphie
Kompilierzeit- vs. Laufzeit-PolymorphieFunktionsüberladungVirtuelle Funktionen – WiederholungRein virtuelle FunktionenAbstrakte KlassenInterface-Design in C++Dynamic Casting & RTTIZusammenfassung – Formen-Rechner3Konstruktoren & Destruktoren
StandardkonstruktorParametrisierter KonstruktorKopierkonstruktorVerschiebekonstruktorKonstruktor-InitialisierungslistenDelegierende KonstruktorenDestruktoren im DetailRule of Three / Five / ZeroZusammenfassung - String-Klasse6Operatorüberladung
Einführung in die OperatorüberladungArithmetische OperatorüberladungVergleichsoperatorüberladungStream-OperatorenZuweisungsoperatorüberladungÜberladen der [] und () OperatorenTypumwandlungsoperatorenRückblick - Matrix-Klasse9Templates
FunktionstemplatesKlassentemplatesTemplate-SpezialisierungVariadische TemplatesSFINAE & Type Traits GrundlagenRückblick - Generische Container