Polymorphie zur Compilezeit vs. Laufzeit
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, obwohl sie sich unterschiedlich verhalten. C++ unterstützt zwei verschiedene Arten von Polymorphismus, die jeweils in einer anderen Phase der Programmausführung aufgelöst werden.
Kompilierzeit-Polymorphie (auch statische Polymorphie genannt) wird vom Compiler aufgelöst, bevor das Programm ausgeführt wird. Der Compiler bestimmt anhand der Funktionssignatur genau, welche Funktion aufgerufen werden soll. 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)Laufzeitpolymorphismus (auch dynamischer Polymorphismus genannt) wird während der Ausführung des Programms aufgelöst. 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: Polymorphie zur Kompilierzeit verursacht keinen Laufzeitaufwand, da Entscheidungen während der Kompilierung getroffen werden, während Polymorphie zur Laufzeit geringe Kosten (Nachschlagen in der vtable) verursacht, aber größere Flexibilität beim Arbeiten mit Objekten bietet, deren Typen erst bei der Ausführung bekannt sind.
Aufgabe
EinfachErstellen wir ein Rechnersystem, das beide Arten von Polymorphismus direkt nebeneinander demonstriert. Du erstellst ein System, in dem der Polymorphismus zur Compile-Zeit verschiedene Eingabetypen durch Funktionsüberladung verarbeitet, während der Polymorphismus zur Laufzeit ermöglicht, verschiedene Berechnungsstrategien dynamisch auszutauschen.
Du organisierst deinen Code auf drei Dateien:
Calculator.h: Definiere eine BasisklasseCalculator, die jede Berechnungsstrategie repräsentiert:- Eine virtuelle Methode
calculate(int a, int b), die einintzurückgibt und Folgendes ausgibt:Base calculation: <a> ? <b>(wobei sie 0 zurückgibt) - Einen virtuellen Destruktor
- Eine virtuelle Methode
Operations.h: Definiere zwei abgeleitete Rechnerklassen, die das Berechnungsverhalten überschreiben:Adder: Überschreibecalculate(), sodassAdding: <a> + <b>ausgegeben und die Summe zurückgegeben wirdMultiplier: Überschreibecalculate(), sodassMultiplying: <a> * <b>ausgegeben und das Produkt zurückgegeben wird
overrideverwenden.main.cpp: Erstelle ein System, das beide Polymorphismus-Arten vorführt. Lies zwei ganzzahlige Eingaben ein (jeweils in einer eigenen Zeile).Demonstriere zuerst den Polymorphismus zur Compile-Zeit, indem du drei überladene
display()-Funktionen erstellst:display(int x)gibt Folgendes aus:Integer value: <x>display(double x)gibt Folgendes aus:Double value: <x>display(const std::string& x)gibt Folgendes aus:String value: <x>
Demonstriere anschließend den Polymorphismus zur Laufzeit, indem du ein Array aus
Calculator*-Zeigern erstellst, das ein BasisobjektCalculator, einAdder-Objekt und einMultiplier-Objekt enthält. Durchlaufe das Array und rufe für jedes Element mit deinen Eingabewertencalculate()auf. Gib nach jeder Berechnung das Ergebnis aus.Strukturiere deine Ausgabe wie folgt:
=== Compile-Time Polymorphism === <display outputs for int, double, string> === Runtime Polymorphism === <calculate outputs with results>Rufe im Abschnitt zur Compile-Zeit
display()zuerst mit der ersten Eingabe als Ganzzahl auf, dann als Double (derselbe Wert mit hinzugefügtem .5) und anschließend als String "Result". Räume deine dynamisch angelegten Rechner am Ende auf.
Beispielsweise 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: 30Beachte, wie der Compiler die richtige display()-Überladung anhand des Argumenttyps auswählt (Entscheidung zur Compile-Zeit), während die richtige calculate()-Methode zur Laufzeit anhand des tatsächlichen Objekttyps über den VTable-Mechanismus bestimmt wird.
Probier es selbst
#include <iostream>
#include <string>
#include "Calculator.h"
#include "Operations.h"
// TODO: Erstelle drei überladene display()-Funktionen:
// 1. display(int x) - prints "Integer value: <x>"
// 2. display(double x) - gibt "Double value: <x>" aus
// 3. display(const std::string& x) - gibt "String value: <x>" aus
int main() {
// Lese zwei Integer-Eingaben
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)
// - den String "Result"
std::cout << std::endl;
// === Runtime Polymorphism ===
std::cout << "=== Runtime Polymorphism ===" << std::endl;
// TODO: Erstelle ein Array von Calculator*-Zeigern mit 3 Elementen:
// - a base Calculator
// - einen Adder
// - einen Multiplier
// TODO: Durchlaufe das Array, rufe calculate(a, b) für jedes auf,
// und gib "Result: <return_value>" nach jeder Berechnung aus
// TODO: Dynamisch allokierten Speicher freigeben
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 & QuelldateienNamespaces & GültigkeitsbereichEinführung in OOP mit C++Klassen vs. ObjekteDer „this“-ZeigerMethoden (Member-Funktionen)Attribute (Datenmember)Grundlagen zu Ctors & DtorsRückblick – Einfacher Taschenrechner4Klassenattribute
Instanz- vs. statische MemberGetter und SetterConst-MemberfunktionenMutable-SchlüsselwortStatische Methoden und VariablenFriend-Funktionen und -KlassenZusammenfassung – Bankkontoverwaltung7Vererbung
Grundlagen der VererbungZugriffsebenen bei der VererbungAufrufreihenfolge von Ctor & DtorÜberschreiben von MethodenVirtuelle Funktionen & VTableMehrfachvererbungVirtuelle VererbungRückblick – Mitarbeiterhierarchie2Speicherverwaltung
Stack- vs. Heap-SpeicherZeiger und ReferenzenDynamischer Speicher (new/delete)Intelligente Zeiger in C++RAII in C++Rückblick – Dynamischer Array-Manager5Kapselung
Zugriffsbezeichner in C++Zugriffsbezeichner im DetailInformationsverbergungStruct vs. KlasseVerschachtelte und innere KlassenWiederholung – Studierendenverwaltungssystem8Polymorphie
Polymorphie zur Compilezeit vs. LaufzeitFunktionsüberladungVirtuelle Funktionen erneut betrachtetRein virtuelle FunktionenAbstrakte KlassenInterface-Design in C++Dynamic Casting & RTTIRückblick – Shape-Calculator3Konstruktoren & Destruktoren
StandardkonstruktorParametrisierter KonstruktorKopierkonstruktorMove-KonstruktorKonstruktor-InitialisierungslistenDelegierende KonstruktorenDestruktor im DetailDrei-/Fünf-/Null-RegelZusammenfassung – String-Klasse6Operatorüberladung
Einführung in die OperatorüberladungArithmetische OperatorüberladungVergleichsoperatorüberladungStream-OperatorenZuweisungsoperatorüberladungÜberladung der Operatoren [] und ()TypkonvertierungsoperatorenWiederholung – Matrix-Klasse9Templates
FunktionstemplatesKlassen-TemplatesTemplate-SpezialisierungVariadische TemplatesGrundlagen zu SFINAE und Type TraitsRückblick – Generischer ContainerÜbe selbstständig: Online-C++-Compiler