Polimorfizm czasu kompilacji i wykonania
Część sekcji Programowanie obiektowe ścieżki C++ w Coddy. Lekcja 56 z 104.
Polimorfizm oznacza „wiele form” i jest podstawową koncepcją programowania obiektowego, która pozwala traktować obiekty w jednolity sposób, mimo że zachowują się różnie. C++ obsługuje dwa odrębne typy polimorfizmu, z których każdy jest rozstrzygany na innym etapie wykonywania programu.
Polimorfizm czasu kompilacji (nazywany również polimorfizmem statycznym) jest rozstrzygany przez kompilator przed uruchomieniem programu. Kompilator dokładnie określa, którą funkcję wywołać na podstawie sygnatury funkcji. Obejmuje to przeciążanie funkcji i szablony:
void print(int x) { std::cout << "Integer: " << x << std::endl; }
void print(double x) { std::cout << "Double: " << x << std::endl; }
print(5); // Kompilator wybiera print(int)
print(3.14); // Kompilator wybiera print(double)Polimorfizm czasu wykonania (zwany także polimorfizmem dynamicznym) jest rozstrzygany podczas działania programu. Decyzja o tym, którą funkcję wywołać, zależy od rzeczywistego typu obiektu, a nie typu wskaźnika lub referencji. Osiąga się to za pomocą funkcji wirtualnych:
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(); // Ustalane w czasie wykonywania: "Drawing circle"Kluczowy kompromis: polimorfizm statyczny nie powoduje żadnego narzutu w czasie działania, ponieważ decyzje są podejmowane podczas kompilacji, natomiast polimorfizm dynamiczny wiąże się z niewielkim kosztem (wyszukiwaniem w tabeli metod wirtualnych), ale zapewnia większą elastyczność podczas pracy z obiektami, których typy nie są znane aż do momentu wykonania programu.
Wyzwanie
ŁatwyZbudujmy system kalkulatora, który pokazuje obok siebie oba rodzaje polimorfizmu. Utworzysz system, w którym polimorfizm czasu kompilacji obsługuje różne typy danych wejściowych za pomocą przeciążania funkcji, a polimorfizm czasu wykonania pozwala dynamicznie wymieniać różne strategie obliczeń.
Rozmieścisz kod w trzech plikach:
Calculator.h: Zdefiniuj bazową klasęCalculator, która reprezentuje dowolną strategię obliczeń:- Wirtualna metoda
calculate(int a, int b), która zwraca wartośćinti wypisuje:Base calculation: <a> ? <b>(zwracając 0) - Wirtualny destruktor
- Wirtualna metoda
Operations.h: Zdefiniuj dwie klasy kalkulatorów pochodne, które nadpisują sposób wykonywania obliczeń:Adder: Nadpisz metodęcalculate(), aby wypisywałaAdding: <a> + <b>i zwracała sumęMultiplier: Nadpisz metodęcalculate(), aby wypisywałaMultiplying: <a> * <b>i zwracała iloczyn
override.main.cpp: Utwórz system, który pokazuje oba rodzaje polimorfizmu. Wczytaj dwie liczby całkowite (każdą w osobnym wierszu).Najpierw pokaż polimorfizm czasu kompilacji, tworząc trzy przeciążone funkcje
display():display(int x)wypisuje:Integer value: <x>display(double x)wypisuje:Double value: <x>display(const std::string& x)wypisuje:String value: <x>
Następnie pokaż polimorfizm czasu wykonania, tworząc tablicę wskaźników
Calculator*, zawierającą obiekt bazowyCalculator, obiektAdderi obiektMultiplier. Przejdź przez tablicę w pętli i wywołajcalculate()dla każdego obiektu, używając wczytanych wartości, a po każdym obliczeniu wypisz wynik.Ułóż dane wyjściowe w następujący sposób:
=== Compile-Time Polymorphism === <display outputs for int, double, string> === Runtime Polymorphism === <calculate outputs with results>W sekcji dotyczącej czasu kompilacji wywołaj
display()najpierw z pierwszą wartością jako liczbą całkowitą, następnie jako liczbą typu double (ta sama wartość z dodanym .5), a na końcu z ciągiem znaków "Result". Po zakończeniu zwolnij pamięć zajmowaną przez dynamicznie utworzone obiekty kalkulatorów.
Na przykład dla danych wejściowych 10 i 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: 30Zwróć uwagę, jak kompilator wybiera właściwe przeciążenie display() na podstawie typu argumentu (decyzja podejmowana w czasie kompilacji), podczas gdy właściwa metoda calculate() jest określana w czasie wykonania na podstawie rzeczywistego typu obiektu, za pomocą mechanizmu vtable.
Spróbuj swoich sił
#include <iostream>
#include <string>
#include "Calculator.h"
#include "Operations.h"
// TODO: Utwórz trzy przeciążone funkcje display():
// 1. display(int x) - wypisuje "Integer value: <x>"
// 2. display(double x) - wypisuje "Double value: <x>"
// 3. display(const std::string& x) - wypisuje "String value: <x>"
int main() {
// Wczytaj dwie liczby całkowite
int a, b;
std::cin >> a;
std::cin >> b;
// === Compile-Time Polymorphism ===
std::cout << "=== Compile-Time Polymorphism ===" << std::endl;
// TODO: Wywołaj display() z następującymi argumentami:
// - a jako liczba całkowita
// - a jako wartość typu double (dodaj do niej 0.5)
// - napis "Result"
std::cout << std::endl;
// === Runtime Polymorphism ===
std::cout << "=== Runtime Polymorphism ===" << std::endl;
// TODO: Utwórz tablicę wskaźników Calculator* zawierającą 3 elementy:
// - obiekt klasy bazowej Calculator
// - obiekt klasy Adder
// - obiekt Multiplier
// TODO: Przejdź przez tablicę, wywołując calculate(a, b) dla każdego elementu,
// i wypisz "Result: <return_value>" po każdym obliczeniu
// TODO: Zwolnij dynamicznie przydzieloną pamięć
return 0;
}
Ta lekcja zawiera krótki quiz. Zacznij lekcję, żeby na niego odpowiedzieć i śledzić swoje postępy.
Wszystkie lekcje w sekcji Programowanie obiektowe
1Podstawy programowania obiektowego
Pliki zewnętrzneBudowanie i kompilacja C++Pliki nagłówkowe i pliki źródłowePrzestrzenie nazw i zakresWprowadzenie do programowania obiektowego w C++Klasy a obiektyWskaźnik „this”Metody (funkcje składowe)Atrybuty (składowe danych)Podstawy konstruktorów i destruktorówPowtórzenie – prosty kalkulator4Właściwości klas
Elementy instancji a elementy statyczneGettery i setteryStałe funkcje składoweSłowo kluczowe mutableMetody i zmienne statyczneFunkcje i klasy zaprzyjaźnionePodsumowanie – menedżer konta bankowego7Dziedziczenie
Podstawy dziedziczeniaPoziomy dostępu w dziedziczeniuKolejność wywołań konstruktora i destruktoraNadpisywanie metodFunkcje wirtualne i VTableWielokrotne dziedziczenieDziedziczenie wirtualnePowtórzenie — hierarchia pracowników2Zarządzanie pamięcią
Pamięć stosu a stertyWskaźniki i referencjePamięć dynamiczna (new/delete)Inteligentne wskaźniki w C++RAII w C++Podsumowanie — menedżer tablicy dynamicznej5Hermetyzacja
Specyfikatory dostępu w C++Specyfikatory dostępu — szczegółowoUkrywanie informacjiStruktura a klasaKlasy zagnieżdżone i wewnętrznePodsumowanie — system ewidencji studentów8Polimorfizm
Polimorfizm czasu kompilacji i wykonaniaPrzeciążanie funkcjiFunkcje wirtualne — powtórkaCzysto wirtualne funkcjeKlasy abstrakcyjneProjektowanie interfejsów w C++Rzutowanie dynamiczne i RTTIPodsumowanie — kalkulator kształtów3Konstruktory i destruktory
Konstruktor domyślnyKonstruktor z parametramiKonstruktor kopiującyKonstruktor przenoszącyListy inicjalizacyjne konstruktoraKonstruktory delegująceDestruktor — szczegółowe omówienieZasada trzech / pięciu / zeraPodsumowanie — klasa String6Przeciążanie operatorów
Wprowadzenie do przeciążania operatorówPrzeciążanie operatorów arytmetycznychPrzeciążanie operatorów porównaniaOperatory strumieniowePrzeciążanie operatora przypisaniaPrzeciążanie operatorów [] i ()Operatory konwersji typówPodsumowanie — klasa Matrix9Szablony
Szablony funkcjiSzablony klasSpecjalizacja szablonówSzablony wariadycznePodstawy SFINAE i cech typówPodsumowanie — kontener generycznyPoćwicz samodzielnie: Kompilator C++ online