Temat: C++: Dziedziczenie i polimorfizm
Dziedziczenie umożliwia tworzenie nowych klas bazując na istniejących klasach, co ułatwia ponowne wykorzystanie kodu i organizację złożonych programów. Z kolei polimorfizm pozwala na wykorzystanie jednego interfejsu do różnych form funkcjonalności, co zwiększa elastyczność i skalowalność kodu.
W kontekście C++, dziedziczenie i polimorfizm są ze sobą ściśle powiązane. Dziedziczenie pozwala na tworzenie hierarchii klas, gdzie klasy pochodne dziedziczą atrybuty i zachowania po klasach bazowych, natomiast polimorfizm umożliwia wykorzystanie tych klas w sposób, który nie jest z góry określony przez ich hierarchię.
Przyjrzyjmy się bliżej tym dwóm koncepcjom, ich znaczeniu w projektowaniu oprogramowania i sposobom, w jakie współpracują one ze sobą, aby zwiększyć możliwości i efektywność kodu napisanego w języku C++.
Rodzaje dziedziczenia
W C++ istnieją różne rodzaje dziedziczenia, które definiują, jak klasy pochodne mogą korzystać z członków klasy bazowej:
- Publiczne: Gdy klasa pochodna dziedziczy publicznie, wszystkie publiczne i chronione składowe klasy bazowej zachowują swój dostęp w klasie pochodnej.
- Chronione (Protected): W tym przypadku, publiczne i chronione składowe klasy bazowej stają się chronione w klasie pochodnej.
- Prywatne: Tutaj, publiczne i chronione składowe klasy bazowej stają się prywatnymi składowymi klasy pochodnej.
Wielokrotne dziedziczenie
C++ oferuje również możliwość wielokrotnego dziedziczenia, co oznacza, że klasa może dziedziczyć funkcjonalność z więcej niż jednej klasy bazowej. Jest to potężne narzędzie, ale również może prowadzić do skomplikowanych zależności i problemów, takich jak diament problemu dziedziczenia, gdzie ta sama klasa bazowa jest dziedziczona przez różne ścieżki.
Konstruktory i destruktory w dziedziczeniu
Konstruktory i destruktory są wywoływane w określonej kolejności, zgodnie z hierarchią dziedziczenia. Konstruktory klas bazowych są zwykle wywoływane przed konstruktorami klas pochodnych, podczas gdy destruktory działają w odwrotnej kolejności, co zapewnia prawidłową inicjalizację i deinicjalizację obiektów.
Polimorfizm Statyczny i Dynamiczny
W C++ istnieją dwa główne rodzaje polimorfizmu: statyczny i dynamiczny. Polimorfizm statyczny jest osiągany głównie przez przeciążanie funkcji i szablonów. Pozwala to na używanie tej samej nazwy funkcji dla różnych kontekstów, w zależności od argumentów przekazanych do funkcji. Z kolei polimorfizm dynamiczny korzysta z wirtualnych funkcji i jest związany z dziedziczeniem. Umożliwia różne implementacje tej samej metody w klasach pochodnych, co jest decydowane w czasie wykonania programu, a nie jego kompilacji.
Wirtualne Funkcje i Klasy Abstrakcyjne
Kluczowym elementem polimorfizmu dynamicznego są wirtualne funkcje. Definiują one interfejs, który klasy pochodne mogą nadpisać, dostarczając własne implementacje. Klasy zawierające przynajmniej jedną wirtualną funkcję nazywane są klasami abstrakcyjnymi i służą jako baza dla polimorfizmu. Klasy abstrakcyjne często zawierają co najmniej jedną czysto wirtualną funkcję, która musi być zaimplementowana przez klasy pochodne.
Dynamiczne Rzutowanie Typów
Dynamiczne rzutowanie typów, czyli dynamic_cast, jest wykorzystywane do bezpiecznego rzutowania obiektów do ich właściwego typu w hierarchii dziedziczenia. Jest to szczególnie przydatne w kontekście polimorfizmu, gdy musimy zarządzać obiektami różnych klas pochodnych poprzez wskaźniki lub referencje do klasy bazowej. dynamic_cast sprawdza w czasie wykonania, czy rzutowanie jest legalne, co zapewnia bezpieczeństwo typów i pomaga uniknąć błędów wynikających z niewłaściwego rzutowania.
Przykład dziedziczenia w C++
#include <iostream>
using namespace std;
// Klasa bazowa
class Pojazd {
public:
Pojazd() { cout << "Tworzenie Pojazdu" << endl; }
virtual ~Pojazd() { cout << "Usuwanie Pojazdu" << endl; }
virtual void uruchom() { cout << "Pojazd uruchomiony" << endl; }
};
// Klasa pochodna
class Samochod : public Pojazd {
public:
Samochod() { cout << "Tworzenie Samochodu" << endl; }
~Samochod() { cout << "Usuwanie Samochodu" << endl; }
void uruchom() override { cout << "Samochod uruchomiony" << endl; }
};
int main() {
Pojazd *pojazd = new Samochod();
pojazd->uruchom();
delete pojazd;
return 0;
}
Analiza Kodu
#include <iostream>
using namespace std;
// Klasa bazowa
class Pojazd {
public:
Pojazd() { cout << "Tworzenie Pojazdu" << endl; }
virtual ~Pojazd() { cout << "Usuwanie Pojazdu" << endl; }
virtual void uruchom() { cout << "Pojazd uruchomiony" << endl; }
};
Klasa Bazowa Pojazd:
- Posiada konstruktor, który wyświetla komunikat przy tworzeniu obiektu.
- Zawiera wirtualny destruktor, co jest kluczowe w hierarchii dziedziczenia, zwłaszcza przy pracy z wskaźnikami do obiektów klas bazowych. Zapewnia to poprawne wywołanie destruktorów klas pochodnych.
- Metoda
uruchom()jest oznaczona jako wirtualna, co pozwala na jej nadpisanie w klasach pochodnych.
// Klasa pochodna
class Samochod : public Pojazd {
public:
Samochod() { cout << "Tworzenie Samochodu" << endl; }
~Samochod() { cout << "Usuwanie Samochodu" << endl; }
void uruchom() override { cout << "Samochod uruchomiony" << endl; }
};
Klasa Pochodna Samochod:
- Dziedziczy po klasie
Pojazd. - Konstruktor i destruktor wypisują komunikaty przy tworzeniu i usuwaniu obiektów klasy
Samochod. - Nadpisuje metodę
uruchom(), co jest sygnalizowane przez słowo kluczoweoverride.
int main() {
Pojazd *pojazd = new Samochod();
pojazd->uruchom();
delete pojazd;
return 0;
}
Funkcja main:
- Inicjuje wskaźnik typu
Pojazd, przypisując do niego nowo utworzony obiekt klasySamochod. - Jest to przykład polimorfizmu. Obiekt klasy
Samochodjest traktowany jakoPojazd, ale zachowuje swoje specyficzne cechy (w tym nadpisaną metodęuruchom()). - Wywoływana jest metoda
uruchom()klasySamochod, mimo że wskaźnik jest typuPojazd. Jest to możliwe dzięki wirtualnym metodom. - Używane jest słowo kluczowe
deletedo zwolnienia pamięci. Dzięki wirtualnemu destruktorowi w klasiePojazd, destruktorSamochodjest również poprawnie wywoływany.
Przykład polimorfizmu w C++
#include <iostream>
using namespace std;
// Klasa bazowa
class Figura {
public:
virtual void rysuj() = 0; // Czysta funkcja wirtualna
};
// Klasa pochodna
class Kwadrat : public Figura {
public:
void rysuj() override { cout << "Rysowanie Kwadratu." << endl; }
};
class Kolo : public Figura {
public:
void rysuj() override { cout << "Rysowanie Koła." << endl; }
};
int main() {
Figura* f1 = new Kwadrat();
Figura* f2 = new Kolo();
f1->rysuj();
f2->rysuj();
delete f1;
delete f2;
return 0;
}
Analiza Kodu
#include <iostream>
using namespace std;
// Klasa bazowa
class Figura {
public:
virtual void rysuj() = 0; // Czysta funkcja wirtualna
};
Klasa Bazowa Figura:
- Jest to klasa abstrakcyjna, ponieważ zawiera czystą funkcję wirtualną
rysuj(). Oznacza to, że nie można utworzyć bezpośrednio instancji tej klasy. - Słowo kluczowe
virtualwskazuje, że funkcjarysuj()może być nadpisana w klasie pochodnej.
// Klasa pochodna
class Kwadrat : public Figura {
public:
void rysuj() override { cout << "Rysowanie Kwadratu." << endl; }
};
class Kolo : public Figura {
public:
void rysuj() override { cout << "Rysowanie Koła." << endl; }
};
Klasy Pochodne Kwadrat i Kolo:
- Obydwie klasy dziedziczą po klasie
Figura. - Implementują one metodę
rysuj(), co jest wymagane, ponieważFigurajest klasą abstrakcyjną. - Słowo kluczowe
overridezapewnia, że metoda jest prawidłowo nadpisana z klasy bazowej.
int main() {
Figura* f1 = new Kwadrat();
Figura* f2 = new Kolo();
f1->rysuj();
f2->rysuj();
delete f1;
delete f2;
return 0;
}
Funkcja main:
- Tworzone są dwa wskaźniki typu
Figura,f1if2, ale inicjalizowane są jakoKwadratiKolo. - Jest to przykład polimorfizmu. Mimo że
f1if2są wskaźnikami doFigura, wywołują odpowiednie implementacje metodyrysuj()w zależności od rzeczywistego typu obiektu, na który wskazują. - Metoda
rysuj()jest wywoływana dla każdego obiektu, pokazując polimorficzne zachowanie. - Następuje zwolnienie zasobów za pomocą
delete, co jest ważne przy zarządzaniu pamięcią dynamiczną w C++.
| Testy przypięte do lekcji | |
|---|---|
| Aby uzyskać dostęp do testów i ćwiczeń interaktywnych - Zaloguj się |